Skip to content

Save/Load Editor Splits #13510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down
52 changes: 52 additions & 0 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2585,6 +2585,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",
Expand Down Expand Up @@ -3283,6 +3313,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: &[],
Expand Down
13 changes: 13 additions & 0 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,4 +738,17 @@ pub mod completers {

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()
}
}
171 changes: 170 additions & 1 deletion helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1047,6 +1048,33 @@ pub struct Breakpoint {
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};

type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>;
Expand All @@ -1055,6 +1083,7 @@ pub struct Editor {
/// Current editing mode.
pub mode: Mode,
pub tree: Tree,
pub split_info: HashMap<String, SplitEntryTree>,
pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>,

Expand Down Expand Up @@ -1179,6 +1208,7 @@ impl Action {
}

/// Error thrown on failed document closed
#[derive(Debug)]
pub enum CloseError {
/// Document doesn't exist
DoesNotExist,
Expand Down Expand Up @@ -1206,6 +1236,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(),
Expand Down Expand Up @@ -1712,6 +1743,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.
fn new_document(&mut self, mut doc: Document) -> DocumentId {
let id = self.next_document_id;
Expand Down
Loading