mirror of https://github.com/helix-editor/helix
Merge 163d10448f
into 205e7ece70
commit
51168d97d9
|
@ -63,6 +63,8 @@
|
||||||
| `:vsplit-new`, `:vnew` | Open a scratch buffer in a vertical split. |
|
| `:vsplit-new`, `:vnew` | Open a scratch buffer in a vertical split. |
|
||||||
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
|
| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
|
||||||
| `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
|
| `:hsplit-new`, `:hnew` | Open a scratch buffer in a horizontal split. |
|
||||||
|
| `:save-splits` | Save the current split with the name specified as argument or a default name is none provided. |
|
||||||
|
| `:load-splits` | Loads the specified split or the default one if not name is provided. |
|
||||||
| `:tutor` | Open the tutorial. |
|
| `:tutor` | Open the tutorial. |
|
||||||
| `:goto`, `:g` | Goto line number. |
|
| `:goto`, `:g` | Goto line number. |
|
||||||
| `:set-language`, `:lang` | Set the language of current buffer (show current language if no value specified). |
|
| `:set-language`, `:lang` | Set the language of current buffer (show current language if no value specified). |
|
||||||
|
|
|
@ -2581,6 +2581,36 @@ const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[
|
||||||
completers::repeating_filenames,
|
completers::repeating_filenames,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
fn save_splits(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure!(args.len() <= 1, ":save-splits takes at most one argument");
|
||||||
|
|
||||||
|
cx.editor.save_split(match args.len() {
|
||||||
|
0 => "".to_string(),
|
||||||
|
_ => args.first().unwrap().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_splits(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
|
||||||
|
if event != PromptEvent::Validate {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure!(args.len() <= 1, ":load-splits takes at most one argument");
|
||||||
|
|
||||||
|
cx.editor.load_split(match args.len() {
|
||||||
|
0 => "".to_string(),
|
||||||
|
_ => args.first().unwrap().to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "quit",
|
name: "quit",
|
||||||
|
@ -3279,6 +3309,28 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
..Signature::DEFAULT
|
..Signature::DEFAULT
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "save-splits",
|
||||||
|
aliases: &[],
|
||||||
|
doc: "Save the current split with the name specified as argument or a default name is none provided.",
|
||||||
|
fun: save_splits,
|
||||||
|
completer: CommandCompleter::none(),
|
||||||
|
signature: Signature {
|
||||||
|
positionals: (0, None),
|
||||||
|
..Signature::DEFAULT
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "load-splits",
|
||||||
|
aliases: &[],
|
||||||
|
doc: "Loads the specified split or the default one if not name is provided.",
|
||||||
|
fun: load_splits,
|
||||||
|
completer: CommandCompleter::all(completers::splits),
|
||||||
|
signature: Signature {
|
||||||
|
positionals: (0, None),
|
||||||
|
..Signature::DEFAULT
|
||||||
|
},
|
||||||
|
},
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "tutor",
|
name: "tutor",
|
||||||
aliases: &[],
|
aliases: &[],
|
||||||
|
|
|
@ -738,4 +738,17 @@ pub mod completers {
|
||||||
|
|
||||||
completions
|
completions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn splits(editor: &Editor, input: &str) -> Vec<Completion> {
|
||||||
|
let iter = editor
|
||||||
|
.split_info
|
||||||
|
.keys()
|
||||||
|
.filter(|k| !k.is_empty())
|
||||||
|
.map(|k| k.to_string());
|
||||||
|
|
||||||
|
fuzzy_match(input, iter, false)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, _)| ((0..), name.into()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ use crate::{
|
||||||
input::KeyEvent,
|
input::KeyEvent,
|
||||||
register::Registers,
|
register::Registers,
|
||||||
theme::{self, Theme},
|
theme::{self, Theme},
|
||||||
tree::{self, Tree},
|
tree::{self, Tree, TreeInfoTree},
|
||||||
|
view::ViewPosition,
|
||||||
Document, DocumentId, View, ViewId,
|
Document, DocumentId, View, ViewId,
|
||||||
};
|
};
|
||||||
use dap::StackFrame;
|
use dap::StackFrame;
|
||||||
|
@ -1057,6 +1058,33 @@ pub struct Breakpoint {
|
||||||
pub log_message: Option<String>,
|
pub log_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data structures to represent a split view.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Layout {
|
||||||
|
Horizontal,
|
||||||
|
Vertical,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SplitEntryNode {
|
||||||
|
pub layout: Layout,
|
||||||
|
pub children: Vec<SplitEntryTree>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SplitEntryLeaf {
|
||||||
|
// Path to the document.
|
||||||
|
pub path: PathBuf,
|
||||||
|
// Where was the position of the view.
|
||||||
|
pub view_position: ViewPosition,
|
||||||
|
pub selection: Selection,
|
||||||
|
// Whether this was the focused split or not.
|
||||||
|
pub focus: bool,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum SplitEntryTree {
|
||||||
|
Leaf(Option<SplitEntryLeaf>),
|
||||||
|
Node(SplitEntryNode),
|
||||||
|
}
|
||||||
|
|
||||||
use futures_util::stream::{Flatten, Once};
|
use futures_util::stream::{Flatten, Once};
|
||||||
|
|
||||||
type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>;
|
type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>;
|
||||||
|
@ -1065,6 +1093,7 @@ pub struct Editor {
|
||||||
/// Current editing mode.
|
/// Current editing mode.
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub tree: Tree,
|
pub tree: Tree,
|
||||||
|
pub split_info: HashMap<String, SplitEntryTree>,
|
||||||
pub next_document_id: DocumentId,
|
pub next_document_id: DocumentId,
|
||||||
pub documents: BTreeMap<DocumentId, Document>,
|
pub documents: BTreeMap<DocumentId, Document>,
|
||||||
|
|
||||||
|
@ -1189,6 +1218,7 @@ impl Action {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error thrown on failed document closed
|
/// Error thrown on failed document closed
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum CloseError {
|
pub enum CloseError {
|
||||||
/// Document doesn't exist
|
/// Document doesn't exist
|
||||||
DoesNotExist,
|
DoesNotExist,
|
||||||
|
@ -1216,6 +1246,7 @@ impl Editor {
|
||||||
Self {
|
Self {
|
||||||
mode: Mode::Normal,
|
mode: Mode::Normal,
|
||||||
tree: Tree::new(area),
|
tree: Tree::new(area),
|
||||||
|
split_info: HashMap::new(),
|
||||||
next_document_id: DocumentId::default(),
|
next_document_id: DocumentId::default(),
|
||||||
documents: BTreeMap::new(),
|
documents: BTreeMap::new(),
|
||||||
saves: HashMap::new(),
|
saves: HashMap::new(),
|
||||||
|
@ -1722,6 +1753,144 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_path_from_view_id(&self, view_id: ViewId) -> Option<PathBuf> {
|
||||||
|
let doc = self.tree.try_get(view_id).unwrap().doc;
|
||||||
|
let doc = &self.documents[&doc];
|
||||||
|
|
||||||
|
doc.path().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_split_tree(&self, focus_id: ViewId, tree_info: TreeInfoTree) -> SplitEntryTree {
|
||||||
|
match tree_info {
|
||||||
|
TreeInfoTree::Leaf(view_id) => {
|
||||||
|
SplitEntryTree::Leaf(self.get_path_from_view_id(view_id).map(|path| {
|
||||||
|
let doc = self.tree.try_get(view_id).unwrap().doc;
|
||||||
|
let doc = self.document(doc).unwrap();
|
||||||
|
SplitEntryLeaf {
|
||||||
|
path,
|
||||||
|
view_position: doc.view_offset(view_id),
|
||||||
|
selection: doc.selection(view_id).clone(),
|
||||||
|
focus: view_id == focus_id,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
TreeInfoTree::Node(node) => {
|
||||||
|
let mut children = Vec::with_capacity(node.children.len());
|
||||||
|
|
||||||
|
for child in node.children {
|
||||||
|
children.push(self.get_split_tree(focus_id, child));
|
||||||
|
}
|
||||||
|
|
||||||
|
let layout = match node.layout {
|
||||||
|
tree::Layout::Horizontal => Layout::Horizontal,
|
||||||
|
tree::Layout::Vertical => Layout::Vertical,
|
||||||
|
};
|
||||||
|
|
||||||
|
SplitEntryTree::Node(SplitEntryNode { layout, children })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_split(&mut self, split_name: String) {
|
||||||
|
let tree_info = self.tree.get_tree_info();
|
||||||
|
|
||||||
|
self.split_info.insert(
|
||||||
|
split_name,
|
||||||
|
self.get_split_tree(tree_info.focus, tree_info.tree),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_split_tree(&mut self, focus: ViewId, split_tree: &SplitEntryTree) -> Option<ViewId> {
|
||||||
|
self.focus(focus);
|
||||||
|
|
||||||
|
match split_tree {
|
||||||
|
SplitEntryTree::Leaf(leaf) => {
|
||||||
|
let leaf = match leaf {
|
||||||
|
Some(l) => l,
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
match self.open(&leaf.path, Action::Replace) {
|
||||||
|
Err(err) => {
|
||||||
|
self.set_error(format!(
|
||||||
|
"Unable to load split for '{}': {}",
|
||||||
|
leaf.path.to_string_lossy(),
|
||||||
|
err
|
||||||
|
));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let (view, doc) = current!(self);
|
||||||
|
|
||||||
|
doc.set_view_offset(view.id, leaf.view_position);
|
||||||
|
doc.set_selection(view.id, leaf.selection.clone());
|
||||||
|
|
||||||
|
if leaf.focus {
|
||||||
|
Some(view.id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SplitEntryTree::Node(node) => {
|
||||||
|
let mut view_child_pairs = Vec::with_capacity(node.children.len());
|
||||||
|
for child in &node.children {
|
||||||
|
let layout = match node.layout {
|
||||||
|
Layout::Horizontal => Action::HorizontalSplit,
|
||||||
|
Layout::Vertical => Action::VerticalSplit,
|
||||||
|
};
|
||||||
|
let _ = self.new_file(layout);
|
||||||
|
view_child_pairs.push((view!(self).id, child));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut to_focus = None;
|
||||||
|
for (view, child) in view_child_pairs {
|
||||||
|
let f = self.load_split_tree(view, child);
|
||||||
|
assert!(!(to_focus.is_some() && f.is_some()));
|
||||||
|
to_focus = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the temporal view and buffer.
|
||||||
|
let doc_to_close = self.tree.get(focus).doc;
|
||||||
|
self.close(focus);
|
||||||
|
self.close_document(doc_to_close, true).unwrap();
|
||||||
|
|
||||||
|
to_focus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_split(&mut self, split_name: String) -> anyhow::Result<()> {
|
||||||
|
if !self.split_info.contains_key(&split_name) {
|
||||||
|
anyhow::bail!(format!("Split '{}' doesn't exist.", split_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// First let's close all the views currently open. Note that we need to
|
||||||
|
// skip one, otherwise we end up without views to work with.
|
||||||
|
let views: Vec<_> = self.tree.views().skip(1).map(|(view, _)| view.id).collect();
|
||||||
|
for view_id in views {
|
||||||
|
self.close(view_id);
|
||||||
|
}
|
||||||
|
let _ = self.new_file(Action::Replace);
|
||||||
|
|
||||||
|
// Get the split that the user asked for.
|
||||||
|
let split_entry_tree = match self.split_info.get(&split_name) {
|
||||||
|
Some(se) => se,
|
||||||
|
None => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load the split.
|
||||||
|
let focus = self.load_split_tree(self.tree.focus, &split_entry_tree.clone());
|
||||||
|
|
||||||
|
// Lets focus to the view we are suppose to.
|
||||||
|
match focus {
|
||||||
|
Some(f) => self.focus(f),
|
||||||
|
None => bail!("Unable to load and focus splits"),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate an id for a new document and register it.
|
/// Generate an id for a new document and register it.
|
||||||
fn new_document(&mut self, mut doc: Document) -> DocumentId {
|
fn new_document(&mut self, mut doc: Document) -> DocumentId {
|
||||||
let id = self.next_document_id;
|
let id = self.next_document_id;
|
||||||
|
|
|
@ -83,6 +83,25 @@ impl Default for Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These structures are a compact representation of a tree. A representation of
|
||||||
|
// the current tree can be obtained with the method `get_tree_info`. They are
|
||||||
|
// used by the editor to save splits and potentially load it later.
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct TreeInfoNode {
|
||||||
|
pub layout: Layout,
|
||||||
|
pub children: Vec<TreeInfoTree>,
|
||||||
|
}
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub enum TreeInfoTree {
|
||||||
|
Leaf(ViewId),
|
||||||
|
Node(TreeInfoNode),
|
||||||
|
}
|
||||||
|
#[derive(PartialEq, Debug)]
|
||||||
|
pub struct TreeInfo {
|
||||||
|
pub focus: ViewId,
|
||||||
|
pub tree: TreeInfoTree,
|
||||||
|
}
|
||||||
|
|
||||||
impl Tree {
|
impl Tree {
|
||||||
pub fn new(area: Rect) -> Self {
|
pub fn new(area: Rect) -> Self {
|
||||||
let root = Node::container(Layout::Vertical);
|
let root = Node::container(Layout::Vertical);
|
||||||
|
@ -669,6 +688,35 @@ impl Tree {
|
||||||
pub fn area(&self) -> Rect {
|
pub fn area(&self) -> Rect {
|
||||||
self.area
|
self.area
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_tree_info_impl(&self, node: &Node) -> TreeInfoTree {
|
||||||
|
match &node.content {
|
||||||
|
Content::View(view) => TreeInfoTree::Leaf(view.id),
|
||||||
|
Content::Container(container) => {
|
||||||
|
let mut children = Vec::with_capacity(container.children.len());
|
||||||
|
|
||||||
|
for child in &container.children {
|
||||||
|
let node = &self.nodes[*child];
|
||||||
|
children.push(self.get_tree_info_impl(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
TreeInfoTree::Node(TreeInfoNode {
|
||||||
|
layout: container.layout,
|
||||||
|
children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tree_info(&self) -> TreeInfo {
|
||||||
|
let root = self.root;
|
||||||
|
let root = &self.nodes[root];
|
||||||
|
|
||||||
|
TreeInfo {
|
||||||
|
focus: self.focus,
|
||||||
|
tree: self.get_tree_info_impl(root),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -966,4 +1014,89 @@ mod test {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_empty_tree_info() {
|
||||||
|
let tree_area_width = 180;
|
||||||
|
let tree = Tree::new(Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: tree_area_width,
|
||||||
|
height: 80,
|
||||||
|
});
|
||||||
|
|
||||||
|
let tree_info = tree.get_tree_info();
|
||||||
|
|
||||||
|
assert_eq!(tree_info.focus, tree.root);
|
||||||
|
assert_eq!(
|
||||||
|
tree_info.tree,
|
||||||
|
TreeInfoTree::Node(TreeInfoNode {
|
||||||
|
layout: Layout::Vertical,
|
||||||
|
children: vec![],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_two_node_tree_info() {
|
||||||
|
let tree_area_width = 180;
|
||||||
|
let mut tree = Tree::new(Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: tree_area_width,
|
||||||
|
height: 80,
|
||||||
|
});
|
||||||
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
||||||
|
let view_id1 = tree.insert(view);
|
||||||
|
|
||||||
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
||||||
|
let view_id2 = tree.insert(view);
|
||||||
|
|
||||||
|
let tree_info = tree.get_tree_info();
|
||||||
|
|
||||||
|
assert_eq!(tree_info.focus, view_id2);
|
||||||
|
assert_eq!(
|
||||||
|
tree_info.tree,
|
||||||
|
TreeInfoTree::Node(TreeInfoNode {
|
||||||
|
layout: Layout::Vertical,
|
||||||
|
children: vec![TreeInfoTree::Leaf(view_id1), TreeInfoTree::Leaf(view_id2)],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_two_level_tree_info() {
|
||||||
|
let tree_area_width = 180;
|
||||||
|
let mut tree = Tree::new(Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: tree_area_width,
|
||||||
|
height: 80,
|
||||||
|
});
|
||||||
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
||||||
|
let view_id1 = tree.insert(view);
|
||||||
|
|
||||||
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
||||||
|
let view_id2 = tree.insert(view);
|
||||||
|
|
||||||
|
let view = View::new(DocumentId::default(), GutterConfig::default());
|
||||||
|
let view_id3 = tree.split(view, Layout::Horizontal);
|
||||||
|
|
||||||
|
let tree_info = tree.get_tree_info();
|
||||||
|
|
||||||
|
assert_eq!(tree_info.focus, view_id3);
|
||||||
|
assert_eq!(
|
||||||
|
tree_info.tree,
|
||||||
|
TreeInfoTree::Node(TreeInfoNode {
|
||||||
|
layout: Layout::Vertical,
|
||||||
|
children: vec![
|
||||||
|
TreeInfoTree::Leaf(view_id1),
|
||||||
|
TreeInfoTree::Node(TreeInfoNode {
|
||||||
|
layout: Layout::Horizontal,
|
||||||
|
children: vec![TreeInfoTree::Leaf(view_id2), TreeInfoTree::Leaf(view_id3)],
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue