diff options
author | Blaž Hrastnik | 2021-11-06 15:28:19 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2021-11-06 15:28:19 +0000 |
commit | f2b709a3c3a9cc036bfea46734efd7e4100eb34b (patch) | |
tree | ad5f921f13659e5ba395442e13389af317ee81b0 /helix-core/src | |
parent | cde57dae356021c6ca8c2a2ed68777bd9d0bc0b2 (diff) | |
parent | f979bdc442ab3150a369ff8bee0703e90e32e2a4 (diff) |
Merge branch 'master' into debug
Diffstat (limited to 'helix-core/src')
-rw-r--r-- | helix-core/src/auto_pairs.rs | 3 | ||||
-rw-r--r-- | helix-core/src/chars.rs | 2 | ||||
-rw-r--r-- | helix-core/src/comment.rs | 5 | ||||
-rw-r--r-- | helix-core/src/diagnostic.rs | 7 | ||||
-rw-r--r-- | helix-core/src/graphemes.rs | 4 | ||||
-rw-r--r-- | helix-core/src/history.rs | 105 | ||||
-rw-r--r-- | helix-core/src/indent.rs | 1 | ||||
-rw-r--r-- | helix-core/src/lib.rs | 3 | ||||
-rw-r--r-- | helix-core/src/line_ending.rs | 6 | ||||
-rw-r--r-- | helix-core/src/movement.rs | 4 | ||||
-rw-r--r-- | helix-core/src/object.rs | 9 | ||||
-rw-r--r-- | helix-core/src/position.rs | 81 | ||||
-rw-r--r-- | helix-core/src/register.rs | 4 | ||||
-rw-r--r-- | helix-core/src/selection.rs | 11 | ||||
-rw-r--r-- | helix-core/src/syntax.rs | 90 | ||||
-rw-r--r-- | helix-core/src/textobject.rs | 51 | ||||
-rw-r--r-- | helix-core/src/transaction.rs | 10 |
17 files changed, 296 insertions, 100 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 9b901e9b..cc966852 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,3 +1,6 @@ +//! When typing the opening character of one of the possible pairs defined below, +//! this module provides the functionality to insert the paired closing character. + use crate::{Range, Rope, Selection, Tendril, Transaction}; use smallvec::SmallVec; diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs index 24133dd3..c8e5efbd 100644 --- a/helix-core/src/chars.rs +++ b/helix-core/src/chars.rs @@ -1,3 +1,5 @@ +//! Utility functions to categorize a `char`. + use crate::LineEnding; #[derive(Debug, Eq, PartialEq)] diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 3d8e1ce3..b22a95a6 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -1,3 +1,6 @@ +//! This module contains the functionality toggle comments on lines over the selection +//! using the comment character defined in the user's `languages.toml` + use crate::{ find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, }; @@ -60,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st let token = token.unwrap_or("//"); let comment = Tendril::from(format!("{} ", token)); - let mut lines: Vec<usize> = Vec::new(); + let mut lines: Vec<usize> = Vec::with_capacity(selection.len()); let mut min_next_line = 0; for selection in selection { diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index e08a71e7..ad1ba16a 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -1,3 +1,6 @@ +//! LSP diagnostic utility types. + +/// Describes the severity level of a [`Diagnostic`]. #[derive(Debug, Eq, PartialEq)] pub enum Severity { Error, @@ -6,12 +9,14 @@ pub enum Severity { Hint, } -#[derive(Debug)] +/// A range of `char`s within the text. +#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)] pub struct Range { pub start: usize, pub end: usize, } +/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html) #[derive(Debug)] pub struct Diagnostic { pub range: Range, diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index 0465fe51..c6398875 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -1,4 +1,6 @@ -// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs +//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents. +//! +//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs> use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice}; use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; use unicode_width::UnicodeWidthStr; diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 67ded166..b53c01fe 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -4,48 +4,50 @@ use regex::Regex; use std::num::NonZeroUsize; use std::time::{Duration, Instant}; -// Stores the history of changes to a buffer. -// -// Currently the history is represented as a vector of revisions. The vector -// always has at least one element: the empty root revision. Each revision -// with the exception of the root has a parent revision, a [Transaction] -// that can be applied to its parent to transition from the parent to itself, -// and an inversion of that transaction to transition from the parent to its -// latest child. -// -// When using `u` to undo a change, an inverse of the stored transaction will -// be applied which will transition the buffer to the parent state. -// -// Each revision with the exception of the last in the vector also has a -// last child revision. When using `U` to redo a change, the last child transaction -// will be applied to the current state of the buffer. -// -// The current revision is the one currently displayed in the buffer. -// -// Commiting a new revision to the history will update the last child of the -// current revision, and push a new revision to the end of the vector. -// -// Revisions are commited with a timestamp. :earlier and :later can be used -// to jump to the closest revision to a moment in time relative to the timestamp -// of the current revision plus (:later) or minus (:earlier) the duration -// given to the command. If a single integer is given, the editor will instead -// jump the given number of revisions in the vector. -// -// Limitations: -// * Changes in selections currently don't commit history changes. The selection -// will only be updated to the state after a commited buffer change. -// * The vector of history revisions is currently unbounded. This might -// cause the memory consumption to grow significantly large during long -// editing sessions. -// * Because delete transactions currently don't store the text that they -// delete, we also store an inversion of the transaction. +/// Stores the history of changes to a buffer. +/// +/// Currently the history is represented as a vector of revisions. The vector +/// always has at least one element: the empty root revision. Each revision +/// with the exception of the root has a parent revision, a [Transaction] +/// that can be applied to its parent to transition from the parent to itself, +/// and an inversion of that transaction to transition from the parent to its +/// latest child. +/// +/// When using `u` to undo a change, an inverse of the stored transaction will +/// be applied which will transition the buffer to the parent state. +/// +/// Each revision with the exception of the last in the vector also has a +/// last child revision. When using `U` to redo a change, the last child transaction +/// will be applied to the current state of the buffer. +/// +/// The current revision is the one currently displayed in the buffer. +/// +/// Commiting a new revision to the history will update the last child of the +/// current revision, and push a new revision to the end of the vector. +/// +/// Revisions are commited with a timestamp. :earlier and :later can be used +/// to jump to the closest revision to a moment in time relative to the timestamp +/// of the current revision plus (:later) or minus (:earlier) the duration +/// given to the command. If a single integer is given, the editor will instead +/// jump the given number of revisions in the vector. +/// +/// Limitations: +/// * Changes in selections currently don't commit history changes. The selection +/// will only be updated to the state after a commited buffer change. +/// * The vector of history revisions is currently unbounded. This might +/// cause the memory consumption to grow significantly large during long +/// editing sessions. +/// * Because delete transactions currently don't store the text that they +/// delete, we also store an inversion of the transaction. +/// +/// Using time to navigate the history: <https://github.com/helix-editor/helix/pull/194> #[derive(Debug)] pub struct History { revisions: Vec<Revision>, current: usize, } -// A single point in history. See [History] for more information. +/// A single point in history. See [History] for more information. #[derive(Debug)] struct Revision { parent: usize, @@ -111,6 +113,7 @@ impl History { self.current == 0 } + /// Undo the last edit. pub fn undo(&mut self) -> Option<&Transaction> { if self.at_root() { return None; @@ -121,6 +124,7 @@ impl History { Some(¤t_revision.inversion) } + /// Redo the last edit. pub fn redo(&mut self) -> Option<&Transaction> { let current_revision = &self.revisions[self.current]; let last_child = current_revision.last_child?; @@ -147,8 +151,8 @@ impl History { } } - // List of nodes on the way from `n` to 'a`. Doesn`t include `a`. - // Includes `n` unless `a == n`. `a` must be an ancestor of `n`. + /// List of nodes on the way from `n` to 'a`. Doesn`t include `a`. + /// Includes `n` unless `a == n`. `a` must be an ancestor of `n`. fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> { let mut path = Vec::new(); while n != a { @@ -158,6 +162,7 @@ impl History { path } + /// Create a [`Transaction`] that will jump to a specific revision in the history. fn jump_to(&mut self, to: usize) -> Vec<Transaction> { let lca = self.lowest_common_ancestor(self.current, to); let up = self.path_up(self.current, lca); @@ -171,10 +176,12 @@ impl History { up_txns.chain(down_txns).collect() } + /// Creates a [`Transaction`] that will undo `delta` revisions. fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> { self.jump_to(self.current.saturating_sub(delta)) } + /// Creates a [`Transaction`] that will redo `delta` revisions. fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> { self.jump_to( self.current @@ -183,7 +190,7 @@ impl History { ) } - // Helper for a binary search case below. + /// Helper for a binary search case below. fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize { let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp); let dur_i = self.revisions[i].timestamp.duration_since(instant); @@ -194,6 +201,8 @@ impl History { } } + /// Creates a [`Transaction`] that will match a revision created at around + /// `instant`. fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> { let search_result = self .revisions @@ -209,6 +218,8 @@ impl History { self.jump_to(revision) } + /// Creates a [`Transaction`] that will match a revision created `duration` ago + /// from the timestamp of current revision. fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> { match self.revisions[self.current].timestamp.checked_sub(duration) { Some(instant) => self.jump_instant(instant), @@ -216,6 +227,8 @@ impl History { } } + /// Creates a [`Transaction`] that will match a revision created `duration` in + /// the future from the timestamp of the current revision. fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> { match self.revisions[self.current].timestamp.checked_add(duration) { Some(instant) => self.jump_instant(instant), @@ -223,6 +236,7 @@ impl History { } } + /// Creates an undo [`Transaction`]. pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> { use UndoKind::*; match uk { @@ -231,6 +245,7 @@ impl History { } } + /// Creates a redo [`Transaction`]. pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> { use UndoKind::*; match uk { @@ -240,13 +255,14 @@ impl History { } } +/// Whether to undo by a number of edits or a duration of time. #[derive(Debug, PartialEq)] pub enum UndoKind { Steps(usize), TimePeriod(std::time::Duration), } -// A subset of sytemd.time time span syntax units. +/// A subset of sytemd.time time span syntax units. const TIME_UNITS: &[(&[&str], &str, u64)] = &[ (&["seconds", "second", "sec", "s"], "seconds", 1), (&["minutes", "minute", "min", "m"], "minutes", 60), @@ -254,11 +270,20 @@ const TIME_UNITS: &[(&[&str], &str, u64)] = &[ (&["days", "day", "d"], "days", 24 * 60 * 60), ]; +/// Checks if the duration input can be turned into a valid duration. It must be a +/// positive integer and denote the [unit of time.](`TIME_UNITS`) +/// Examples of valid durations: +/// * `5 sec` +/// * `5 min` +/// * `5 hr` +/// * `5 days` static DURATION_VALIDATION_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap()); +/// Captures both the number and unit as separate capture groups. static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap()); +/// Parse a string (e.g. "5 sec") and try to convert it into a [`Duration`]. fn parse_human_duration(s: &str) -> Result<Duration, String> { if !DURATION_VALIDATION_REGEX.is_match(s) { return Err("duration should be composed \ diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 1f32d038..df158363 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -464,6 +464,7 @@ where unit: String::from(" "), }), indent_query: OnceCell::new(), + textobject_query: OnceCell::new(), debugger: None, }], }); diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 0854eb04..f4284139 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> { line.chars().position(|ch| !ch.is_whitespace()) } +/// Find `.git` root. pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> { let current_dir = std::env::current_dir().expect("unable to determine current directory"); @@ -193,7 +194,7 @@ pub use tendril::StrTendril as Tendril; pub use {regex, tree_sitter}; pub use graphemes::RopeGraphemes; -pub use position::{coords_at_pos, pos_at_coords, Position}; +pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position}; pub use selection::{Range, Selection}; pub use smallvec::{smallvec, SmallVec}; pub use syntax::Syntax; diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 18ea5f9f..3541305c 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -20,7 +20,7 @@ pub enum LineEnding { impl LineEnding { #[inline] - pub fn len_chars(&self) -> usize { + pub const fn len_chars(&self) -> usize { match self { Self::Crlf => 2, _ => 1, @@ -28,7 +28,7 @@ impl LineEnding { } #[inline] - pub fn as_str(&self) -> &'static str { + pub const fn as_str(&self) -> &'static str { match self { Self::Crlf => "\u{000D}\u{000A}", Self::LF => "\u{000A}", @@ -42,7 +42,7 @@ impl LineEnding { } #[inline] - pub fn from_char(ch: char) -> Option<LineEnding> { + pub const fn from_char(ch: char) -> Option<LineEnding> { match ch { '\u{000A}' => Some(LineEnding::LF), '\u{000B}' => Some(LineEnding::VT), diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 5d080545..9e85bd21 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -53,6 +53,10 @@ pub fn move_vertically( let pos = range.cursor(slice); // Compute the current position's 2d coordinates. + // TODO: switch this to use `visual_coords_at_pos` rather than + // `coords_at_pos` as this will cause a jerky movement when the visual + // position does not match, like moving from a line with tabs/CJK to + // a line without let Position { row, col } = coords_at_pos(slice, pos); let horiz = range.horiz.unwrap_or(col as u32); diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index d9558dd8..717c5994 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -13,8 +13,13 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) let parent = match tree .root_node() .descendant_for_byte_range(from, to) - .and_then(|node| node.parent()) - { + .and_then(|node| { + if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) { + node.parent() + } else { + Some(node) + } + }) { Some(parent) => parent, None => return range, }; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 08a8aed5..c6018ce6 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -2,6 +2,7 @@ use crate::{ chars::char_is_line_ending, graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes}, line_ending::line_end_char_index, + unicode::width::UnicodeWidthChar, RopeSlice, }; @@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point { } /// Convert a character index to (line, column) coordinates. /// -/// TODO: this should be split into two methods: one for visual -/// row/column, and one for "objective" row/column (possibly with -/// the column specified in `char`s). The former would be used -/// for cursor movement, and the latter would be used for e.g. the -/// row:column display in the status line. +/// column in `char` count which can be used for row:column display in +/// status line. See [`visual_coords_at_pos`] for a visual one. pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position { let line = text.char_to_line(pos); @@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position { Position::new(line, col) } +/// Convert a character index to (line, column) coordinates visually. +/// +/// Takes \t, double-width characters (CJK) into account as well as text +/// not in the document in the future. +/// See [`coords_at_pos`] for an "objective" one. +pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position { + let line = text.char_to_line(pos); + + let line_start = text.line_to_char(line); + let pos = ensure_grapheme_boundary_prev(text, pos); + let col = text + .slice(line_start..pos) + .chars() + .flat_map(|c| match c { + '\t' => Some(tab_width), + c => UnicodeWidthChar::width(c), + }) + .sum(); + + Position::new(line, col) +} + /// Convert (line, column) coordinates to a character index. /// /// If the `line` coordinate is beyond the end of the file, the EOF @@ -130,7 +150,6 @@ mod test { assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d // Test with wide characters. - // TODO: account for character width. let text = Rope::from("今日はいい\n"); let slice = text.slice(..); assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); @@ -151,7 +170,6 @@ mod test { assert_eq!(coords_at_pos(slice, 9), (1, 0).into()); // Test with wide-character grapheme clusters. - // TODO: account for character width. let text = Rope::from("किमपि\n"); let slice = text.slice(..); assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); @@ -161,7 +179,6 @@ mod test { assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // Test with tabs. - // Todo: account for tab stops. let text = Rope::from("\tHello\n"); let slice = text.slice(..); assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); @@ -170,6 +187,54 @@ mod test { } #[test] + fn test_visual_coords_at_pos() { + let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n + assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w + assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o + assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d + + // Test with wide characters. + let text = Rope::from("今日はいい\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into()); + assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into()); + assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into()); + assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into()); + assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); + + // Test with grapheme clusters. + let text = Rope::from("a̐éö̲\r\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into()); + assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into()); + assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into()); + assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into()); + + // Test with wide-character grapheme clusters. + // TODO: account for cluster. + let text = Rope::from("किमपि\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into()); + assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into()); + assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); + assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); + + // Test with tabs. + let text = Rope::from("\tHello\n"); + let slice = text.slice(..); + assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into()); + assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into()); + assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into()); + } + + #[test] fn test_pos_at_coords() { let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); let slice = text.slice(..); diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index c3e6652e..c5444eb7 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -7,7 +7,7 @@ pub struct Register { } impl Register { - pub fn new(name: char) -> Self { + pub const fn new(name: char) -> Self { Self { name, values: Vec::new(), @@ -18,7 +18,7 @@ impl Register { Self { name, values } } - pub fn name(&self) -> char { + pub const fn name(&self) -> char { self.name } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 18af4d08..f3b5d2c8 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -362,6 +362,11 @@ impl Selection { /// Adds a new range to the selection and makes it the primary range. pub fn remove(mut self, index: usize) -> Self { + assert!( + self.ranges.len() > 1, + "can't remove the last range from a selection!" + ); + self.ranges.remove(index); if index < self.primary_index || self.primary_index == self.ranges.len() { self.primary_index -= 1; @@ -369,6 +374,12 @@ impl Selection { self } + /// Replace a range in the selection with a new range. + pub fn replace(mut self, index: usize, range: Range) -> Self { + self.ranges[index] = range; + self.normalize() + } + /// Map selections over a set of changes. Useful for adjusting the selection position after /// applying changes to a document. pub fn map(self, changes: &ChangeSet) -> Self { diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 441802a5..18504c21 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -49,7 +49,7 @@ pub struct Configuration { #[serde(rename_all = "kebab-case")] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub(crate) language_id: String, + pub language_id: String, pub scope: String, // source.rust pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> @@ -76,6 +76,8 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) indent_query: OnceCell<Option<IndentQuery>>, + #[serde(skip)] + pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>, #[serde(skip_serializing_if = "Option::is_none")] pub debugger: Option<DebugAdapterConfig>, } @@ -160,6 +162,32 @@ pub struct IndentQuery { pub outdent: HashSet<String>, } +#[derive(Debug)] +pub struct TextObjectQuery { + pub query: Query, +} + +impl TextObjectQuery { + /// Run the query on the given node and return sub nodes which match given + /// capture ("function.inside", "class.around", etc). + pub fn capture_nodes<'a>( + &'a self, + capture_name: &str, + node: Node<'a>, + slice: RopeSlice<'a>, + cursor: &'a mut QueryCursor, + ) -> Option<impl Iterator<Item = Node<'a>>> { + let capture_idx = self.query.capture_index_for_name(capture_name)?; + let captures = cursor.captures(&self.query, node, RopeProvider(slice)); + + captures + .filter_map(move |(mat, idx)| { + (mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node) + }) + .into() + } +} + fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> { let path = crate::RUNTIME_DIR .join("queries") @@ -208,13 +236,14 @@ impl LanguageConfiguration { // highlights_query += "\n(ERROR) @error"; let injections_query = read_query(&language, "injections.scm"); - let locals_query = read_query(&language, "locals.scm"); if highlights_query.is_empty() { None } else { - let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?; + let language = get_language(&crate::RUNTIME_DIR, &self.language_id) + .map_err(|e| log::info!("{}", e)) + .ok()?; let config = HighlightConfiguration::new( language, &highlights_query, @@ -258,6 +287,18 @@ impl LanguageConfiguration { .as_ref() } + pub fn textobject_query(&self) -> Option<&TextObjectQuery> { + self.textobject_query + .get_or_init(|| -> Option<TextObjectQuery> { + let lang_name = self.language_id.to_ascii_lowercase(); + let query_text = read_query(&lang_name, "textobjects.scm"); + let lang = self.highlight_config.get()?.as_ref()?.language; + let query = Query::new(lang, &query_text).ok()?; + Some(TextObjectQuery { query }) + }) + .as_ref() + } + pub fn scope(&self) -> &str { &self.scope } @@ -451,7 +492,7 @@ impl Syntax { /// Iterate over the highlighted regions for a given slice of source code. pub fn highlight_iter<'a>( - &self, + &'a self, source: RopeSlice<'a>, range: Option<std::ops::Range<usize>>, cancellation_flag: Option<&'a AtomicUsize>, @@ -466,11 +507,10 @@ impl Syntax { let highlighter = &mut ts_parser.borrow_mut(); highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) }); - let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) }; + let tree_ref = self.tree(); let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; - let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) }; - let config_ref = - unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) }; + let query_ref = &self.config.query; + let config_ref = self.config.as_ref(); // if reusing cursors & no range this resets to whole range cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); @@ -582,39 +622,7 @@ impl LanguageLayer { self.tree.as_ref(), ) .ok_or(Error::Cancelled)?; - // unsafe { syntax.parser.set_cancellation_flag(None) }; - // let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new); - - // Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526) - // if let Some(combined_injections_query) = &config.combined_injections_query { - // let mut injections_by_pattern_index = - // vec![(None, Vec::new(), false); combined_injections_query.pattern_count()]; - // let matches = - // cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source)); - // for mat in matches { - // let entry = &mut injections_by_pattern_index[mat.pattern_index]; - // let (language_name, content_node, include_children) = - // injection_for_match(config, combined_injections_query, &mat, source); - // if language_name.is_some() { - // entry.0 = language_name; - // } - // if let Some(content_node) = content_node { - // entry.1.push(content_node); - // } - // entry.2 = include_children; - // } - // for (lang_name, content_nodes, includes_children) in injections_by_pattern_index { - // if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) { - // if let Some(next_config) = (injection_callback)(lang_name) { - // let ranges = - // Self::intersect_ranges(&ranges, &content_nodes, includes_children); - // if !ranges.is_empty() { - // queue.push((next_config, depth + 1, ranges)); - // } - // } - // } - // } - // } + self.tree = Some(tree) } Ok(()) diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index b965f6df..975ed115 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -1,9 +1,13 @@ +use std::fmt::Display; + use ropey::RopeSlice; +use tree_sitter::{Node, QueryCursor}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::graphemes::next_grapheme_boundary; use crate::movement::Direction; use crate::surround; +use crate::syntax::LanguageConfiguration; use crate::Range; fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { @@ -51,6 +55,15 @@ pub enum TextObject { Inside, } +impl Display for TextObject { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Around => "around", + Self::Inside => "inside", + }) + } +} + // count doesn't do anything yet pub fn textobject_word( slice: RopeSlice, @@ -108,6 +121,44 @@ pub fn textobject_surround( .unwrap_or(range) } +/// Transform the given range to select text objects based on tree-sitter. +/// `object_name` is a query capture base name like "function", "class", etc. +/// `slice_tree` is the tree-sitter node corresponding to given text slice. +pub fn textobject_treesitter( + slice: RopeSlice, + range: Range, + textobject: TextObject, + object_name: &str, + slice_tree: Node, + lang_config: &LanguageConfiguration, + _count: usize, +) -> Range { + let get_range = move || -> Option<Range> { + let byte_pos = slice.char_to_byte(range.cursor(slice)); + + let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner + let mut cursor = QueryCursor::new(); + let node = lang_config + .textobject_query()? + .capture_nodes(&capture_name, slice_tree, slice, &mut cursor)? + .filter(|node| node.byte_range().contains(&byte_pos)) + .min_by_key(|node| node.byte_range().len())?; + + let len = slice.len_bytes(); + let start_byte = node.start_byte(); + let end_byte = node.end_byte(); + if start_byte >= len || end_byte >= len { + return None; + } + + let start_char = slice.byte_to_char(start_byte); + let end_char = slice.byte_to_char(end_byte); + + Some(Range::new(start_char, end_char)) + }; + get_range().unwrap_or(range) +} + #[cfg(test)] mod test { use super::TextObject::*; diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index d682f058..dfc18fbe 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -132,6 +132,9 @@ impl ChangeSet { if self.changes.is_empty() { return other; } + if other.changes.is_empty() { + return self; + } let len = self.changes.len(); @@ -465,6 +468,13 @@ impl Transaction { } } + pub fn compose(mut self, other: Self) -> Self { + self.changes = self.changes.compose(other.changes); + // Other selection takes precedence + self.selection = other.selection; + self + } + pub fn with_selection(mut self, selection: Selection) -> Self { self.selection = Some(selection); self |