From c638b6b60e69697b7e7957ed1af1ac071c41974b Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Mon, 15 Nov 2021 07:30:45 -0800 Subject: [PATCH] helix-term/commands: implement buffer-close (bc, bclose) (#1035) * helix-view/view: impl method to remove document from jumps * helix-view/editor: impl close_document * helix-view/editor: remove close_buffer argument from `close` According to archseer, this was never implemented or used properly. Now that we have a proper "buffer close" function, we can get rid of this. * helix-term/commands: implement buffer-close (bc, bclose) This behaves the same as Kakoune's `delete-buffer` / `db` command: * With 3 files opened by the user with `:o ab`, `:o cd`, and `:o ef`: * `buffer-close` once closes `ef` and switches to `cd` * `buffer-close` again closes `cd` and switches to `ab` * `buffer-close` again closes `ab` and switches to a scratch buffer * With 3 files opened from the command line with `hx -- ab cd ef`: * `buffer-close` once closes `ab` and switches to `cd` * `buffer-close` again closes `cd` and switches to `ef` * `buffer-close` again closes `ef` and switches to a scratch buffer * With 1 file opened (`ab`): * `buffer-close` once closes `ab` and switches to a scratch buffer * `buffer-close` again closes the scratch buffer and switches to a new scratch buffer * helix-term/commands: implement buffer-close! (bclose!, bc!) Namely, if you have a document open in multiple splits, all the splits will be closed at the same time, leaving only splits without that document focused (or a scratch buffer if they were all focused on that buffer). * helix-view/tree: reset focus if Tree is empty --- helix-term/src/commands.rs | 50 +++++++++++++--- helix-view/src/editor.rs | 113 ++++++++++++++++++++++++++++--------- helix-view/src/tree.rs | 3 + helix-view/src/view.rs | 4 ++ 4 files changed, 135 insertions(+), 35 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8c0a005c..c7aab726 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1700,8 +1700,7 @@ mod cmd { buffers_remaining_impl(cx.editor)? } - cx.editor - .close(view!(cx.editor).id, /* close_buffer */ false); + cx.editor.close(view!(cx.editor).id); Ok(()) } @@ -1711,8 +1710,7 @@ mod cmd { _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - cx.editor - .close(view!(cx.editor).id, /* close_buffer */ false); + cx.editor.close(view!(cx.editor).id); Ok(()) } @@ -1730,6 +1728,28 @@ mod cmd { Ok(()) } + fn buffer_close( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let view = view!(cx.editor); + let doc_id = view.doc; + cx.editor.close_document(doc_id, false)?; + Ok(()) + } + + fn force_buffer_close( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let view = view!(cx.editor); + let doc_id = view.doc; + cx.editor.close_document(doc_id, true)?; + Ok(()) + } + fn write_impl>( cx: &mut compositor::Context, path: Option

, @@ -1976,7 +1996,7 @@ mod cmd { // close all views let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - cx.editor.close(view_id, false); + cx.editor.close(view_id); } } @@ -2020,7 +2040,7 @@ mod cmd { // close all views let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - editor.close(view_id, false); + editor.close(view_id); } Ok(()) @@ -2332,6 +2352,20 @@ mod cmd { fun: open, completer: Some(completers::filename), }, + TypableCommand { + name: "buffer-close", + aliases: &["bc", "bclose"], + doc: "Close the current buffer.", + fun: buffer_close, + completer: None, // FIXME: buffer completer + }, + TypableCommand { + name: "buffer-close!", + aliases: &["bc!", "bclose!"], + doc: "Close the current buffer forcefully (ignoring unsaved changes).", + fun: force_buffer_close, + completer: None, // FIXME: buffer completer + }, TypableCommand { name: "write", aliases: &["w"], @@ -4914,7 +4948,7 @@ fn wclose(cx: &mut Context) { } let view_id = view!(cx.editor).id; // close current split - cx.editor.close(view_id, /* close_buffer */ false); + cx.editor.close(view_id); } fn wonly(cx: &mut Context) { @@ -4926,7 +4960,7 @@ fn wonly(cx: &mut Context) { .collect::>(); for (view_id, focus) in views { if !focus { - cx.editor.close(view_id, /* close_buffer */ false); + cx.editor.close(view_id); } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e4015707..4712c52a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -22,7 +22,7 @@ use anyhow::Error; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; use helix_core::syntax; -use helix_core::Position; +use helix_core::{Position, Selection}; use serde::Deserialize; @@ -235,9 +235,28 @@ impl Editor { } } + fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) { + let view = self.tree.get_mut(current_view); + view.doc = doc_id; + view.offset = Position::default(); + + let doc = self.documents.get_mut(&doc_id).unwrap(); + + // initialize selection for view + doc.selections + .entry(view.id) + .or_insert_with(|| Selection::point(0)); + // TODO: reuse align_view + let pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + let line = doc.text().char_to_line(pos); + view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + } + pub fn switch(&mut self, id: DocumentId, action: Action) { use crate::tree::Layout; - use helix_core::Selection; if !self.documents.contains_key(&id) { log::error!("cannot switch to document that does not exist (anymore)"); @@ -271,22 +290,9 @@ impl Editor { view.jumps.push(jump); view.last_accessed_doc = Some(view.doc); } - view.doc = id; - view.offset = Position::default(); - - let (view, doc) = current!(self); - // initialize selection for view - doc.selections - .entry(view.id) - .or_insert_with(|| Selection::point(0)); - // TODO: reuse align_view - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let line = doc.text().char_to_line(pos); - view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + let view_id = view.id; + self.replace_document_in_view(view_id, id); return; } @@ -318,11 +324,16 @@ impl Editor { self._refresh(); } - fn new_file_from_document(&mut self, action: Action, mut document: Document) -> DocumentId { + fn new_document(&mut self, mut document: Document) -> DocumentId { let id = DocumentId(self.next_document_id); self.next_document_id += 1; document.id = id; self.documents.insert(id, document); + id + } + + fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId { + let id = self.new_document(document); self.switch(id, action); id } @@ -392,7 +403,7 @@ impl Editor { Ok(id) } - pub fn close(&mut self, id: ViewId, close_buffer: bool) { + pub fn close(&mut self, id: ViewId) { let view = self.tree.get(self.tree.focus); // remove selection self.documents @@ -401,18 +412,66 @@ impl Editor { .selections .remove(&id); - if close_buffer { - // get around borrowck issues - let doc = &self.documents[&view.doc]; + self.tree.remove(id); + self._refresh(); + } - if let Some(language_server) = doc.language_server() { - tokio::spawn(language_server.text_document_did_close(doc.identifier())); - } - self.documents.remove(&view.doc); + pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> { + let doc = match self.documents.get(&doc_id) { + Some(doc) => doc, + None => anyhow::bail!("document does not exist"), + }; + + if !force && doc.is_modified() { + anyhow::bail!( + "buffer {:?} is modified", + doc.relative_path() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "[scratch]".into()) + ); + } + + if let Some(language_server) = doc.language_server() { + tokio::spawn(language_server.text_document_did_close(doc.identifier())); + } + + let views_to_close = self + .tree + .views() + .filter_map(|(view, _focus)| { + if view.doc == doc_id { + Some(view.id) + } else { + None + } + }) + .collect::>(); + + for view_id in views_to_close { + self.close(view_id); + } + + self.documents.remove(&doc_id); + + // If the document we removed was visible in all views, we will have no more views. We don't + // want to close the editor just for a simple buffer close, so we need to create a new view + // containing either an existing document, or a brand new document. + if self.tree.views().peekable().peek().is_none() { + let doc_id = self + .documents + .iter() + .map(|(&doc_id, _)| doc_id) + .next() + .unwrap_or_else(|| self.new_document(Document::default())); + let view = View::new(doc_id); + let view_id = self.tree.insert(view); + let doc = self.documents.get_mut(&doc_id).unwrap(); + doc.selections.insert(view_id, Selection::point(0)); } - self.tree.remove(id); self._refresh(); + + Ok(()) } pub fn resize(&mut self, area: Rect) { diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 064334b1..de5046ac 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -314,6 +314,9 @@ impl Tree { pub fn recalculate(&mut self) { if self.is_empty() { + // There are no more views, so the tree should focus itself again. + self.focus = self.root; + return; } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 6a624ded..a77f1562 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -54,6 +54,10 @@ impl JumpList { None } } + + pub fn remove(&mut self, doc_id: &DocumentId) { + self.jumps.retain(|(other_id, _)| other_id != doc_id); + } } #[derive(Debug)]