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. |
|
||||
| `: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). |
|
||||
|
|
|
@ -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: &[],
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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)>>;
|
||||
|
@ -1065,6 +1093,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>,
|
||||
|
||||
|
@ -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<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;
|
||||
|
|
|
@ -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 {
|
||||
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::<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