aboutsummaryrefslogtreecommitdiff
path: root/helix-core
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core')
-rw-r--r--helix-core/Cargo.toml7
-rw-r--r--helix-core/src/auto_pairs.rs3
-rw-r--r--helix-core/src/chars.rs2
-rw-r--r--helix-core/src/comment.rs5
-rw-r--r--helix-core/src/diagnostic.rs7
-rw-r--r--helix-core/src/graphemes.rs4
-rw-r--r--helix-core/src/history.rs105
-rw-r--r--helix-core/src/indent.rs1
-rw-r--r--helix-core/src/lib.rs3
-rw-r--r--helix-core/src/line_ending.rs6
-rw-r--r--helix-core/src/movement.rs4
-rw-r--r--helix-core/src/object.rs9
-rw-r--r--helix-core/src/position.rs81
-rw-r--r--helix-core/src/register.rs4
-rw-r--r--helix-core/src/selection.rs11
-rw-r--r--helix-core/src/syntax.rs90
-rw-r--r--helix-core/src/textobject.rs51
-rw-r--r--helix-core/src/transaction.rs10
18 files changed, 300 insertions, 103 deletions
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index 51096453..ea695d34 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "helix-core"
-version = "0.4.1"
+version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
-edition = "2018"
+edition = "2021"
license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
@@ -13,7 +13,7 @@ include = ["src/**/*", "README.md"]
[features]
[dependencies]
-helix-syntax = { version = "0.4", path = "../helix-syntax" }
+helix-syntax = { version = "0.5", path = "../helix-syntax" }
ropey = "1.3"
smallvec = "1.7"
@@ -27,6 +27,7 @@ once_cell = "1.8"
arc-swap = "1"
regex = "1"
+log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"
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(&current_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