diff options
Diffstat (limited to 'helix-view')
-rw-r--r-- | helix-view/src/document.rs | 32 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 149 | ||||
-rw-r--r-- | helix-view/src/macros.rs | 40 | ||||
-rw-r--r-- | helix-view/src/tree.rs | 3 | ||||
-rw-r--r-- | helix-view/src/view.rs | 45 |
5 files changed, 202 insertions, 67 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index ce5df8ee..76b19a07 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192; const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); +pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, @@ -96,7 +98,7 @@ pub struct Document { // It can be used as a cell where we will take it out to get some parts of the history and put // it back as it separated from the edits. We could split out the parts manually but that will // be more troublesome. - history: Cell<History>, + pub history: Cell<History>, pub savepoint: Option<Transaction>, @@ -494,7 +496,9 @@ impl Document { /// Detect the programming language based on the file type. pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { if let Some(path) = &self.path { - let language_config = config_loader.language_config_for_file_name(path); + let language_config = config_loader + .language_config_for_file_name(path) + .or_else(|| config_loader.language_config_for_shebang(self.text())); self.set_language(theme, language_config); } } @@ -749,19 +753,35 @@ impl Document { } /// Undo modifications to the [`Document`] according to `uk`. - pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { + pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { let txns = self.history.get_mut().earlier(uk); + let mut success = false; for txn in txns { - self.apply_impl(&txn, view_id); + if self.apply_impl(&txn, view_id) { + success = true; + } } + if success { + // reset changeset to fix len + self.changes = ChangeSet::new(self.text()); + } + success } /// Redo modifications to the [`Document`] according to `uk`. - pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { + pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { let txns = self.history.get_mut().later(uk); + let mut success = false; for txn in txns { - self.apply_impl(&txn, view_id); + if self.apply_impl(&txn, view_id) { + success = true; + } + } + if success { + // reset changeset to fix len + self.changes = ChangeSet::new(self.text()); } + success } /// Commit pending changes to history diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 40c65c4c..f423de84 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,5 +1,6 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, + document::SCRATCH_BUFFER_NAME, graphics::{CursorKind, Rect}, theme::{self, Theme}, tree::{self, Tree}, @@ -11,8 +12,8 @@ use futures_util::stream::select_all::SelectAll; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ - collections::BTreeMap, - collections::HashMap, + collections::{BTreeMap, HashMap}, + io::stdin, path::{Path, PathBuf}, pin::Pin, sync::Arc, @@ -25,8 +26,8 @@ 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_dap as dap; +use helix_core::{Position, Selection}; use serde::Deserialize; @@ -39,7 +40,7 @@ where } #[derive(Debug, Clone, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case", default)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. pub scrolloff: usize, @@ -138,6 +139,8 @@ pub struct Editor { pub idle_timer: Pin<Box<Sleep>>, pub last_motion: Option<Motion>, + + pub exit_code: i32, } #[derive(Debug, Copy, Clone)] @@ -179,6 +182,7 @@ impl Editor { idle_timer: Box::pin(sleep(config.idle_timeout)), last_motion: None, config, + exit_code: 0, } } @@ -208,6 +212,12 @@ impl Editor { } pub fn set_theme(&mut self, theme: Theme) { + // `ui.selection` is the only scope required to be able to render a theme. + if theme.find_scope_index("ui.selection").is_none() { + self.set_error("Invalid theme: `ui.selection` required".to_owned()); + return; + } + let scopes = theme.scopes(); for config in self .syn_loader @@ -238,9 +248,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)"); @@ -274,26 +303,19 @@ 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; } Action::Load => { + let view_id = view!(self).id; + if let Some(doc) = self.document_mut(id) { + if doc.selections().is_empty() { + doc.selections.insert(view_id, Selection::point(0)); + } + } return; } Action::HorizontalSplit => { @@ -315,16 +337,29 @@ impl Editor { self._refresh(); } - pub fn new_file(&mut self, action: Action) -> DocumentId { + fn new_document(&mut self, mut document: Document) -> DocumentId { let id = DocumentId(self.next_document_id); self.next_document_id += 1; - let mut doc = Document::default(); - doc.id = id; - self.documents.insert(id, doc); + 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 } + pub fn new_file(&mut self, action: Action) -> DocumentId { + self.new_file_from_document(action, Document::default()) + } + + pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> { + let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?; + Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding)))) + } + pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> { let path = helix_core::path::get_canonicalized_path(&path)?; @@ -381,7 +416,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 @@ -390,18 +425,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_BUFFER_NAME.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::<Vec<_>>(); + + 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/macros.rs b/helix-view/src/macros.rs index 63d76a42..04f8df94 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -11,10 +11,19 @@ /// Returns `(&mut View, &mut Document)` #[macro_export] macro_rules! current { - ( $( $editor:ident ).+ ) => {{ - let view = $crate::view_mut!( $( $editor ).+ ); + ($editor:expr) => {{ + let view = $crate::view_mut!($editor); let id = view.doc; - let doc = $( $editor ).+ .documents.get_mut(&id).unwrap(); + let doc = $editor.documents.get_mut(&id).unwrap(); + (view, doc) + }}; +} + +#[macro_export] +macro_rules! current_ref { + ($editor:expr) => {{ + let view = $editor.tree.get($editor.tree.focus); + let doc = &$editor.documents[&view.doc]; (view, doc) }}; } @@ -23,8 +32,8 @@ macro_rules! current { /// Returns `&mut Document` #[macro_export] macro_rules! doc_mut { - ( $( $editor:ident ).+ ) => {{ - $crate::current!( $( $editor ).+ ).1 + ($editor:expr) => {{ + $crate::current!($editor).1 }}; } @@ -32,8 +41,8 @@ macro_rules! doc_mut { /// Returns `&mut View` #[macro_export] macro_rules! view_mut { - ( $( $editor:ident ).+ ) => {{ - $( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus) + ($editor:expr) => {{ + $editor.tree.get_mut($editor.tree.focus) }}; } @@ -41,23 +50,14 @@ macro_rules! view_mut { /// Returns `&View` #[macro_export] macro_rules! view { - ( $( $editor:ident ).+ ) => {{ - $( $editor ).+ .tree.get($( $editor ).+ .tree.focus) + ($editor:expr) => {{ + $editor.tree.get($editor.tree.focus) }}; } #[macro_export] macro_rules! doc { - ( $( $editor:ident ).+ ) => {{ - $crate::current_ref!( $( $editor ).+ ).1 - }}; -} - -#[macro_export] -macro_rules! current_ref { - ( $( $editor:ident ).+ ) => {{ - let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus); - let doc = &$( $editor ).+ .documents[&view.doc]; - (view, doc) + ($editor:expr) => {{ + $crate::current_ref!($editor).1 }}; } 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 ee236e94..02aa1327 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)] @@ -85,7 +89,12 @@ impl View { self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline } - pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + // + pub fn offset_coords_to_in_view( + &self, + doc: &Document, + scrolloff: usize, + ) -> Option<(usize, usize)> { let cursor = doc .selection(self.id) .primary() @@ -104,23 +113,43 @@ impl View { let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; - if line > last_line.saturating_sub(scrolloff) { + let row = if line > last_line.saturating_sub(scrolloff) { // scroll down - self.offset.row += line - (last_line.saturating_sub(scrolloff)); + self.offset.row + line - (last_line.saturating_sub(scrolloff)) } else if line < self.offset.row + scrolloff { // scroll up - self.offset.row = line.saturating_sub(scrolloff); - } + line.saturating_sub(scrolloff) + } else { + self.offset.row + }; - if col > last_col.saturating_sub(scrolloff) { + let col = if col > last_col.saturating_sub(scrolloff) { // scroll right - self.offset.col += col - (last_col.saturating_sub(scrolloff)); + self.offset.col + col - (last_col.saturating_sub(scrolloff)) } else if col < self.offset.col + scrolloff { // scroll left - self.offset.col = col.saturating_sub(scrolloff); + col.saturating_sub(scrolloff) + } else { + self.offset.col + }; + if row == self.offset.row && col == self.offset.col { + None + } else { + Some((row, col)) + } + } + + pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) { + self.offset.row = row; + self.offset.col = col; } } + pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool { + self.offset_coords_to_in_view(doc, scrolloff).is_none() + } + /// Calculates the last visible line on screen #[inline] pub fn last_line(&self, doc: &Document) -> usize { |