diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 219f6b95f..5f94d658a 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -63,6 +63,8 @@ | `:vsplit-new`, `:vnew` | Open a scratch buffer in a vertical split. | | `:hsplit`, `:hs`, `:sp` | Open the file 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. | | `:goto`, `:g` | Goto line number. | | `:set-language`, `:lang` | Set the language of current buffer (show current language if no value specified). | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2013a9d81..3e086e573 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2581,6 +2581,36 @@ const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[ 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] = &[ TypableCommand { name: "quit", @@ -3279,6 +3309,28 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ ..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 { name: "tutor", aliases: &[], diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 106bfbfb8..151d3ff0f 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -738,4 +738,17 @@ pub mod completers { completions } + + pub fn splits(editor: &Editor, input: &str) -> Vec { + 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() + } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index cb9586e79..7e7aaca25 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -11,7 +11,8 @@ use crate::{ input::KeyEvent, register::Registers, theme::{self, Theme}, - tree::{self, Tree}, + tree::{self, Tree, TreeInfoTree}, + view::ViewPosition, Document, DocumentId, View, ViewId, }; use dap::StackFrame; @@ -1057,6 +1058,33 @@ pub struct Breakpoint { pub log_message: Option, } +// 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, +} +#[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), + Node(SplitEntryNode), +} + use futures_util::stream::{Flatten, Once}; type Diagnostics = BTreeMap>; @@ -1065,6 +1093,7 @@ pub struct Editor { /// Current editing mode. pub mode: Mode, pub tree: Tree, + pub split_info: HashMap, pub next_document_id: DocumentId, pub documents: BTreeMap, @@ -1189,6 +1218,7 @@ impl Action { } /// Error thrown on failed document closed +#[derive(Debug)] pub enum CloseError { /// Document doesn't exist DoesNotExist, @@ -1216,6 +1246,7 @@ impl Editor { Self { mode: Mode::Normal, tree: Tree::new(area), + split_info: HashMap::new(), next_document_id: DocumentId::default(), documents: BTreeMap::new(), saves: HashMap::new(), @@ -1722,6 +1753,144 @@ impl Editor { } } + fn get_path_from_view_id(&self, view_id: ViewId) -> Option { + 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 { + 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. fn new_document(&mut self, mut doc: Document) -> DocumentId { let id = self.next_document_id; diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index aba947a21..8eac20b1a 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -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, +} +#[derive(PartialEq, Debug)] +pub enum TreeInfoTree { + Leaf(ViewId), + Node(TreeInfoNode), +} +#[derive(PartialEq, Debug)] +pub struct TreeInfo { + pub focus: ViewId, + pub tree: TreeInfoTree, +} + impl Tree { pub fn new(area: Rect) -> Self { let root = Node::container(Layout::Vertical); @@ -669,6 +688,35 @@ impl Tree { pub fn area(&self) -> Rect { 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)] @@ -966,4 +1014,89 @@ mod test { .collect::>() ); } + + #[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)], + }) + ] + }), + ) + } }