pull/13510/merge
Luis Useche 2025-06-14 23:57:52 +03:00 committed by GitHub
commit 51168d97d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 370 additions and 1 deletions

View File

@ -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). |

View File

@ -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: &[],

View File

@ -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()
}
} }

View File

@ -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;

View File

@ -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)],
})
]
}),
)
}
} }