aboutsummaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view')
-rw-r--r--helix-view/src/document.rs32
-rw-r--r--helix-view/src/editor.rs149
-rw-r--r--helix-view/src/macros.rs40
-rw-r--r--helix-view/src/tree.rs3
-rw-r--r--helix-view/src/view.rs45
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 {