aboutsummaryrefslogtreecommitdiff
path: root/helix-core
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core')
-rw-r--r--helix-core/Cargo.toml18
-rw-r--r--helix-core/src/auto_pairs.rs781
-rw-r--r--helix-core/src/chars.rs9
-rw-r--r--helix-core/src/diagnostic.rs15
-rw-r--r--helix-core/src/diff.rs6
-rw-r--r--helix-core/src/graphemes.rs86
-rw-r--r--helix-core/src/history.rs4
-rw-r--r--helix-core/src/increment/date_time.rs490
-rw-r--r--helix-core/src/increment/mod.rs8
-rw-r--r--helix-core/src/increment/number.rs (renamed from helix-core/src/numbers.rs)40
-rw-r--r--helix-core/src/indent.rs147
-rw-r--r--helix-core/src/lib.rs44
-rw-r--r--helix-core/src/line_ending.rs2
-rw-r--r--helix-core/src/match_brackets.rs2
-rw-r--r--helix-core/src/movement.rs52
-rw-r--r--helix-core/src/object.rs73
-rw-r--r--helix-core/src/position.rs12
-rw-r--r--helix-core/src/register.rs4
-rw-r--r--helix-core/src/selection.rs109
-rw-r--r--helix-core/src/shellwords.rs164
-rw-r--r--helix-core/src/surround.rs1
-rw-r--r--helix-core/src/syntax.rs1260
-rw-r--r--helix-core/src/transaction.rs42
23 files changed, 2431 insertions, 938 deletions
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index ea695d34..7ff91cfd 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-core"
-version = "0.5.0"
+version = "0.6.0"
authors = ["BlaΕΎ Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
@@ -13,17 +13,18 @@ include = ["src/**/*", "README.md"]
[features]
[dependencies]
-helix-syntax = { version = "0.5", path = "../helix-syntax" }
+helix-syntax = { version = "0.6", path = "../helix-syntax" }
ropey = "1.3"
-smallvec = "1.7"
-tendril = "0.4.2"
-unicode-segmentation = "1.8"
+smallvec = "1.8"
+smartstring = "0.2.9"
+unicode-segmentation = "1.9"
unicode-width = "0.1"
-unicode-general-category = "0.4"
+unicode-general-category = "0.5"
# slab = "0.4.2"
+slotmap = "1.0"
tree-sitter = "0.20"
-once_cell = "1.8"
+once_cell = "1.9"
arc-swap = "1"
regex = "1"
@@ -35,6 +36,9 @@ toml = "0.5"
similar = "2.1"
etcetera = "0.3"
+encoding_rs = "0.8"
+
+chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index cc966852..f4359a34 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,7 +1,10 @@
//! 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 crate::{
+ graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
+};
+use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +18,9 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
-const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
+// [TODO] build this dynamically in language config. see #992
+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@@ -25,14 +30,19 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
-// TODO: delete implementation where it erases the whole bracket (|) -> |
+// [TODO]
+// * delete implementation where it erases the whole bracket (|) -> |
+// * change to multi character pairs to handle cases like placing the cursor in the
+// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+ debug!("autopairs hook selection: {:#?}", selection);
+
for &(open, close) in PAIRS {
if open == ch {
if open == close {
- return handle_same(doc, selection, open);
+ return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
}
@@ -47,18 +57,145 @@ pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction>
None
}
-// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
-// for example "&'a mut", or "fn<'a>"
-
-fn next_char(doc: &Rope, pos: usize) -> Option<char> {
- if pos >= doc.len_chars() {
+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
+ if pos == 0 {
return None;
}
- Some(doc.char(pos))
+
+ doc.get_char(pos - 1)
+}
+
+fn is_single_grapheme(doc: &Rope, range: &Range) -> bool {
+ let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
+ let first = graphemes.next();
+ let second = graphemes.next();
+ debug!("first: {:#?}, second: {:#?}", first, second);
+ first.is_some() && second.is_none()
+}
+
+/// calculate what the resulting range should be for an auto pair insertion
+fn get_next_range(
+ doc: &Rope,
+ start_range: &Range,
+ offset: usize,
+ typed_char: char,
+ len_inserted: usize,
+) -> Range {
+ // When the character under the cursor changes due to complete pair
+ // insertion, we must look backward a grapheme and then add the length
+ // of the insertion to put the resulting cursor in the right place, e.g.
+ //
+ // foo[\r\n] - anchor: 3, head: 5
+ // foo([)]\r\n - anchor: 4, head: 5
+ //
+ // foo[\r\n] - anchor: 3, head: 5
+ // foo'[\r\n] - anchor: 4, head: 6
+ //
+ // foo([)]\r\n - anchor: 4, head: 5
+ // foo()[\r\n] - anchor: 5, head: 7
+ //
+ // [foo]\r\n - anchor: 0, head: 3
+ // [foo(])\r\n - anchor: 0, head: 5
+
+ // inserting at the very end of the document after the last newline
+ if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
+ return Range::new(
+ start_range.anchor + offset + typed_char.len_utf8(),
+ start_range.head + offset + typed_char.len_utf8(),
+ );
+ }
+
+ let single_grapheme = is_single_grapheme(doc, start_range);
+ let doc_slice = doc.slice(..);
+
+ // just skip over graphemes
+ if len_inserted == 0 {
+ let end_anchor = if single_grapheme {
+ graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
+
+ // even for backward inserts with multiple grapheme selections,
+ // we want the anchor to stay where it is so that the relative
+ // selection does not change, e.g.:
+ //
+ // foo([) wor]d -> insert ) -> foo()[ wor]d
+ } else {
+ start_range.anchor + offset
+ };
+
+ return Range::new(
+ end_anchor,
+ graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
+ );
+ }
+
+ // trivial case: only inserted a single-char opener, just move the selection
+ if len_inserted == 1 {
+ let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
+ start_range.anchor + offset + typed_char.len_utf8()
+ } else {
+ start_range.anchor + offset
+ };
+
+ return Range::new(
+ end_anchor,
+ start_range.head + offset + typed_char.len_utf8(),
+ );
+ }
+
+ // If the head = 0, then we must be in insert mode with a backward
+ // cursor, which implies the head will just move
+ let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
+ start_range.head + offset + typed_char.len_utf8()
+ } else {
+ // We must have a forward cursor, which means we must move to the
+ // other end of the grapheme to get to where the new characters
+ // are inserted, then move the head to where it should be
+ let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
+ debug!(
+ "prev_bound: {}, offset: {}, len_inserted: {}",
+ prev_bound, offset, len_inserted
+ );
+ prev_bound + offset + len_inserted
+ };
+
+ let end_anchor = match (start_range.len(), start_range.direction()) {
+ // if we have a zero width cursor, it shifts to the same number
+ (0, _) => end_head,
+
+ // If we are inserting for a regular one-width cursor, the anchor
+ // moves with the head. This is the fast path for ASCII.
+ (1, Direction::Forward) => end_head - 1,
+ (1, Direction::Backward) => end_head + 1,
+
+ (_, Direction::Forward) => {
+ if single_grapheme {
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head)
+ + typed_char.len_utf8()
+
+ // if we are appending, the anchor stays where it is; only offset
+ // for multiple range insertions
+ } else {
+ start_range.anchor + offset
+ }
+ }
+
+ (_, Direction::Backward) => {
+ if single_grapheme {
+ // if we're backward, then the head is at the first char
+ // of the typed char, so we need to add the length of
+ // the closing char
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
+ } else {
+ // when we are inserting in front of a selection, we need to move
+ // the anchor over by however many characters were inserted overall
+ start_range.anchor + offset + len_inserted
+ }
+ }
+ };
+
+ Range::new(end_anchor, end_head)
}
-// TODO: selections should be extended if range, moved if point.
-// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open(
doc: &Rope,
selection: &Selection,
@@ -66,98 +203,584 @@ fn handle_open(
close: char,
close_before: &str,
) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
-
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
-
- let head = pos + offs + open.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let cursor = start_range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let len_inserted;
- match next {
+ let change = match next_char {
Some(ch) if !close_before.contains(ch) => {
- offs += 1;
- // TODO: else return (use default handler that inserts open)
- (pos, pos, Some(Tendril::from_char(open)))
+ len_inserted = open.len_utf8();
+ let mut tendril = Tendril::new();
+ tendril.push(open);
+ (cursor, cursor, Some(tendril))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
- let mut pair = Tendril::with_capacity(2);
- pair.push_char(open);
- pair.push_char(close);
+ let pair = Tendril::from_iter([open, close]);
+ len_inserted = open.len_utf8() + close.len_utf8();
+ (cursor, cursor, Some(pair))
+ }
+ };
- offs += 2;
+ let next_range = get_next_range(doc, start_range, offs, open, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
- (pos, pos, Some(pair))
- }
- }
+ change
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let cursor = start_range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let mut len_inserted = 0;
- let head = pos + offs + close.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let change = if next_char == Some(close) {
+ // return transaction that moves past close
+ (cursor, cursor, None) // no-op
+ } else {
+ len_inserted += close.len_utf8();
+ let mut tendril = Tendril::new();
+ tendril.push(close);
+ (cursor, cursor, Some(tendril))
+ };
+
+ let next_range = get_next_range(doc, start_range, offs, close, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
+
+ change
+ });
+
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
+}
+
+/// handle cases where open and close is the same, or in triples ("""docstring""")
+fn handle_same(
+ doc: &Rope,
+ selection: &Selection,
+ token: char,
+ close_before: &str,
+ open_before: &str,
+) -> Transaction {
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
+
+ let mut offs = 0;
- if next == Some(close) {
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let cursor = start_range.cursor(doc.slice(..));
+ let mut len_inserted = 0;
+
+ let next_char = doc.get_char(cursor);
+ let prev_char = prev_char(doc, cursor);
+
+ let change = if next_char == Some(token) {
// return transaction that moves past close
- (pos, pos, None) // no-op
+ (cursor, cursor, None) // no-op
} else {
- offs += close.len_utf8();
+ let mut pair = Tendril::new();
+ pair.push(token);
- // TODO: else return (use default handler that inserts close)
- (pos, pos, Some(Tendril::from_char(close)))
- }
+ // for equal pairs, don't insert both open and close if either
+ // side has a non-pair char
+ if (next_char.is_none() || close_before.contains(next_char.unwrap()))
+ && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
+ {
+ pair.push(token);
+ }
+
+ len_inserted += pair.len();
+ (cursor, cursor, Some(pair))
+ };
+
+ let next_range = get_next_range(doc, start_range, offs, token, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
+
+ change
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
-// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
- // if not cursor but selection, wrap
- // let next = next char
-
- // if next == bracket {
- // // if start of syntax node, insert token twice (new pair because node is complete)
- // // elseif colsedBracketAt
- // // is_triple == allow triple && next 3 is equal
- // // cursor jump over
- // }
- //} else if allow_triple && followed by triple {
- //}
- //} else if next != word char && prev != bracket && prev != word char {
- // // condition checks for cases like I' where you don't want I'' (or I'm)
- // insert pair ("")
- //}
- None
+#[cfg(test)]
+mod test {
+ use super::*;
+ use smallvec::smallvec;
+
+ const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
+
+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open != close)
+ }
+
+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open == close)
+ }
+
+ fn test_hooks(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ ch: char,
+ expected_doc: &Rope,
+ expected_sel: &Selection,
+ ) {
+ let trans = hook(in_doc, in_sel, ch).unwrap();
+ let mut actual_doc = in_doc.clone();
+ assert!(trans.apply(&mut actual_doc));
+ assert_eq!(expected_doc, &actual_doc);
+ assert_eq!(expected_sel, trans.selection().unwrap());
+ }
+
+ fn test_hooks_with_pairs<I, F, R>(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ pairs: I,
+ get_expected_doc: F,
+ actual_sel: &Selection,
+ ) where
+ I: IntoIterator<Item = &'static (char, char)>,
+ F: Fn(char, char) -> R,
+ R: Into<Rope>,
+ Rope: From<R>,
+ {
+ pairs.into_iter().for_each(|(open, close)| {
+ test_hooks(
+ in_doc,
+ in_sel,
+ *open,
+ &Rope::from(get_expected_doc(*open, *close)),
+ actual_sel,
+ )
+ });
+ }
+
+ // [] indicates range
+
+ /// [] -> insert ( -> ([])
+ #[test]
+ fn test_insert_blank() {
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, close| format!("{}{}{}", open, close, LINE_END),
+ &Selection::single(2, 1),
+ );
+
+ let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
+
+ test_hooks_with_pairs(
+ &empty_doc,
+ &Selection::single(empty_doc.len_chars(), LINE_END.len()),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{line_end}{open}{close}{line_end}",
+ open = open,
+ close = close,
+ line_end = LINE_END
+ )
+ },
+ &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
+ );
+ }
+
+ #[test]
+ fn test_insert_before_multi_code_point_graphemes() {
+ test_hooks_with_pairs(
+ &Rope::from(format!("hello πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ goodbye{}", LINE_END)),
+ &Selection::single(13, 6),
+ PAIRS,
+ |open, _| format!("hello {}πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦ goodbye{}", open, LINE_END),
+ &Selection::single(14, 7),
+ );
+ }
+
+ #[test]
+ fn test_insert_at_end_of_document() {
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(LINE_END.len(), LINE_END.len()),
+ PAIRS,
+ |open, close| format!("{}{}{}", LINE_END, open, close),
+ &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
+ );
+
+ test_hooks_with_pairs(
+ &Rope::from(format!("foo{}", LINE_END)),
+ &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
+ PAIRS,
+ |open, close| format!("foo{}{}{}", LINE_END, open, close),
+ &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
+ );
+ }
+
+ /// [] -> append ( -> ([])
+ #[test]
+ fn test_append_blank() {
+ test_hooks_with_pairs(
+ // this is what happens when you have a totally blank document and then append
+ &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
+ // before inserting the pair, the cursor covers all of both empty lines
+ &Selection::single(0, LINE_END.len() * 2),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{line_end}{open}{close}{line_end}",
+ line_end = LINE_END,
+ open = open,
+ close = close
+ )
+ },
+ // after inserting pair, the cursor covers the first new line and the open char
+ &Selection::single(0, LINE_END.len() + 2),
+ );
+ }
+
+ /// [] ([])
+ /// [] -> insert -> ([])
+ /// [] ([])
+ #[test]
+ fn test_insert_blank_multi_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from("\n\n\n"),
+ &Selection::new(
+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
+ 0,
+ ),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ ),
+ );
+ }
+
+ /// fo[o] -> append ( -> fo[o(])
+ #[test]
+ fn test_append() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\n"),
+ &Selection::single(2, 4),
+ differing_pairs(),
+ |open, close| format!("foo{}{}\n", open, close),
+ &Selection::single(2, 5),
+ );
+ }
+
+ /// foo[] -> append to end of line ( -> foo([])
+ #[test]
+ fn test_append_single_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from(format!("foo{}", LINE_END)),
+ &Selection::single(3, 3 + LINE_END.len()),
+ differing_pairs(),
+ |open, close| format!("foo{}{}{}", open, close, LINE_END),
+ &Selection::single(4, 5),
+ );
+ }
+
+ /// fo[o] fo[o(])
+ /// fo[o] -> append ( -> fo[o(])
+ /// fo[o] fo[o(])
+ #[test]
+ fn test_append_multi() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\nfoo\nfoo\n"),
+ &Selection::new(
+ smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
+ 0,
+ ),
+ differing_pairs(),
+ |open, close| {
+ format!(
+ "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
+ 0,
+ ),
+ );
+ }
+
+ /// ([)] -> insert ) -> ()[]
+ #[test]
+ fn test_insert_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(2, 1),
+ *close,
+ &doc,
+ &Selection::single(2 + LINE_END.len(), 2),
+ );
+ }
+ }
+
+ /// [(]) -> append ) -> [()]
+ #[test]
+ fn test_append_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(0, 2),
+ *close,
+ &doc,
+ &Selection::single(0, 2 + LINE_END.len()),
+ );
+ }
+ }
+
+ /// ([]) ()[]
+ /// ([]) -> insert ) -> ()[]
+ /// ([]) ()[]
+ #[test]
+ fn test_insert_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
+ 0,
+ );
+
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ }
+ }
+
+ /// [(]) [()]
+ /// [(]) -> append ) -> [()]
+ /// [(]) [()]
+ #[test]
+ fn test_append_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
+ 0,
+ );
+
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert ( -> (([]))
+ #[test]
+ fn test_insert_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::single(3, 2);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// [word(]) -> append ( -> [word((]))
+ #[test]
+ fn test_append_open_inside_pair() {
+ let sel = Selection::single(0, 6);
+ let expected_sel = Selection::single(0, 7);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("word{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "word{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert " -> ("[]")
+ #[test]
+ fn test_insert_nested_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::single(3, 2);
+
+ for (outer_open, outer_close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+ for (inner_open, inner_close) in matching_pairs() {
+ let expected_doc = Rope::from(format!(
+ "{}{}{}{}",
+ outer_open, inner_open, inner_close, outer_close
+ ));
+
+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ }
+ }
+ }
+
+ /// [(]) -> append " -> [("]")
+ #[test]
+ fn test_append_nested_open_inside_pair() {
+ let sel = Selection::single(0, 2);
+ let expected_sel = Selection::single(0, 3);
+
+ for (outer_open, outer_close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+ for (inner_open, inner_close) in matching_pairs() {
+ let expected_doc = Rope::from(format!(
+ "{}{}{}{}",
+ outer_open, inner_open, inner_close, outer_close
+ ));
+
+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ }
+ }
+ }
+
+ /// []word -> insert ( -> ([]word
+ #[test]
+ fn test_insert_open_before_non_pair() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(2, 1),
+ )
+ }
+
+ /// [wor]d -> insert ( -> ([wor]d
+ #[test]
+ fn test_insert_open_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(3, 0),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(4, 1),
+ )
+ }
+
+ /// [wor]d -> append ) -> [wor)]d
+ #[test]
+ fn test_append_close_inside_non_pair_with_selection() {
+ let sel = Selection::single(0, 4);
+ let expected_sel = Selection::single(0, 5);
+
+ for (_, close) in PAIRS {
+ let doc = Rope::from("word");
+ let expected_doc = Rope::from(format!("wor{}d", close));
+ test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// foo[ wor]d -> insert ( -> foo([) wor]d
+ #[test]
+ fn test_insert_open_trailing_word_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("foo word"),
+ &Selection::single(7, 3),
+ differing_pairs(),
+ |open, close| format!("foo{}{} word", open, close),
+ &Selection::single(9, 4),
+ )
+ }
+
+ /// foo([) wor]d -> insert ) -> foo()[ wor]d
+ #[test]
+ fn test_insert_close_inside_pair_trailing_word_with_selection() {
+ for (open, close) in differing_pairs() {
+ test_hooks(
+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+ &Selection::single(9, 4),
+ *close,
+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+ &Selection::single(9, 5),
+ )
+ }
+ }
+
+ /// we want pairs that are *not* the same char to be inserted after
+ /// a non-pair char, for cases like functions, but for pairs that are
+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
+ ///
+ /// word[] -> insert ( -> word([])
+ /// word[] -> insert ' -> word'[]
+ #[test]
+ fn test_insert_open_after_non_pair() {
+ let doc = Rope::from(format!("word{}", LINE_END));
+ let sel = Selection::single(5, 4);
+ let expected_sel = Selection::single(6, 5);
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ differing_pairs(),
+ |open, close| format!("word{}{}{}", open, close, LINE_END),
+ &expected_sel,
+ );
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ matching_pairs(),
+ |open, _| format!("word{}{}", open, LINE_END),
+ &expected_sel,
+ );
+ }
}
diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs
index c8e5efbd..54991574 100644
--- a/helix-core/src/chars.rs
+++ b/helix-core/src/chars.rs
@@ -91,12 +91,11 @@ mod test {
#[test]
fn test_categorize() {
- const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
- const WORD_TEST_CASE: &'static str =
- "_hello_world_γ‚γ„γ†γˆγŠγƒΌ1234567890οΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌ";
- const PUNCTUATION_TEST_CASE: &'static str =
+ const EOL_TEST_CASE: &str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+ const WORD_TEST_CASE: &str = "_hello_world_γ‚γ„γ†γˆγŠγƒΌ1234567890οΌ‘οΌ’οΌ“οΌ”οΌ•οΌ–οΌ—οΌ˜οΌ™οΌ";
+ const PUNCTUATION_TEST_CASE: &str =
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~οΌβ€οΌƒοΌ„οΌ…οΌ†β€™οΌˆοΌ‰οΌŠοΌ‹γ€γ€‚οΌšοΌ›οΌœοΌοΌžοΌŸοΌ γ€Œγ€οΌΎο½€ο½›ο½œο½ο½ž";
- const WHITESPACE_TEST_CASE: &'static str = "αš€ γ€€β€―β€‰β€ˆ";
+ const WHITESPACE_TEST_CASE: &str = "αš€ γ€€β€―β€‰β€ˆ";
for ch in EOL_TEST_CASE.chars() {
assert_eq!(CharCategory::Eol, categorize_char(ch));
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index 4fcf51c9..210ad639 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,12 +1,19 @@
//! LSP diagnostic utility types.
+use serde::{Deserialize, Serialize};
/// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
pub enum Severity {
- Error,
- Warning,
- Info,
Hint,
+ Info,
+ Warning,
+ Error,
+}
+
+impl Default for Severity {
+ fn default() -> Self {
+ Self::Hint
+ }
}
/// A range of `char`s within the text.
diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs
index a83db333..6960c679 100644
--- a/helix-core/src/diff.rs
+++ b/helix-core/src/diff.rs
@@ -11,10 +11,6 @@ pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
// A timeout is set so after 1 seconds, the algorithm will start
// approximating. This is especially important for big `Rope`s or
// `Rope`s that are extremely dissimilar to each other.
- //
- // Note: Ignore the clippy warning, as the trait bounds of
- // `Transaction::change()` require an iterator implementing
- // `ExactIterator`.
let mut config = similar::TextDiff::configure();
config.timeout(std::time::Duration::from_secs(1));
@@ -62,7 +58,7 @@ mod tests {
let mut old = Rope::from(a);
let new = Rope::from(b);
compare_ropes(&old, &new).apply(&mut old);
- old.to_string() == new.to_string()
+ old == new
}
}
}
diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index c6398875..aa898684 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -120,6 +120,43 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
chunk_char_idx + tmp
}
+#[must_use]
+pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize {
+ // Bounds check
+ debug_assert!(byte_idx <= slice.len_bytes());
+
+ // Get the chunk with our byte index in it.
+ let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
+
+ // Set up the grapheme cursor.
+ let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
+
+ // Find the nth next grapheme cluster boundary.
+ for _ in 0..n {
+ loop {
+ match gc.next_boundary(chunk, chunk_byte_idx) {
+ Ok(None) => return slice.len_bytes(),
+ Ok(Some(n)) => {
+ byte_idx = n;
+ break;
+ }
+ Err(GraphemeIncomplete::NextChunk) => {
+ chunk_byte_idx += chunk.len();
+ let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx);
+ chunk = a;
+ // chunk_char_idx = c;
+ }
+ Err(GraphemeIncomplete::PreContext(n)) => {
+ let ctx_chunk = slice.chunk_at_byte(n - 1).0;
+ gc.provide_context(ctx_chunk, n - ctx_chunk.len());
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ byte_idx
+}
+
/// Finds the next grapheme boundary after the given char position.
#[must_use]
#[inline(always)]
@@ -127,6 +164,13 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1)
}
+/// Finds the next grapheme boundary after the given byte position.
+#[must_use]
+#[inline(always)]
+pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize {
+ nth_next_grapheme_boundary_byte(slice, byte_idx, 1)
+}
+
/// Returns the passed char index if it's already a grapheme boundary,
/// or the next grapheme boundary char index if not.
#[must_use]
@@ -151,6 +195,23 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
}
}
+/// Returns the passed byte index if it's already a grapheme boundary,
+/// or the next grapheme boundary byte index if not.
+#[must_use]
+#[inline]
+pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize {
+ if byte_idx == 0 {
+ byte_idx
+ } else {
+ // TODO: optimize so we're not constructing grapheme cursor twice
+ if is_grapheme_boundary_byte(slice, byte_idx) {
+ byte_idx
+ } else {
+ next_grapheme_boundary_byte(slice, byte_idx)
+ }
+ }
+}
+
/// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
@@ -179,6 +240,31 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
}
}
+/// Returns whether the given byte position is a grapheme boundary.
+#[must_use]
+pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool {
+ // Bounds check
+ debug_assert!(byte_idx <= slice.len_bytes());
+
+ // Get the chunk with our byte index in it.
+ let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
+
+ // Set up the grapheme cursor.
+ let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
+
+ // Determine if the given position is a grapheme cluster boundary.
+ loop {
+ match gc.is_boundary(chunk, chunk_byte_idx) {
+ Ok(n) => return n,
+ Err(GraphemeIncomplete::PreContext(n)) => {
+ let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
+ gc.provide_context(ctx_chunk, ctx_byte_start);
+ }
+ Err(_) => unreachable!(),
+ }
+ }
+}
+
/// An iterator over the graphemes of a `RopeSlice`.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs
index 4b1c8d3b..bb95213c 100644
--- a/helix-core/src/history.rs
+++ b/helix-core/src/history.rs
@@ -448,8 +448,8 @@ mod test {
change: crate::transaction::Change,
instant: Instant,
) {
- let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
- history.commit_revision_at_timestamp(&txn, &state, instant);
+ let txn = Transaction::change(&state.doc, vec![change].into_iter());
+ history.commit_revision_at_timestamp(&txn, state, instant);
txn.apply(&mut state.doc);
}
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs
new file mode 100644
index 00000000..91fa5963
--- /dev/null
+++ b/helix-core/src/increment/date_time.rs
@@ -0,0 +1,490 @@
+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
+use once_cell::sync::Lazy;
+use regex::Regex;
+use ropey::RopeSlice;
+
+use std::borrow::Cow;
+use std::cmp;
+
+use super::Increment;
+use crate::{Range, Tendril};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct DateTimeIncrementor {
+ date_time: NaiveDateTime,
+ range: Range,
+ fmt: &'static str,
+ field: DateField,
+}
+
+impl DateTimeIncrementor {
+ pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
+ let range = if range.is_empty() {
+ if range.anchor < text.len_chars() {
+ // Treat empty range as a cursor range.
+ range.put_cursor(text, range.anchor + 1, true)
+ } else {
+ // The range is empty and at the end of the text.
+ return None;
+ }
+ } else {
+ range
+ };
+
+ FORMATS.iter().find_map(|format| {
+ let from = range.from().saturating_sub(format.max_len);
+ let to = (range.from() + format.max_len).min(text.len_chars());
+
+ let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
+ let text: Cow<str> = text.slice(from..to).into();
+
+ let captures = format.regex.captures(&text)?;
+ if captures.len() - 1 != format.fields.len() {
+ return None;
+ }
+
+ let date_time = captures.get(0)?;
+ let offset = range.from() - from_in_text;
+ let range = Range::new(date_time.start() + offset, date_time.end() + offset);
+
+ let field = captures
+ .iter()
+ .skip(1)
+ .enumerate()
+ .find_map(|(i, capture)| {
+ let capture = capture?;
+ let capture_range = capture.range();
+
+ if capture_range.contains(&from_in_text)
+ && capture_range.contains(&(to_in_text - 1))
+ {
+ Some(format.fields[i])
+ } else {
+ None
+ }
+ })?;
+
+ let has_date = format.fields.iter().any(|f| f.unit.is_date());
+ let has_time = format.fields.iter().any(|f| f.unit.is_time());
+
+ let date_time = &text[date_time.start()..date_time.end()];
+ let date_time = match (has_date, has_time) {
+ (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
+ (true, false) => {
+ let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
+
+ date.and_hms(0, 0, 0)
+ }
+ (false, true) => {
+ let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
+
+ NaiveDate::from_ymd(0, 1, 1).and_time(time)
+ }
+ (false, false) => return None,
+ };
+
+ Some(DateTimeIncrementor {
+ date_time,
+ range,
+ fmt: format.fmt,
+ field,
+ })
+ })
+ }
+}
+
+impl Increment for DateTimeIncrementor {
+ fn increment(&self, amount: i64) -> (Range, Tendril) {
+ let date_time = match self.field.unit {
+ DateUnit::Years => add_years(self.date_time, amount),
+ DateUnit::Months => add_months(self.date_time, amount),
+ DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
+ DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
+ DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
+ DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
+ DateUnit::AmPm => toggle_am_pm(self.date_time),
+ }
+ .unwrap_or(self.date_time);
+
+ (self.range, date_time.format(self.fmt).to_string().into())
+ }
+}
+
+static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
+ vec![
+ Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23
+ Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23
+ Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12
+ Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12
+ Format::new("%Y-%m-%d"), // 2021-11-24
+ Format::new("%Y/%m/%d"), // 2021/11/24
+ Format::new("%a %b %d %Y"), // Wed Nov 24 2021
+ Format::new("%d-%b-%Y"), // 24-Nov-2021
+ Format::new("%Y %b %d"), // 2021 Nov 24
+ Format::new("%b %d, %Y"), // Nov 24, 2021
+ Format::new("%-I:%M:%S %P"), // 7:21:53 am
+ Format::new("%-I:%M %P"), // 7:21 am
+ Format::new("%-I:%M:%S %p"), // 7:21:53 AM
+ Format::new("%-I:%M %p"), // 7:21 AM
+ Format::new("%H:%M:%S"), // 23:24:23
+ Format::new("%H:%M"), // 23:24
+ ]
+});
+
+#[derive(Debug)]
+struct Format {
+ fmt: &'static str,
+ fields: Vec<DateField>,
+ regex: Regex,
+ max_len: usize,
+}
+
+impl Format {
+ fn new(fmt: &'static str) -> Self {
+ let mut remaining = fmt;
+ let mut fields = Vec::new();
+ let mut regex = String::new();
+ let mut max_len = 0;
+
+ while let Some(i) = remaining.find('%') {
+ let after = &remaining[i + 1..];
+ let mut chars = after.chars();
+ let c = chars.next().unwrap();
+
+ let spec_len = if c == '-' {
+ 1 + chars.next().unwrap().len_utf8()
+ } else {
+ c.len_utf8()
+ };
+
+ let specifier = &after[..spec_len];
+ let field = DateField::from_specifier(specifier).unwrap();
+ fields.push(field);
+ max_len += field.max_len + remaining[..i].len();
+ regex += &remaining[..i];
+ regex += &format!("({})", field.regex);
+ remaining = &after[spec_len..];
+ }
+
+ let regex = Regex::new(&regex).unwrap();
+
+ Self {
+ fmt,
+ fields,
+ regex,
+ max_len,
+ }
+ }
+}
+
+impl PartialEq for Format {
+ fn eq(&self, other: &Self) -> bool {
+ self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len
+ }
+}
+
+impl Eq for Format {}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+struct DateField {
+ regex: &'static str,
+ unit: DateUnit,
+ max_len: usize,
+}
+
+impl DateField {
+ fn from_specifier(specifier: &str) -> Option<Self> {
+ match specifier {
+ "Y" => Some(Self {
+ regex: r"\d{4}",
+ unit: DateUnit::Years,
+ max_len: 5,
+ }),
+ "y" => Some(Self {
+ regex: r"\d\d",
+ unit: DateUnit::Years,
+ max_len: 2,
+ }),
+ "m" => Some(Self {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Months,
+ max_len: 2,
+ }),
+ "d" => Some(Self {
+ regex: r"[0-3]\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "-d" => Some(Self {
+ regex: r"[1-3]?\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "a" => Some(Self {
+ regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
+ unit: DateUnit::Days,
+ max_len: 3,
+ }),
+ "A" => Some(Self {
+ regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
+ unit: DateUnit::Days,
+ max_len: 9,
+ }),
+ "b" | "h" => Some(Self {
+ regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
+ unit: DateUnit::Months,
+ max_len: 3,
+ }),
+ "B" => Some(Self {
+ regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
+ unit: DateUnit::Months,
+ max_len: 9,
+ }),
+ "H" => Some(Self {
+ regex: r"[0-2]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "M" => Some(Self {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Minutes,
+ max_len: 2,
+ }),
+ "S" => Some(Self {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Seconds,
+ max_len: 2,
+ }),
+ "I" => Some(Self {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "-I" => Some(Self {
+ regex: r"1?\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "P" => Some(Self {
+ regex: r"am|pm",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ "p" => Some(Self {
+ regex: r"AM|PM",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum DateUnit {
+ Years,
+ Months,
+ Days,
+ Hours,
+ Minutes,
+ Seconds,
+ AmPm,
+}
+
+impl DateUnit {
+ fn is_date(self) -> bool {
+ matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days)
+ }
+
+ fn is_time(self) -> bool {
+ matches!(
+ self,
+ DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds
+ )
+ }
+}
+
+fn ndays_in_month(year: i32, month: u32) -> u32 {
+ // The first day of the next month...
+ let (y, m) = if month == 12 {
+ (year + 1, 1)
+ } else {
+ (year, month + 1)
+ };
+ let d = NaiveDate::from_ymd(y, m, 1);
+
+ // ...is preceded by the last day of the original month.
+ d.pred().day()
+}
+
+fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+ let month = (date_time.month0() as i64).checked_add(amount)?;
+ let year = date_time.year() + i32::try_from(month / 12).ok()?;
+ let year = if month.is_negative() { year - 1 } else { year };
+
+ // Normalize month
+ let month = month % 12;
+ let month = if month.is_negative() {
+ month + 12
+ } else {
+ month
+ } as u32
+ + 1;
+
+ let day = cmp::min(date_time.day(), ndays_in_month(year, month));
+
+ Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
+}
+
+fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+ let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
+ let ndays = ndays_in_month(year, date_time.month());
+
+ if date_time.day() > ndays {
+ let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
+ Some(d.succ().and_time(date_time.time()))
+ } else {
+ date_time.with_year(year)
+ }
+}
+
+fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
+ date_time.checked_add_signed(duration)
+}
+
+fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
+ if date_time.hour() < 12 {
+ add_duration(date_time, Duration::hours(12))
+ } else {
+ add_duration(date_time, Duration::hours(-12))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::Rope;
+
+ #[test]
+ fn test_increment_date_times() {
+ let tests = [
+ // (original, cursor, amount, expected)
+ ("2020-02-28", 0, 1, "2021-02-28"),
+ ("2020-02-29", 0, 1, "2021-03-01"),
+ ("2020-01-31", 5, 1, "2020-02-29"),
+ ("2020-01-20", 5, 1, "2020-02-20"),
+ ("2021-01-01", 5, -1, "2020-12-01"),
+ ("2021-01-31", 5, -2, "2020-11-30"),
+ ("2020-02-28", 8, 1, "2020-02-29"),
+ ("2021-02-28", 8, 1, "2021-03-01"),
+ ("2021-02-28", 0, -1, "2020-02-28"),
+ ("2021-03-01", 0, -1, "2020-03-01"),
+ ("2020-02-29", 5, -1, "2020-01-29"),
+ ("2020-02-20", 5, -1, "2020-01-20"),
+ ("2020-02-29", 8, -1, "2020-02-28"),
+ ("2021-03-01", 8, -1, "2021-02-28"),
+ ("1980/12/21", 8, 100, "1981/03/31"),
+ ("1980/12/21", 8, -100, "1980/09/12"),
+ ("1980/12/21", 8, 1000, "1983/09/17"),
+ ("1980/12/21", 8, -1000, "1978/03/27"),
+ ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
+ ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
+ ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
+ ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
+ ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
+ ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
+ ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
+ ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
+ ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
+ ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
+ ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
+ ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
+ ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
+ ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
+ ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
+ ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
+ ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
+ ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
+ ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
+ ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
+ ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
+ ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
+ ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
+ ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
+ ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
+ ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
+ ("24-Nov-2021", 0, 1, "25-Nov-2021"),
+ ("24-Nov-2021", 3, 1, "24-Dec-2021"),
+ ("24-Nov-2021", 7, 1, "24-Nov-2022"),
+ ("2021 Nov 24", 0, 1, "2022 Nov 24"),
+ ("2021 Nov 24", 5, 1, "2021 Dec 24"),
+ ("2021 Nov 24", 9, 1, "2021 Nov 25"),
+ ("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
+ ("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
+ ("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
+ ("7:21:53 am", 0, 1, "8:21:53 am"),
+ ("7:21:53 am", 3, 1, "7:22:53 am"),
+ ("7:21:53 am", 5, 1, "7:21:54 am"),
+ ("7:21:53 am", 8, 1, "7:21:53 pm"),
+ ("7:21:53 AM", 0, 1, "8:21:53 AM"),
+ ("7:21:53 AM", 3, 1, "7:22:53 AM"),
+ ("7:21:53 AM", 5, 1, "7:21:54 AM"),
+ ("7:21:53 AM", 8, 1, "7:21:53 PM"),
+ ("7:21 am", 0, 1, "8:21 am"),
+ ("7:21 am", 3, 1, "7:22 am"),
+ ("7:21 am", 5, 1, "7:21 pm"),
+ ("7:21 AM", 0, 1, "8:21 AM"),
+ ("7:21 AM", 3, 1, "7:22 AM"),
+ ("7:21 AM", 5, 1, "7:21 PM"),
+ ("23:24:23", 1, 1, "00:24:23"),
+ ("23:24:23", 3, 1, "23:25:23"),
+ ("23:24:23", 6, 1, "23:24:24"),
+ ("23:24", 1, 1, "00:24"),
+ ("23:24", 3, 1, "23:25"),
+ ];
+
+ for (original, cursor, amount, expected) in tests {
+ let rope = Rope::from_str(original);
+ let range = Range::new(cursor, cursor + 1);
+ assert_eq!(
+ DateTimeIncrementor::from_range(rope.slice(..), range)
+ .unwrap()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
+ );
+ }
+ }
+
+ #[test]
+ fn test_invalid_date_times() {
+ let tests = [
+ "0000-00-00",
+ "1980-2-21",
+ "1980-12-1",
+ "12345",
+ "2020-02-30",
+ "1999-12-32",
+ "19-12-32",
+ "1-2-3",
+ "0000/00/00",
+ "1980/2/21",
+ "1980/12/1",
+ "12345",
+ "2020/02/30",
+ "1999/12/32",
+ "19/12/32",
+ "1/2/3",
+ "123:456:789",
+ "11:61",
+ "2021-55-12 08:12:54",
+ ];
+
+ for invalid in tests {
+ let rope = Rope::from_str(invalid);
+ let range = Range::new(0, 1);
+
+ assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
+ }
+ }
+}
diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs
new file mode 100644
index 00000000..f5945774
--- /dev/null
+++ b/helix-core/src/increment/mod.rs
@@ -0,0 +1,8 @@
+pub mod date_time;
+pub mod number;
+
+use crate::{Range, Tendril};
+
+pub trait Increment {
+ fn increment(&self, amount: i64) -> (Range, Tendril);
+}
diff --git a/helix-core/src/numbers.rs b/helix-core/src/increment/number.rs
index e9f3c898..57171f67 100644
--- a/helix-core/src/numbers.rs
+++ b/helix-core/src/increment/number.rs
@@ -2,6 +2,8 @@ use std::borrow::Cow;
use ropey::RopeSlice;
+use super::Increment;
+
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
@@ -9,9 +11,9 @@ use crate::{
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
- pub range: Range,
- pub value: i64,
- pub radix: u32,
+ value: i64,
+ radix: u32,
+ range: Range,
text: RopeSlice<'a>,
}
@@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
text,
})
}
+}
- /// Add `amount` to the number and return the formatted text.
- pub fn incremented_text(&self, amount: i64) -> Tendril {
+impl<'a> Increment for NumberIncrementor<'a> {
+ fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
@@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> {
}
}
- new_text.into()
+ (self.range, new_text.into())
}
}
@@ -366,8 +369,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -392,8 +396,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -419,8 +424,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -464,8 +470,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -491,8 +498,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 8ccc0120..5d20edc1 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -1,6 +1,5 @@
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
- find_first_non_whitespace_char,
syntax::{IndentQuery, LanguageConfiguration, Syntax},
tree_sitter::Node,
Rope, RopeSlice,
@@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
/// To determine indentation of a newly inserted line, figure out the indentation at the last col
/// of the previous line.
-#[allow(dead_code)]
-fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
+pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
let mut len = 0;
for ch in line.chars() {
match ch {
@@ -207,10 +205,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
Some(node)
}
-fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
- // NOTE: can't use contains() on query because of comparing Vec<String> and &str
- // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
-
+/// Calculate the indentation at a given treesitter node.
+/// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies).
+/// This is because the indentation is only increased starting at the second line of the node.
+fn calculate_indentation(
+ query: &IndentQuery,
+ node: Option<Node>,
+ line: usize,
+ newline: bool,
+) -> usize {
let mut increment: isize = 0;
let mut node = match node {
@@ -218,70 +221,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
None => return 0,
};
- let mut prev_start = node.start_position().row;
-
- // if we're calculating indentation for a brand new line then the current node will become the
- // parent node. We need to take it's indentation level into account too.
- let node_kind = node.kind();
- if newline && query.indent.contains(node_kind) {
- increment += 1;
- }
-
- while let Some(parent) = node.parent() {
- let parent_kind = parent.kind();
- let start = parent.start_position().row;
-
- // detect deeply nested indents in the same line
- // .map(|a| { <-- ({ is two scopes
- // let len = 1; <-- indents one level
- // }) <-- }) is two scopes
- let starts_same_line = start == prev_start;
-
- if query.outdent.contains(node.kind()) && !starts_same_line {
- // we outdent by skipping the rules for the current level and jumping up
- // node = parent;
- increment -= 1;
- // continue;
+ let mut current_line = line;
+ let mut consider_indent = newline;
+ let mut increment_from_line: isize = 0;
+
+ loop {
+ let node_kind = node.kind();
+ let start = node.start_position().row;
+ if current_line != start {
+ // Indent/dedent by at most one per line:
+ // .map(|a| { <-- ({ is two scopes
+ // let len = 1; <-- indents one level
+ // }) <-- }) is two scopes
+ if consider_indent || increment_from_line < 0 {
+ increment += increment_from_line.signum();
+ }
+ increment_from_line = 0;
+ current_line = start;
+ consider_indent = true;
}
- if query.indent.contains(parent_kind) // && not_first_or_last_sibling
- && !starts_same_line
- {
- // println!("is_scope {}", parent_kind);
- prev_start = start;
- increment += 1
+ if query.outdent.contains(node_kind) {
+ increment_from_line -= 1;
+ }
+ if query.indent.contains(node_kind) {
+ increment_from_line += 1;
}
- // if last_scope && increment > 0 && ...{ ignore }
-
- node = parent;
+ if let Some(parent) = node.parent() {
+ node = parent;
+ } else {
+ break;
+ }
+ }
+ if consider_indent || increment_from_line < 0 {
+ increment += increment_from_line.signum();
}
-
increment.max(0) as usize
}
-#[allow(dead_code)]
-fn suggested_indent_for_line(
- language_config: &LanguageConfiguration,
- syntax: Option<&Syntax>,
- text: RopeSlice,
- line_num: usize,
- _tab_width: usize,
-) -> usize {
- if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
- return suggested_indent_for_pos(
- Some(language_config),
- syntax,
- text,
- start + text.line_to_char(line_num),
- false,
- );
- };
-
- // if the line is blank, indent should be zero
- 0
-}
-
// TODO: two usecases: if we are triggering this for a new, blank line:
// - it should return 0 when mass indenting stuff
// - it should look up the wrapper node and count it too when we press o/O
@@ -290,23 +268,20 @@ pub fn suggested_indent_for_pos(
syntax: Option<&Syntax>,
text: RopeSlice,
pos: usize,
+ line: usize,
new_line: bool,
-) -> usize {
+) -> Option<usize> {
if let (Some(query), Some(syntax)) = (
language_config.and_then(|config| config.indent_query()),
syntax,
) {
let byte_start = text.char_to_byte(pos);
let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
-
- // let config = load indentation query config from Syntax(should contain language_config)
-
// TODO: special case for comments
// TODO: if preserve_leading_whitespace
- calculate_indentation(query, node, new_line)
+ Some(calculate_indentation(query, node, line, new_line))
} else {
- // TODO: heuristics for non-tree sitter grammars
- 0
+ None
}
}
@@ -438,7 +413,8 @@ where
",
);
- let doc = Rope::from(doc);
+ let doc = doc;
+ use crate::diagnostic::Severity;
use crate::syntax::{
Configuration, IndentationConfiguration, LanguageConfiguration, Loader,
};
@@ -456,6 +432,8 @@ where
roots: vec![],
comment_token: None,
auto_format: false,
+ diagnostic_severity: Severity::Warning,
+ tree_sitter_library: None,
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
@@ -474,20 +452,29 @@ where
let language_config = loader.language_config_for_scope("source.rust").unwrap();
let highlight_config = language_config.highlight_config(&[]).unwrap();
- let syntax = Syntax::new(&doc, highlight_config.clone());
+ let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader));
let text = doc.slice(..);
let tab_width = 4;
for i in 0..doc.len_lines() {
let line = text.line(i);
- let indent = indent_level_for_line(line, tab_width);
- assert_eq!(
- suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
- indent,
- "line {}: {}",
- i,
- line
- );
+ if let Some(pos) = crate::find_first_non_whitespace_char(line) {
+ let indent = indent_level_for_line(line, tab_width);
+ assert_eq!(
+ suggested_indent_for_pos(
+ Some(&language_config),
+ Some(&syntax),
+ text,
+ text.line_to_char(i) + pos,
+ i,
+ false
+ ),
+ Some(indent),
+ "line {}: \"{}\"",
+ i,
+ line
+ );
+ }
}
}
}
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 7d790406..fa8566ab 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -1,3 +1,5 @@
+pub use encoding_rs as encoding;
+
pub mod auto_pairs;
pub mod chars;
pub mod comment;
@@ -5,18 +7,19 @@ pub mod diagnostic;
pub mod diff;
pub mod graphemes;
pub mod history;
+pub mod increment;
pub mod indent;
pub mod line_ending;
pub mod macros;
pub mod match_brackets;
pub mod movement;
-pub mod numbers;
pub mod object;
pub mod path;
mod position;
pub mod register;
pub mod search;
pub mod selection;
+pub mod shellwords;
mod state;
pub mod surround;
pub mod syntax;
@@ -36,8 +39,14 @@ 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> {
+/// Find project root.
+///
+/// Order of detection:
+/// * Top-most folder containing a root marker in current git repository
+/// * Git repostory root if no marker detected
+/// * Top-most folder containing a root marker if not git repository detected
+/// * Current working directory as fallback
+pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let root = match root {
@@ -49,16 +58,30 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
current_dir.join(root)
}
}
- None => current_dir,
+ None => current_dir.clone(),
};
+ let mut top_marker = None;
for ancestor in root.ancestors() {
- // TODO: also use defined roots if git isn't found
+ for marker in root_markers {
+ if ancestor.join(marker).exists() {
+ top_marker = Some(ancestor);
+ break;
+ }
+ }
+ // don't go higher than repo
if ancestor.join(".git").is_dir() {
- return Some(ancestor.to_path_buf());
+ // Use workspace if detected from marker
+ return Some(top_marker.unwrap_or(ancestor).to_path_buf());
}
}
- None
+
+ // In absence of git repo, use workspace if detected
+ if top_marker.is_some() {
+ top_marker.map(|a| a.to_path_buf())
+ } else {
+ Some(current_dir)
+ }
}
pub fn runtime_dir() -> std::path::PathBuf {
@@ -158,7 +181,7 @@ mod merge_toml_tests {
";
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
- .expect("Couldn't parse built-in langauges config");
+ .expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user);
@@ -189,7 +212,10 @@ use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
pub use ropey::{Rope, RopeBuilder, RopeSlice};
-pub use tendril::StrTendril as Tendril;
+// pub use tendril::StrTendril as Tendril;
+pub use smartstring::SmartString;
+
+pub type Tendril = SmartString<smartstring::LazyCompact>;
#[doc(inline)]
pub use {regex, tree_sitter};
diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs
index 3541305c..8eb426e1 100644
--- a/helix-core/src/line_ending.rs
+++ b/helix-core/src/line_ending.rs
@@ -250,7 +250,7 @@ mod line_ending_tests {
assert_eq!(get_line_ending_of_str(&text[..6]), Some(LineEnding::CR));
assert_eq!(get_line_ending_of_str(&text[..12]), Some(LineEnding::LF));
assert_eq!(get_line_ending_of_str(&text[..17]), Some(LineEnding::Crlf));
- assert_eq!(get_line_ending_of_str(&text[..]), None);
+ assert_eq!(get_line_ending_of_str(text), None);
}
#[test]
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index cd554005..0189dedd 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -11,7 +11,7 @@ const PAIRS: &[(char, char)] = &[
('\"', '\"'),
];
-// limit matching pairs to only ( ) { } [ ] < >
+// limit matching pairs to only ( ) { } [ ] < > ' ' " "
// Returns the position of the matching bracket under cursor.
//
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 01a8f890..47fe6827 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -307,8 +307,6 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
#[cfg(test)]
mod test {
- use std::array::{self, IntoIter};
-
use ropey::Rope;
use super::*;
@@ -360,7 +358,7 @@ mod test {
((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
];
- for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
+ for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
}
@@ -374,7 +372,7 @@ mod test {
let mut range = Range::point(position);
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
@@ -384,7 +382,7 @@ mod test {
((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
- ]);
+ ];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
@@ -402,11 +400,11 @@ mod test {
let mut range = Range::point(position);
let original_anchor = range.anchor;
- let moves = IntoIter::new([
+ let moves = [
(Direction::Forward, 1usize),
(Direction::Forward, 5usize),
(Direction::Backward, 3usize),
- ]);
+ ];
for (direction, amount) in moves {
range = move_horizontally(slice, range, direction, amount, Movement::Extend);
@@ -420,7 +418,7 @@ mod test {
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into(), true);
let mut range = Range::point(position);
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
((Direction::Forward, 1usize), (1, 0)),
((Direction::Forward, 2usize), (3, 0)),
((Direction::Forward, 1usize), (4, 0)),
@@ -430,7 +428,7 @@ mod test {
((Direction::Backward, 0usize), (4, 0)),
((Direction::Forward, 5), (5, 0)),
((Direction::Forward, 999usize), (5, 0)),
- ]);
+ ];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_vertically(slice, range, direction, amount, Movement::Move);
@@ -450,7 +448,7 @@ mod test {
H,
V,
}
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
// Places cursor at the end of line
((Axis::H, Direction::Forward, 8usize), (0, 8)),
// First descent preserves column as the target line is wider
@@ -463,7 +461,7 @@ mod test {
((Axis::V, Direction::Backward, 999usize), (0, 8)),
((Axis::V, Direction::Forward, 4usize), (4, 8)),
((Axis::V, Direction::Forward, 999usize), (5, 0)),
- ]);
+ ];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
@@ -489,7 +487,7 @@ mod test {
H,
V,
}
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
// Places cursor at the fourth kana.
((Axis::H, Direction::Forward, 4), (0, 4)),
// Descent places cursor at the 4th character.
@@ -498,7 +496,7 @@ mod test {
((Axis::H, Direction::Backward, 1usize), (1, 3)),
// Jumping back up 1 line.
((Axis::V, Direction::Backward, 1usize), (0, 3)),
- ]);
+ ];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
@@ -530,7 +528,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_next_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 6))]),
(" Starting from a boundary advances the anchor",
@@ -604,7 +602,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 6)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -616,7 +614,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_next_long_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 6))]),
(" Starting from a boundary advances the anchor",
@@ -688,7 +686,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 8)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -700,7 +698,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_previous_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))]),
@@ -773,7 +771,7 @@ mod test {
vec![
(1, Range::new(0, 6), Range::new(6, 0)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -785,7 +783,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_previous_long_words() {
- let tests = array::IntoIter::new([
+ let tests = [
(
"Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))],
@@ -870,7 +868,7 @@ mod test {
vec![
(1, Range::new(0, 8), Range::new(8, 0)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -882,7 +880,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_end_of_next_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
("Basic forward motion from the end of a word to the end of the next",
@@ -954,7 +952,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -966,7 +964,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_end_of_previous_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic backward motion from the middle of a word",
vec![(1, Range::new(9, 9), Range::new(10, 5))]),
("Starting from after boundary retreats the anchor",
@@ -1036,7 +1034,7 @@ mod test {
vec![
(1, Range::new(0, 10), Range::new(10, 4)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -1048,7 +1046,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_end_of_next_long_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
("Basic forward motion from the end of a word to the end of the next",
@@ -1118,7 +1116,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 7)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs
index 717c5994..b06f4144 100644
--- a/helix-core/src/object.rs
+++ b/helix-core/src/object.rs
@@ -1,31 +1,72 @@
use crate::{Range, RopeSlice, Selection, Syntax};
+use tree_sitter::Node;
-// TODO: to contract_selection we'd need to store the previous ranges before expand.
-// Maybe just contract to the first child node?
-pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
+pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ select_node_impl(syntax, text, selection, |descendant, from, to| {
+ if descendant.start_byte() == from && descendant.end_byte() == to {
+ descendant.parent()
+ } else {
+ Some(descendant)
+ }
+ })
+}
+
+pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ select_node_impl(syntax, text, selection, |descendant, _from, _to| {
+ descendant.child(0).or(Some(descendant))
+ })
+}
+
+pub fn select_sibling<F>(
+ syntax: &Syntax,
+ text: RopeSlice,
+ selection: Selection,
+ sibling_fn: &F,
+) -> Selection
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ select_node_impl(syntax, text, selection, |descendant, _from, _to| {
+ find_sibling_recursive(descendant, sibling_fn)
+ })
+}
+
+fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ sibling_fn(node).or_else(|| {
+ node.parent()
+ .and_then(|node| find_sibling_recursive(node, sibling_fn))
+ })
+}
+
+fn select_node_impl<F>(
+ syntax: &Syntax,
+ text: RopeSlice,
+ selection: Selection,
+ select_fn: F,
+) -> Selection
+where
+ F: Fn(Node, usize, usize) -> Option<Node>,
+{
let tree = syntax.tree();
- selection.clone().transform(|range| {
+ selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
- // find parent of a descendant that matches the range
- let parent = match tree
+ let node = match tree
.root_node()
.descendant_for_byte_range(from, to)
- .and_then(|node| {
- if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
- node.parent()
- } else {
- Some(node)
- }
- }) {
- Some(parent) => parent,
+ .and_then(|node| select_fn(node, from, to))
+ {
+ Some(node) => node,
None => return range,
};
- let from = text.byte_to_char(parent.start_byte());
- let to = text.byte_to_char(parent.end_byte());
+ let from = text.byte_to_char(node.start_byte());
+ let to = text.byte_to_char(node.end_byte());
if range.head < range.anchor {
Range::new(to, from)
diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs
index c6018ce6..93362c77 100644
--- a/helix-core/src/position.rs
+++ b/helix-core/src/position.rs
@@ -109,7 +109,10 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
/// TODO: this should be changed to work in terms of visual row/column, not
/// graphemes.
pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize {
- let Position { row, col } = coords;
+ let Position { mut row, col } = coords;
+ if limit_before_line_ending {
+ row = row.min(text.len_lines() - 1);
+ };
let line_start = text.line_to_char(row);
let line_end = if limit_before_line_ending {
line_end_char_index(&text, row)
@@ -290,5 +293,12 @@ mod test {
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
+
+ // Test out of bounds.
+ let text = Rope::new();
+ let slice = text.slice(..);
+ assert_eq!(pos_at_coords(slice, (10, 0).into(), true), 0);
+ assert_eq!(pos_at_coords(slice, (0, 10).into(), true), 0);
+ assert_eq!(pos_at_coords(slice, (10, 10).into(), true), 0);
}
}
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index b9eb497d..b39e4034 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -68,4 +68,8 @@ impl Registers {
pub fn read(&self, name: char) -> Option<&[String]> {
self.get(name).map(|reg| reg.read())
}
+
+ pub fn inner(&self) -> &HashMap<char, Register> {
+ &self.inner
+ }
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 116a1c7c..c6eceb4b 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -7,6 +7,7 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary,
},
+ movement::Direction,
Assoc, ChangeSet, RopeSlice,
};
use smallvec::{smallvec, SmallVec};
@@ -82,6 +83,13 @@ impl Range {
std::cmp::max(self.anchor, self.head)
}
+ /// Total length of the range.
+ #[inline]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.to() - self.from()
+ }
+
/// The (inclusive) range of lines that the range overlaps.
#[inline]
#[must_use]
@@ -102,6 +110,27 @@ impl Range {
self.anchor == self.head
}
+ /// `Direction::Backward` when head < anchor.
+ /// `Direction::Backward` otherwise.
+ #[inline]
+ #[must_use]
+ pub fn direction(&self) -> Direction {
+ if self.head < self.anchor {
+ Direction::Backward
+ } else {
+ Direction::Forward
+ }
+ }
+
+ // flips the direction of the selection
+ pub fn flip(&self) -> Self {
+ Self {
+ anchor: self.head,
+ head: self.anchor,
+ horiz: self.horiz,
+ }
+ }
+
/// Check two ranges for overlap.
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
@@ -111,6 +140,11 @@ impl Range {
self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
}
+ #[inline]
+ pub fn contains_range(&self, other: &Self) -> bool {
+ self.from() <= other.from() && self.to() >= other.to()
+ }
+
pub fn contains(&self, pos: usize) -> bool {
self.from() <= pos && pos < self.to()
}
@@ -515,6 +549,39 @@ impl Selection {
pub fn len(&self) -> usize {
self.ranges.len()
}
+
+ // returns true if self βŠ‡ other
+ pub fn contains(&self, other: &Selection) -> bool {
+ // can't contain other if it is larger
+ if other.len() > self.len() {
+ return false;
+ }
+
+ let (mut iter_self, mut iter_other) = (self.iter(), other.iter());
+ let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
+
+ loop {
+ match (ele_self, ele_other) {
+ (Some(ra), Some(rb)) => {
+ if !ra.contains_range(rb) {
+ // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other`
+ ele_self = iter_self.next();
+ } else {
+ // matched element from `other`, advance `other`
+ ele_other = iter_other.next();
+ };
+ }
+ (None, Some(_)) => {
+ // exhausted `self`, we can't match the reminder of `other`
+ return false;
+ }
+ (_, None) => {
+ // no elements from `other` left to match, `self` contains `other`
+ return true;
+ }
+ }
+ }
+ }
}
impl<'a> IntoIterator for &'a Selection {
@@ -699,16 +766,16 @@ mod test {
fn test_contains() {
let range = Range::new(10, 12);
- assert_eq!(range.contains(9), false);
- assert_eq!(range.contains(10), true);
- assert_eq!(range.contains(11), true);
- assert_eq!(range.contains(12), false);
- assert_eq!(range.contains(13), false);
+ assert!(!range.contains(9));
+ assert!(range.contains(10));
+ assert!(range.contains(11));
+ assert!(!range.contains(12));
+ assert!(!range.contains(13));
let range = Range::new(9, 6);
- assert_eq!(range.contains(9), false);
- assert_eq!(range.contains(7), true);
- assert_eq!(range.contains(6), true);
+ assert!(!range.contains(9));
+ assert!(range.contains(7));
+ assert!(range.contains(6));
}
#[test]
@@ -953,4 +1020,30 @@ mod test {
&["", "abcd", "efg", "rs", "xyz"]
);
}
+ #[test]
+ fn test_selection_contains() {
+ fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {
+ let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0);
+ let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0);
+ sela.contains(&selb)
+ }
+
+ // exact match
+ assert!(contains(vec!((1, 1)), vec!((1, 1))));
+
+ // larger set contains smaller
+ assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2))));
+
+ // multiple matches
+ assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
+
+ // smaller set can't contain bigger
+ assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
+
+ assert!(contains(
+ vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
+ vec!((3, 4), (7, 9))
+ ));
+ assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6))));
+ }
}
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
new file mode 100644
index 00000000..13f6f3e9
--- /dev/null
+++ b/helix-core/src/shellwords.rs
@@ -0,0 +1,164 @@
+use std::borrow::Cow;
+
+/// Get the vec of escaped / quoted / doublequoted filenames from the input str
+pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
+ enum State {
+ Normal,
+ NormalEscaped,
+ Quoted,
+ QuoteEscaped,
+ Dquoted,
+ DquoteEscaped,
+ }
+
+ use State::*;
+
+ let mut state = Normal;
+ let mut args: Vec<Cow<str>> = Vec::new();
+ let mut escaped = String::with_capacity(input.len());
+
+ let mut start = 0;
+ let mut end = 0;
+
+ for (i, c) in input.char_indices() {
+ state = match state {
+ Normal => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ NormalEscaped
+ }
+ '"' => {
+ end = i;
+ Dquoted
+ }
+ '\'' => {
+ end = i;
+ Quoted
+ }
+ c if c.is_ascii_whitespace() => {
+ end = i;
+ Normal
+ }
+ _ => Normal,
+ },
+ NormalEscaped => Normal,
+ Quoted => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ QuoteEscaped
+ }
+ '\'' => {
+ end = i;
+ Normal
+ }
+ _ => Quoted,
+ },
+ QuoteEscaped => Quoted,
+ Dquoted => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ DquoteEscaped
+ }
+ '"' => {
+ end = i;
+ Normal
+ }
+ _ => Dquoted,
+ },
+ DquoteEscaped => Dquoted,
+ };
+
+ if i >= input.len() - 1 && end == 0 {
+ end = i + 1;
+ }
+
+ if end > 0 {
+ let esc_trim = escaped.trim();
+ let inp = &input[start..end];
+
+ if !(esc_trim.is_empty() && inp.trim().is_empty()) {
+ if esc_trim.is_empty() {
+ args.push(inp.into());
+ } else {
+ args.push([escaped, inp.into()].concat().into());
+ escaped = "".to_string();
+ }
+ }
+ start = i + 1;
+ end = 0;
+ }
+ }
+ args
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_normal() {
+ let input = r#":o single_word twΓ³ wΓΆrds \three\ \"with\ escaping\\"#;
+ let result = shellwords(input);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twΓ³"),
+ Cow::from("wΓΆrds"),
+ Cow::from(r#"three "with escaping\"#),
+ ];
+ // TODO test is_owned and is_borrowed, once they get stabilized.
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_quoted() {
+ let quoted =
+ r#":o 'single_word' 'twΓ³ wΓΆrds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
+ let result = shellwords(quoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twΓ³ wΓΆrds"),
+ Cow::from(r#"three' "with escaping\"#),
+ Cow::from("quote incomplete"),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_dquoted() {
+ let dquoted = r#":o "single_word" "twΓ³ wΓΆrds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
+ let result = shellwords(dquoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twΓ³ wΓΆrds"),
+ Cow::from(r#"three' "with escaping\"#),
+ Cow::from("dquote incomplete"),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_mixed() {
+ let dquoted = r#":o single_word 'twΓ³ wΓΆrds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
+ let result = shellwords(dquoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twΓ³ wΓΆrds"),
+ Cow::from("three' \"with escaping\\"),
+ Cow::from("no space before"),
+ Cow::from("and after"),
+ Cow::from("$#%^@"),
+ Cow::from("%^&(%^"),
+ Cow::from(")(*&^%"),
+ Cow::from(r#"a\\b"#),
+ //last ' just changes to quoted but since we dont have anything after it, it should be ignored
+ ];
+ assert_eq!(expected, result);
+ }
+}
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index b53b0a78..58eb23cf 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -172,6 +172,7 @@ mod test {
use ropey::Rope;
use smallvec::SmallVec;
+ #[allow(clippy::type_complexity)]
fn check_find_nth_pair_pos(
text: &str,
cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index f1c399d2..a5c5e498 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -1,5 +1,6 @@
use crate::{
chars::char_is_line_ending,
+ diagnostic::Severity,
regex::Regex,
transaction::{ChangeSet, Operation},
Rope, RopeSlice, Tendril,
@@ -7,12 +8,13 @@ use crate::{
pub use helix_syntax::get_language;
-use arc_swap::ArcSwap;
+use arc_swap::{ArcSwap, Guard};
+use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
borrow::Cow,
cell::RefCell,
- collections::{HashMap, HashSet},
+ collections::{HashMap, HashSet, VecDeque},
fmt,
path::Path,
sync::Arc,
@@ -50,7 +52,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
- pub language_id: String,
+ pub language_id: String, // c-sharp, rust
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
#[serde(default)]
@@ -63,6 +65,10 @@ pub struct LanguageConfiguration {
#[serde(default)]
pub auto_format: bool,
+ #[serde(default)]
+ pub diagnostic_severity: Severity,
+
+ pub tree_sitter_library: Option<String>, // tree-sitter library name, defaults to language_id
// content_regex
#[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
@@ -92,6 +98,7 @@ pub struct LanguageServerConfiguration {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
+ pub language_id: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
@@ -245,20 +252,22 @@ impl LanguageConfiguration {
if highlights_query.is_empty() {
None
} else {
- let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
- .map_err(|e| log::info!("{}", e))
- .ok()?;
+ let language = get_language(
+ &crate::RUNTIME_DIR,
+ self.tree_sitter_library
+ .as_deref()
+ .unwrap_or(&self.language_id),
+ )
+ .map_err(|e| log::info!("{}", e))
+ .ok()?;
let config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
&locals_query,
- );
+ )
+ .unwrap(); // TODO: avoid panic
- let config = match config {
- Ok(config) => config,
- Err(err) => panic!("{}", err),
- }; // TODO: avoid panic
config.configure(scopes);
Some(Arc::new(config))
}
@@ -308,12 +317,16 @@ impl LanguageConfiguration {
}
}
+// Expose loader as Lazy<> global since it's always static?
+
#[derive(Debug)]
pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_shebang: HashMap<String, usize>,
+
+ scopes: ArcSwap<Vec<String>>,
}
impl Loader {
@@ -322,6 +335,7 @@ impl Loader {
language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
+ scopes: ArcSwap::from_pointee(Vec::new()),
};
for config in config.language {
@@ -366,8 +380,9 @@ impl Loader {
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
- static SHEBANG_REGEX: Lazy<Regex> =
- Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
+ static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
+ Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
+ });
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
@@ -406,8 +421,22 @@ impl Loader {
}
None
}
- pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
- self.language_configs.iter()
+
+ pub fn set_scopes(&self, scopes: Vec<String>) {
+ self.scopes.store(Arc::new(scopes));
+
+ // Reconfigure existing grammars
+ for config in self
+ .language_configs
+ .iter()
+ .filter(|cfg| cfg.is_highlight_initialized())
+ {
+ config.reconfigure(&self.scopes());
+ }
+ }
+
+ pub fn scopes(&self) -> Guard<Arc<Vec<String>>> {
+ self.scopes.load()
}
}
@@ -416,12 +445,6 @@ pub struct TsParser {
cursors: Vec<QueryCursor>,
}
-impl fmt::Debug for TsParser {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("TsParser").finish()
- }
-}
-
// could also just use a pool, or a single instance?
thread_local! {
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -432,9 +455,9 @@ thread_local! {
#[derive(Debug)]
pub struct Syntax {
- config: Arc<HighlightConfiguration>,
-
- root_layer: LanguageLayer,
+ layers: HopSlotMap<LayerId, LanguageLayer>,
+ root: LayerId,
+ loader: Arc<Loader>,
}
fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
@@ -444,38 +467,34 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
}
impl Syntax {
- // buffer, grammar, config, grammars, sync_timeout?
- pub fn new(
- /*language: Lang,*/ source: &Rope,
- config: Arc<HighlightConfiguration>,
- ) -> Self {
- let root_layer = LanguageLayer { tree: None };
+ pub fn new(source: &Rope, config: Arc<HighlightConfiguration>, loader: Arc<Loader>) -> Self {
+ let root_layer = LanguageLayer {
+ tree: None,
+ config,
+ depth: 0,
+ ranges: vec![Range {
+ start_byte: 0,
+ end_byte: usize::MAX,
+ start_point: Point::new(0, 0),
+ end_point: Point::new(usize::MAX, usize::MAX),
+ }],
+ };
- // track markers of injections
// track scope_descriptor: a Vec of scopes for item in tree
+ let mut layers = HopSlotMap::default();
+ let root = layers.insert(root_layer);
+
let mut syntax = Self {
- // grammar,
- config,
- root_layer,
+ root,
+ layers,
+ loader,
};
- // update root layer
- PARSER.with(|ts_parser| {
- // TODO: handle the returned `Result` properly.
- let _ = syntax.root_layer.parse(
- &mut ts_parser.borrow_mut(),
- &syntax.config,
- source,
- 0,
- vec![Range {
- start_byte: 0,
- end_byte: usize::MAX,
- start_point: Point::new(0, 0),
- end_point: Point::new(usize::MAX, usize::MAX),
- }],
- );
- });
+ syntax
+ .update(source, source, &ChangeSet::new(source))
+ .unwrap();
+
syntax
}
@@ -485,32 +504,255 @@ impl Syntax {
source: &Rope,
changeset: &ChangeSet,
) -> Result<(), Error> {
+ let mut queue = VecDeque::new();
+ queue.push_back(self.root);
+
+ let scopes = self.loader.scopes.load();
+ let injection_callback = |language: &str| {
+ self.loader
+ .language_configuration_for_injection_string(language)
+ .and_then(|language_config| language_config.highlight_config(&scopes))
+ };
+
+ // Convert the changeset into tree sitter edits.
+ let edits = generate_edits(old_source, changeset);
+
+ // Use the edits to update all layers markers
+ if !edits.is_empty() {
+ fn point_add(a: Point, b: Point) -> Point {
+ if b.row > 0 {
+ Point::new(a.row.saturating_add(b.row), b.column)
+ } else {
+ Point::new(0, a.column.saturating_add(b.column))
+ }
+ }
+ fn point_sub(a: Point, b: Point) -> Point {
+ if a.row > b.row {
+ Point::new(a.row.saturating_sub(b.row), a.column)
+ } else {
+ Point::new(0, a.column.saturating_sub(b.column))
+ }
+ }
+
+ for layer in &mut self.layers.values_mut() {
+ // The root layer always covers the whole range (0..usize::MAX)
+ if layer.depth == 0 {
+ continue;
+ }
+
+ for range in &mut layer.ranges {
+ // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
+ for edit in edits.iter().rev() {
+ let is_pure_insertion = edit.old_end_byte == edit.start_byte;
+
+ // if edit is after range, skip
+ if edit.start_byte > range.end_byte {
+ // TODO: || (is_noop && edit.start_byte == range.end_byte)
+ continue;
+ }
+
+ // if edit is before range, shift entire range by len
+ if edit.old_end_byte < range.start_byte {
+ range.start_byte =
+ edit.new_end_byte + (range.start_byte - edit.old_end_byte);
+ range.start_point = point_add(
+ edit.new_end_position,
+ point_sub(range.start_point, edit.old_end_position),
+ );
+
+ range.end_byte = edit
+ .new_end_byte
+ .saturating_add(range.end_byte - edit.old_end_byte);
+ range.end_point = point_add(
+ edit.new_end_position,
+ point_sub(range.end_point, edit.old_end_position),
+ );
+ }
+ // if the edit starts in the space before and extends into the range
+ else if edit.start_byte < range.start_byte {
+ range.start_byte = edit.new_end_byte;
+ range.start_point = edit.new_end_position;
+
+ range.end_byte = range
+ .end_byte
+ .saturating_sub(edit.old_end_byte)
+ .saturating_add(edit.new_end_byte);
+ range.end_point = point_add(
+ edit.new_end_position,
+ point_sub(range.end_point, edit.old_end_position),
+ );
+ }
+ // If the edit is an insertion at the start of the tree, shift
+ else if edit.start_byte == range.start_byte && is_pure_insertion {
+ range.start_byte = edit.new_end_byte;
+ range.start_point = edit.new_end_position;
+ } else {
+ range.end_byte = range
+ .end_byte
+ .saturating_sub(edit.old_end_byte)
+ .saturating_add(edit.new_end_byte);
+ range.end_point = point_add(
+ edit.new_end_position,
+ point_sub(range.end_point, edit.old_end_position),
+ );
+ }
+ }
+ }
+ }
+ }
+
PARSER.with(|ts_parser| {
- self.root_layer.update(
- &mut ts_parser.borrow_mut(),
- &self.config,
- old_source,
- source,
- changeset,
- )
- })
+ let ts_parser = &mut ts_parser.borrow_mut();
+ let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
+ // TODO: might need to set cursor range
+ cursor.set_byte_range(0..usize::MAX);
- // TODO: deal with injections and update them too
- }
+ let source_slice = source.slice(..);
- // fn buffer_changed -> call layer.update(range, new_text) on root layer and then all marker layers
+ let mut touched = HashSet::new();
- // call this on transaction.apply() -> buffer_changed(changes)
- //
- // fn parse(language, old_tree, ranges)
- //
- pub fn tree(&self) -> &Tree {
- self.root_layer.tree()
+ // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit
+
+ while let Some(layer_id) = queue.pop_front() {
+ // Mark the layer as touched
+ touched.insert(layer_id);
+
+ let layer = &mut self.layers[layer_id];
+
+ // If a tree already exists, notify it of changes.
+ if let Some(tree) = &mut layer.tree {
+ for edit in edits.iter().rev() {
+ // Apply the edits in reverse.
+ // If we applied them in order then edit 1 would disrupt the positioning of edit 2.
+ tree.edit(edit);
+ }
+ }
+
+ // Re-parse the tree.
+ layer.parse(&mut ts_parser.parser, source)?;
+
+ // Switch to an immutable borrow.
+ let layer = &self.layers[layer_id];
+
+ // Process injections.
+ let matches = cursor.matches(
+ &layer.config.injections_query,
+ layer.tree().root_node(),
+ RopeProvider(source_slice),
+ );
+ let mut injections = Vec::new();
+ for mat in matches {
+ let (language_name, content_node, include_children) = injection_for_match(
+ &layer.config,
+ &layer.config.injections_query,
+ &mat,
+ source_slice,
+ );
+
+ // Explicitly remove this match so that none of its other captures will remain
+ // in the stream of captures.
+ mat.remove();
+
+ // If a language is found with the given name, then add a new language layer
+ // to the highlighted document.
+ if let (Some(language_name), Some(content_node)) = (language_name, content_node)
+ {
+ if let Some(config) = (injection_callback)(&language_name) {
+ let ranges =
+ intersect_ranges(&layer.ranges, &[content_node], include_children);
+
+ if !ranges.is_empty() {
+ injections.push((config, ranges));
+ }
+ }
+ }
+ }
+
+ // Process combined injections.
+ if let Some(combined_injections_query) = &layer.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,
+ layer.tree().root_node(),
+ RopeProvider(source_slice),
+ );
+ for mat in matches {
+ let entry = &mut injections_by_pattern_index[mat.pattern_index];
+ let (language_name, content_node, include_children) = injection_for_match(
+ &layer.config,
+ combined_injections_query,
+ &mat,
+ source_slice,
+ );
+ 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(config) = (injection_callback)(&lang_name) {
+ let ranges = intersect_ranges(
+ &layer.ranges,
+ &content_nodes,
+ includes_children,
+ );
+ if !ranges.is_empty() {
+ injections.push((config, ranges));
+ }
+ }
+ }
+ }
+ }
+
+ let depth = layer.depth + 1;
+ // TODO: can't inline this since matches borrows self.layers
+ for (config, ranges) in injections {
+ // Find an existing layer
+ let layer = self
+ .layers
+ .iter_mut()
+ .find(|(_, layer)| {
+ layer.depth == depth && // TODO: track parent id instead
+ layer.config.language == config.language && layer.ranges == ranges
+ })
+ .map(|(id, _layer)| id);
+
+ // ...or insert a new one.
+ let layer_id = layer.unwrap_or_else(|| {
+ self.layers.insert(LanguageLayer {
+ tree: None,
+ config,
+ depth,
+ ranges,
+ })
+ });
+
+ queue.push_back(layer_id);
+ }
+
+ // TODO: pre-process local scopes at this time, rather than highlight?
+ // would solve problems with locals not working across boundaries
+ }
+
+ // Return the cursor back in the pool.
+ ts_parser.cursors.push(cursor);
+
+ // Remove all untouched layers
+ self.layers.retain(|id, _| touched.contains(&id));
+
+ Ok(())
+ })
}
- //
- // <!--update_for_injection(grammar)-->
- // Highlighting
+ pub fn tree(&self) -> &Tree {
+ self.layers[self.root].tree()
+ }
/// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>(
@@ -518,65 +760,76 @@ impl Syntax {
source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>,
cancellation_flag: Option<&'a AtomicUsize>,
- injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a {
- // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
- // prevents them from being moved. But both of these values are really just
- // pointers, so it's actually ok to move them.
-
- // reuse a cursor from the pool if possible
- let mut cursor = PARSER.with(|ts_parser| {
- let highlighter = &mut ts_parser.borrow_mut();
- highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
+ let mut layers = self
+ .layers
+ .iter()
+ .filter_map(|(_, layer)| {
+ // TODO: if range doesn't overlap layer range, skip it
+
+ // Reuse a cursor from the pool if available.
+ let mut cursor = PARSER.with(|ts_parser| {
+ let highlighter = &mut ts_parser.borrow_mut();
+ highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
+ });
+
+ // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
+ // prevents them from being moved. But both of these values are really just
+ // pointers, so it's actually ok to move them.
+ let cursor_ref =
+ unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
+
+ // if reusing cursors & no range this resets to whole range
+ cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
+
+ let mut captures = cursor_ref
+ .captures(
+ &layer.config.query,
+ layer.tree().root_node(),
+ RopeProvider(source),
+ )
+ .peekable();
+
+ // If there's no captures, skip the layer
+ captures.peek()?;
+
+ Some(HighlightIterLayer {
+ highlight_end_stack: Vec::new(),
+ scope_stack: vec![LocalScope {
+ inherits: false,
+ range: 0..usize::MAX,
+ local_defs: Vec::new(),
+ }],
+ cursor,
+ _tree: None,
+ captures,
+ config: layer.config.as_ref(), // TODO: just reuse `layer`
+ depth: layer.depth, // TODO: just reuse `layer`
+ ranges: &layer.ranges, // TODO: temp
+ })
+ })
+ .collect::<Vec<_>>();
+
+ // HAXX: arrange layers by byte range, with deeper layers positioned first
+ layers.sort_by_key(|layer| {
+ (
+ layer.ranges.first().cloned(),
+ std::cmp::Reverse(layer.depth),
+ )
});
- let tree_ref = self.tree();
- let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
- 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));
-
- let captures = cursor_ref
- .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
- .peekable();
-
- // manually craft the root layer based on the existing tree
- let layer = HighlightIterLayer {
- highlight_end_stack: Vec::new(),
- scope_stack: vec![LocalScope {
- inherits: false,
- range: 0..usize::MAX,
- local_defs: Vec::new(),
- }],
- cursor,
- depth: 0,
- _tree: None,
- captures,
- config: config_ref,
- ranges: vec![Range {
- start_byte: 0,
- end_byte: usize::MAX,
- start_point: Point::new(0, 0),
- end_point: Point::new(usize::MAX, usize::MAX),
- }],
- };
let mut result = HighlightIter {
source,
- byte_offset: range.map_or(0, |r| r.start), // TODO: simplify
- injection_callback,
+ byte_offset: range.map_or(0, |r| r.start),
cancellation_flag,
iter_count: 0,
- layers: vec![layer],
+ layers,
next_event: None,
last_highlight_range: None,
};
result.sort_layers();
result
}
- // on_tokenize
- // on_change_highlighting
// Commenting
// comment_strings_for_pos
@@ -588,246 +841,157 @@ impl Syntax {
// indent_level_for_line
// TODO: Folding
-
- // Syntax APIs
- // get_syntax_node_containing_range ->
- // ...
- // get_syntax_node_at_pos
- // buffer_range_for_scope_at_pos
}
#[derive(Debug)]
pub struct LanguageLayer {
// mode
// grammar
- // depth
+ pub config: Arc<HighlightConfiguration>,
pub(crate) tree: Option<Tree>,
+ pub ranges: Vec<Range>,
+ pub depth: usize,
}
impl LanguageLayer {
- // pub fn new() -> Self {
- // Self { tree: None }
- // }
-
pub fn tree(&self) -> &Tree {
// TODO: no unwrap
self.tree.as_ref().unwrap()
}
- fn parse(
- &mut self,
- ts_parser: &mut TsParser,
- config: &HighlightConfiguration,
- source: &Rope,
- _depth: usize,
- ranges: Vec<Range>,
- ) -> Result<(), Error> {
- if ts_parser.parser.set_included_ranges(&ranges).is_ok() {
- ts_parser
- .parser
- .set_language(config.language)
- .map_err(|_| Error::InvalidLanguage)?;
-
- // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
- let tree = ts_parser
- .parser
- .parse_with(
- &mut |byte, _| {
- if byte <= source.len_bytes() {
- let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
- chunk[byte - start_byte..].as_bytes()
- } else {
- // out of range
- &[]
- }
- },
- self.tree.as_ref(),
- )
- .ok_or(Error::Cancelled)?;
+ fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> {
+ parser.set_included_ranges(&self.ranges).unwrap();
- self.tree = Some(tree)
- }
+ parser
+ .set_language(self.config.language)
+ .map_err(|_| Error::InvalidLanguage)?;
+
+ // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
+ let tree = parser
+ .parse_with(
+ &mut |byte, _| {
+ if byte <= source.len_bytes() {
+ let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
+ chunk[byte - start_byte..].as_bytes()
+ } else {
+ // out of range
+ &[]
+ }
+ },
+ self.tree.as_ref(),
+ )
+ .ok_or(Error::Cancelled)?;
+ // unsafe { ts_parser.parser.set_cancellation_flag(None) };
+ self.tree = Some(tree);
Ok(())
}
+}
- pub(crate) fn generate_edits(
- old_text: RopeSlice,
- changeset: &ChangeSet,
- ) -> Vec<tree_sitter::InputEdit> {
- use Operation::*;
- let mut old_pos = 0;
+pub(crate) fn generate_edits(
+ old_text: &Rope,
+ changeset: &ChangeSet,
+) -> Vec<tree_sitter::InputEdit> {
+ use Operation::*;
+ let mut old_pos = 0;
- let mut edits = Vec::new();
+ let mut edits = Vec::new();
- let mut iter = changeset.changes.iter().peekable();
+ if changeset.changes.is_empty() {
+ return edits;
+ }
- // TODO; this is a lot easier with Change instead of Operation.
+ let mut iter = changeset.changes.iter().peekable();
- fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
- let byte = text.char_to_byte(pos); // <- attempted to index past end
- let line = text.char_to_line(pos);
- let line_start_byte = text.line_to_byte(line);
- let col = byte - line_start_byte;
+ // TODO; this is a lot easier with Change instead of Operation.
- (byte, Point::new(line, col))
- }
+ fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) {
+ let byte = text.char_to_byte(pos); // <- attempted to index past end
+ let line = text.char_to_line(pos);
+ let line_start_byte = text.line_to_byte(line);
+ let col = byte - line_start_byte;
- fn traverse(point: Point, text: &Tendril) -> Point {
- let Point {
- mut row,
- mut column,
- } = point;
-
- // TODO: there should be a better way here.
- let mut chars = text.chars().peekable();
- while let Some(ch) = chars.next() {
- if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
- row += 1;
- column = 0;
- } else {
- column += 1;
- }
+ (byte, Point::new(line, col))
+ }
+
+ fn traverse(point: Point, text: &Tendril) -> Point {
+ let Point {
+ mut row,
+ mut column,
+ } = point;
+
+ // TODO: there should be a better way here.
+ let mut chars = text.chars().peekable();
+ while let Some(ch) = chars.next() {
+ if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
+ row += 1;
+ column = 0;
+ } else {
+ column += 1;
}
- Point { row, column }
}
+ Point { row, column }
+ }
- while let Some(change) = iter.next() {
- let len = match change {
- Delete(i) | Retain(i) => *i,
- Insert(_) => 0,
- };
- let mut old_end = old_pos + len;
+ while let Some(change) = iter.next() {
+ let len = match change {
+ Delete(i) | Retain(i) => *i,
+ Insert(_) => 0,
+ };
+ let mut old_end = old_pos + len;
+
+ match change {
+ Retain(_) => {}
+ Delete(_) => {
+ let (start_byte, start_position) = point_at_pos(old_text, old_pos);
+ let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
+
+ // deletion
+ edits.push(tree_sitter::InputEdit {
+ start_byte, // old_pos to byte
+ old_end_byte, // old_end to byte
+ new_end_byte: start_byte, // old_pos to byte
+ start_position, // old pos to coords
+ old_end_position, // old_end to coords
+ new_end_position: start_position, // old pos to coords
+ });
+ }
+ Insert(s) => {
+ let (start_byte, start_position) = point_at_pos(old_text, old_pos);
- match change {
- Retain(_) => {}
- Delete(_) => {
- let (start_byte, start_position) = point_at_pos(old_text, old_pos);
+ // a subsequent delete means a replace, consume it
+ if let Some(Delete(len)) = iter.peek() {
+ old_end = old_pos + len;
let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
- // TODO: Position also needs to be byte based...
- // let byte = char_to_byte(old_pos)
- // let line = char_to_line(old_pos)
- // let line_start_byte = line_to_byte()
- // Position::new(line, line_start_byte - byte)
+ iter.next();
- // deletion
+ // replacement
edits.push(tree_sitter::InputEdit {
- start_byte, // old_pos to byte
- old_end_byte, // old_end to byte
- new_end_byte: start_byte, // old_pos to byte
- start_position, // old pos to coords
- old_end_position, // old_end to coords
- new_end_position: start_position, // old pos to coords
+ start_byte, // old_pos to byte
+ old_end_byte, // old_end to byte
+ new_end_byte: start_byte + s.len(), // old_pos to byte + s.len()
+ start_position, // old pos to coords
+ old_end_position, // old_end to coords
+ new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
+ });
+ } else {
+ // insert
+ edits.push(tree_sitter::InputEdit {
+ start_byte, // old_pos to byte
+ old_end_byte: start_byte, // same
+ new_end_byte: start_byte + s.len(), // old_pos + s.len()
+ start_position, // old pos to coords
+ old_end_position: start_position, // same
+ new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
});
- }
- Insert(s) => {
- let (start_byte, start_position) = point_at_pos(old_text, old_pos);
-
- // a subsequent delete means a replace, consume it
- if let Some(Delete(len)) = iter.peek() {
- old_end = old_pos + len;
- let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
-
- iter.next();
-
- // replacement
- edits.push(tree_sitter::InputEdit {
- start_byte, // old_pos to byte
- old_end_byte, // old_end to byte
- new_end_byte: start_byte + s.len(), // old_pos to byte + s.len()
- start_position, // old pos to coords
- old_end_position, // old_end to coords
- new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
- });
- } else {
- // insert
- edits.push(tree_sitter::InputEdit {
- start_byte, // old_pos to byte
- old_end_byte: start_byte, // same
- new_end_byte: start_byte + s.len(), // old_pos + s.len()
- start_position, // old pos to coords
- old_end_position: start_position, // same
- new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
- });
- }
}
}
- old_pos = old_end;
}
- edits
- }
-
- fn update(
- &mut self,
- ts_parser: &mut TsParser,
- config: &HighlightConfiguration,
- old_source: &Rope,
- source: &Rope,
- changeset: &ChangeSet,
- ) -> Result<(), Error> {
- if changeset.is_empty() {
- return Ok(());
- }
-
- let edits = Self::generate_edits(old_source.slice(..), changeset);
-
- // Notify the tree about all the changes
- for edit in edits.iter().rev() {
- // apply the edits in reverse. If we applied them in order then edit 1 would disrupt
- // the positioning of edit 2
- self.tree.as_mut().unwrap().edit(edit);
- }
-
- self.parse(
- ts_parser,
- config,
- source,
- 0,
- // TODO: what to do about this range on update
- vec![Range {
- start_byte: 0,
- end_byte: usize::MAX,
- start_point: Point::new(0, 0),
- end_point: Point::new(usize::MAX, usize::MAX),
- }],
- )
+ old_pos = old_end;
}
-
- // fn highlight_iter() -> same as Mode but for this layer. Mode composits these
- // fn buffer_changed
- // fn update(range)
- // fn update_injections()
+ edits
}
-// -- refactored from tree-sitter-highlight to be able to retain state
-// TODO: add seek() to iter
-
-// problem: any time a layer is updated it must update it's injections on the parent (potentially
-// removing some from use)
-// can't modify to vec and exist in it at the same time since that would violate borrows
-// maybe we can do with an arena
-// maybe just caching on the top layer and nevermind the injections for now?
-//
-// Grammar {
-// layers: Vec<Box<Layer>> to prevent memory moves when vec is modified
-// }
-// injections tracked by marker:
-// if marker areas match it's fine and update
-// if not found add new layer
-// if length 0 then area got removed, clean up the layer
-//
-// layer update:
-// if range.len = 0 then remove the layer
-// for change in changes { tree.edit(change) }
-// tree = parser.parse(.., tree, ..)
-// calculate affected range and update injections
-// injection update:
-// look for existing injections
-// if present, range = (first injection start, last injection end)
-//
-// For now cheat and just throw out non-root layers if they exist. This should still improve
-// parsing in majority of cases.
-
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
@@ -864,8 +1028,8 @@ pub enum HighlightEvent {
pub struct HighlightConfiguration {
pub language: Grammar,
pub query: Query,
+ injections_query: Query,
combined_injections_query: Option<Query>,
- locals_pattern_index: usize,
highlights_pattern_index: usize,
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>,
@@ -892,13 +1056,9 @@ struct LocalScope<'a> {
}
#[derive(Debug)]
-struct HighlightIter<'a, F>
-where
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+struct HighlightIter<'a> {
source: RopeSlice<'a>,
byte_offset: usize,
- injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize,
@@ -938,8 +1098,8 @@ struct HighlightIterLayer<'a> {
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
- ranges: Vec<Range>,
depth: usize,
+ ranges: &'a [Range],
}
impl<'a> fmt::Debug for HighlightIterLayer<'a> {
@@ -971,38 +1131,32 @@ impl HighlightConfiguration {
) -> Result<Self, QueryError> {
// Concatenate the query strings, keeping track of the start offset of each section.
let mut query_source = String::new();
- query_source.push_str(injection_query);
- let locals_query_offset = query_source.len();
query_source.push_str(locals_query);
let highlights_query_offset = query_source.len();
query_source.push_str(highlights_query);
// Construct a single query by concatenating the three query strings, but record the
// range of pattern indices that belong to each individual string.
- let mut query = Query::new(language, &query_source)?;
- let mut locals_pattern_index = 0;
+ let query = Query::new(language, &query_source)?;
let mut highlights_pattern_index = 0;
for i in 0..(query.pattern_count()) {
let pattern_offset = query.start_byte_for_pattern(i);
if pattern_offset < highlights_query_offset {
- if pattern_offset < highlights_query_offset {
- highlights_pattern_index += 1;
- }
- if pattern_offset < locals_query_offset {
- locals_pattern_index += 1;
- }
+ highlights_pattern_index += 1;
}
}
+ let mut injections_query = Query::new(language, injection_query)?;
+
// Construct a separate query just for dealing with the 'combined injections'.
// Disable the combined injection patterns in the main query.
let mut combined_injections_query = Query::new(language, injection_query)?;
let mut has_combined_queries = false;
- for pattern_index in 0..locals_pattern_index {
- let settings = query.property_settings(pattern_index);
+ for pattern_index in 0..injections_query.pattern_count() {
+ let settings = injections_query.property_settings(pattern_index);
if settings.iter().any(|s| &*s.key == "injection.combined") {
has_combined_queries = true;
- query.disable_pattern(pattern_index);
+ injections_query.disable_pattern(pattern_index);
} else {
combined_injections_query.disable_pattern(pattern_index);
}
@@ -1034,8 +1188,6 @@ impl HighlightConfiguration {
for (i, name) in query.capture_names().iter().enumerate() {
let i = Some(i as u32);
match name.as_str() {
- "injection.content" => injection_content_capture_index = i,
- "injection.language" => injection_language_capture_index = i,
"local.definition" => local_def_capture_index = i,
"local.definition-value" => local_def_value_capture_index = i,
"local.reference" => local_ref_capture_index = i,
@@ -1044,12 +1196,21 @@ impl HighlightConfiguration {
}
}
+ for (i, name) in injections_query.capture_names().iter().enumerate() {
+ let i = Some(i as u32);
+ match name.as_str() {
+ "injection.content" => injection_content_capture_index = i,
+ "injection.language" => injection_language_capture_index = i,
+ _ => {}
+ }
+ }
+
let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
Ok(Self {
language,
query,
+ injections_query,
combined_injections_query,
- locals_pattern_index,
highlights_pattern_index,
highlight_indices,
non_local_variable_patterns,
@@ -1114,238 +1275,6 @@ impl HighlightConfiguration {
}
impl<'a> HighlightIterLayer<'a> {
- /// Create a new 'layer' of highlighting for this document.
- ///
- /// In the even that the new layer contains "combined injections" (injections where multiple
- /// disjoint ranges are parsed as one syntax tree), these will be eagerly processed and
- /// added to the returned vector.
- fn new<F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a>(
- source: RopeSlice<'a>,
- cancellation_flag: Option<&'a AtomicUsize>,
- injection_callback: &mut F,
- mut config: &'a HighlightConfiguration,
- mut depth: usize,
- mut ranges: Vec<Range>,
- ) -> Result<Vec<Self>, Error> {
- let mut result = Vec::with_capacity(1);
- let mut queue = Vec::new();
- loop {
- // --> Tree parsing part
-
- PARSER.with(|ts_parser| {
- let highlighter = &mut ts_parser.borrow_mut();
-
- if highlighter.parser.set_included_ranges(&ranges).is_ok() {
- highlighter
- .parser
- .set_language(config.language)
- .map_err(|_| Error::InvalidLanguage)?;
-
- unsafe { highlighter.parser.set_cancellation_flag(cancellation_flag) };
- let tree = highlighter
- .parser
- .parse_with(
- &mut |byte, _| {
- if byte <= source.len_bytes() {
- let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
- chunk[byte - start_byte..].as_bytes()
- } else {
- // out of range
- &[]
- }
- },
- None,
- )
- .ok_or(Error::Cancelled)?;
- unsafe { highlighter.parser.set_cancellation_flag(None) };
- let mut cursor = highlighter.cursors.pop().unwrap_or_else(QueryCursor::new);
-
- // Process combined injections.
- 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));
- }
- }
- }
- }
- }
-
- // --> Highlighting query part
-
- // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
- // prevents them from being moved. But both of these values are really just
- // pointers, so it's actually ok to move them.
- let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(&tree) };
- let cursor_ref =
- unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
- let captures = cursor_ref
- .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
- .peekable();
-
- result.push(HighlightIterLayer {
- highlight_end_stack: Vec::new(),
- scope_stack: vec![LocalScope {
- inherits: false,
- range: 0..usize::MAX,
- local_defs: Vec::new(),
- }],
- cursor,
- depth,
- _tree: Some(tree),
- captures,
- config,
- ranges,
- });
- }
-
- Ok(()) // so we can use the try operator
- })?;
-
- if queue.is_empty() {
- break;
- }
-
- let (next_config, next_depth, next_ranges) = queue.remove(0);
- config = next_config;
- depth = next_depth;
- ranges = next_ranges;
- }
-
- Ok(result)
- }
-
- // Compute the ranges that should be included when parsing an injection.
- // This takes into account three things:
- // * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
- // * `nodes` - Every injection takes place within a set of nodes. The injection ranges
- // are the ranges of those nodes.
- // * `includes_children` - For some injections, the content nodes' children should be
- // excluded from the nested document, so that only the content nodes' *own* content
- // is reparsed. For other injections, the content nodes' entire ranges should be
- // reparsed, including the ranges of their children.
- fn intersect_ranges(
- parent_ranges: &[Range],
- nodes: &[Node],
- includes_children: bool,
- ) -> Vec<Range> {
- let mut cursor = nodes[0].walk();
- let mut result = Vec::new();
- let mut parent_range_iter = parent_ranges.iter();
- let mut parent_range = parent_range_iter
- .next()
- .expect("Layers should only be constructed with non-empty ranges vectors");
- for node in nodes.iter() {
- let mut preceding_range = Range {
- start_byte: 0,
- start_point: Point::new(0, 0),
- end_byte: node.start_byte(),
- end_point: node.start_position(),
- };
- let following_range = Range {
- start_byte: node.end_byte(),
- start_point: node.end_position(),
- end_byte: usize::MAX,
- end_point: Point::new(usize::MAX, usize::MAX),
- };
-
- for excluded_range in node
- .children(&mut cursor)
- .filter_map(|child| {
- if includes_children {
- None
- } else {
- Some(child.range())
- }
- })
- .chain([following_range].iter().cloned())
- {
- let mut range = Range {
- start_byte: preceding_range.end_byte,
- start_point: preceding_range.end_point,
- end_byte: excluded_range.start_byte,
- end_point: excluded_range.start_point,
- };
- preceding_range = excluded_range;
-
- if range.end_byte < parent_range.start_byte {
- continue;
- }
-
- while parent_range.start_byte <= range.end_byte {
- if parent_range.end_byte > range.start_byte {
- if range.start_byte < parent_range.start_byte {
- range.start_byte = parent_range.start_byte;
- range.start_point = parent_range.start_point;
- }
-
- if parent_range.end_byte < range.end_byte {
- if range.start_byte < parent_range.end_byte {
- result.push(Range {
- start_byte: range.start_byte,
- start_point: range.start_point,
- end_byte: parent_range.end_byte,
- end_point: parent_range.end_point,
- });
- }
- range.start_byte = parent_range.end_byte;
- range.start_point = parent_range.end_point;
- } else {
- if range.start_byte < range.end_byte {
- result.push(range);
- }
- break;
- }
- }
-
- if let Some(next_range) = parent_range_iter.next() {
- parent_range = next_range;
- } else {
- return result;
- }
- }
- }
- }
- result
- }
-
// First, sort scope boundaries by their byte offset in the document. At a
// given position, emit scope endings before scope beginnings. Finally, emit
// scope boundaries from deeper layers first.
@@ -1371,10 +1300,101 @@ impl<'a> HighlightIterLayer<'a> {
}
}
-impl<'a, F> HighlightIter<'a, F>
-where
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+// Compute the ranges that should be included when parsing an injection.
+// This takes into account three things:
+// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
+// * `nodes` - Every injection takes place within a set of nodes. The injection ranges
+// are the ranges of those nodes.
+// * `includes_children` - For some injections, the content nodes' children should be
+// excluded from the nested document, so that only the content nodes' *own* content
+// is reparsed. For other injections, the content nodes' entire ranges should be
+// reparsed, including the ranges of their children.
+fn intersect_ranges(
+ parent_ranges: &[Range],
+ nodes: &[Node],
+ includes_children: bool,
+) -> Vec<Range> {
+ let mut cursor = nodes[0].walk();
+ let mut result = Vec::new();
+ let mut parent_range_iter = parent_ranges.iter();
+ let mut parent_range = parent_range_iter
+ .next()
+ .expect("Layers should only be constructed with non-empty ranges vectors");
+ for node in nodes.iter() {
+ let mut preceding_range = Range {
+ start_byte: 0,
+ start_point: Point::new(0, 0),
+ end_byte: node.start_byte(),
+ end_point: node.start_position(),
+ };
+ let following_range = Range {
+ start_byte: node.end_byte(),
+ start_point: node.end_position(),
+ end_byte: usize::MAX,
+ end_point: Point::new(usize::MAX, usize::MAX),
+ };
+
+ for excluded_range in node
+ .children(&mut cursor)
+ .filter_map(|child| {
+ if includes_children {
+ None
+ } else {
+ Some(child.range())
+ }
+ })
+ .chain([following_range].iter().cloned())
+ {
+ let mut range = Range {
+ start_byte: preceding_range.end_byte,
+ start_point: preceding_range.end_point,
+ end_byte: excluded_range.start_byte,
+ end_point: excluded_range.start_point,
+ };
+ preceding_range = excluded_range;
+
+ if range.end_byte < parent_range.start_byte {
+ continue;
+ }
+
+ while parent_range.start_byte <= range.end_byte {
+ if parent_range.end_byte > range.start_byte {
+ if range.start_byte < parent_range.start_byte {
+ range.start_byte = parent_range.start_byte;
+ range.start_point = parent_range.start_point;
+ }
+
+ if parent_range.end_byte < range.end_byte {
+ if range.start_byte < parent_range.end_byte {
+ result.push(Range {
+ start_byte: range.start_byte,
+ start_point: range.start_point,
+ end_byte: parent_range.end_byte,
+ end_point: parent_range.end_point,
+ });
+ }
+ range.start_byte = parent_range.end_byte;
+ range.start_point = parent_range.end_point;
+ } else {
+ if range.start_byte < range.end_byte {
+ result.push(range);
+ }
+ break;
+ }
+ }
+
+ if let Some(next_range) = parent_range_iter.next() {
+ parent_range = next_range;
+ } else {
+ return result;
+ }
+ }
+ }
+ }
+ result
+}
+
+impl<'a> HighlightIter<'a> {
fn emit_event(
&mut self,
offset: usize,
@@ -1405,6 +1425,12 @@ where
i += 1;
continue;
}
+ } else {
+ let layer = self.layers.remove(i + 1);
+ PARSER.with(|ts_parser| {
+ let highlighter = &mut ts_parser.borrow_mut();
+ highlighter.cursors.push(layer.cursor);
+ });
}
break;
}
@@ -1421,30 +1447,9 @@ where
}
}
}
-
- fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
- if let Some(sort_key) = layer.sort_key() {
- let mut i = 1;
- while i < self.layers.len() {
- if let Some(sort_key_i) = self.layers[i].sort_key() {
- if sort_key_i > sort_key {
- self.layers.insert(i, layer);
- return;
- }
- i += 1;
- } else {
- self.layers.remove(i);
- }
- }
- self.layers.push(layer);
- }
- }
}
-impl<'a, F> Iterator for HighlightIter<'a, F>
-where
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+impl<'a> Iterator for HighlightIter<'a> {
type Item = Result<HighlightEvent, Error>;
fn next(&mut self) -> Option<Self::Item> {
@@ -1504,55 +1509,12 @@ where
layer.highlight_end_stack.pop();
return self.emit_event(end_byte, Some(HighlightEvent::HighlightEnd));
} else {
- // return self.emit_event(self.source.len(), None);
- return None;
+ return self.emit_event(self.source.len_bytes(), None);
};
let (mut match_, capture_index) = layer.captures.next().unwrap();
let mut capture = match_.captures[capture_index];
- // If this capture represents an injection, then process the injection.
- if match_.pattern_index < layer.config.locals_pattern_index {
- let (language_name, content_node, include_children) =
- injection_for_match(layer.config, &layer.config.query, &match_, self.source);
-
- // Explicitly remove this match so that none of its other captures will remain
- // in the stream of captures.
- match_.remove();
-
- // If a language is found with the given name, then add a new language layer
- // to the highlighted document.
- if let (Some(language_name), Some(content_node)) = (language_name, content_node) {
- if let Some(config) = (self.injection_callback)(&language_name) {
- let ranges = HighlightIterLayer::intersect_ranges(
- &self.layers[0].ranges,
- &[content_node],
- include_children,
- );
- if !ranges.is_empty() {
- match HighlightIterLayer::new(
- self.source,
- self.cancellation_flag,
- &mut self.injection_callback,
- config,
- self.layers[0].depth + 1,
- ranges,
- ) {
- Ok(layers) => {
- for layer in layers {
- self.insert_layer(layer);
- }
- }
- Err(e) => return Some(Err(e)),
- }
- }
- }
- }
-
- self.sort_layers();
- continue 'main;
- }
-
// Remove from the local scope stack any local scopes that have already ended.
while range.start > layer.scope_stack.last().unwrap().range.end {
layer.scope_stack.pop();
@@ -1747,14 +1709,6 @@ fn injection_for_match<'a>(
(language_name, content_node, include_children)
}
-// fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) {
-// if vec.len() > capacity {
-// vec.truncate(capacity);
-// vec.shrink_to_fit();
-// }
-// vec.clear();
-// }
-
pub struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
@@ -1921,6 +1875,8 @@ mod test {
.map(String::from)
.collect();
+ let loader = Loader::new(Configuration { language: vec![] });
+
let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
let config = HighlightConfiguration::new(
language,
@@ -1943,7 +1899,7 @@ mod test {
fn main() {}
",
);
- let syntax = Syntax::new(&source, Arc::new(config));
+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
let tree = syntax.tree();
let root = tree.root_node();
assert_eq!(root.kind(), "source_file");
@@ -1970,7 +1926,7 @@ mod test {
&doc,
vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
);
- let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
+ let edits = generate_edits(&doc, transaction.changes());
// transaction.apply(&mut state);
assert_eq!(
@@ -1999,7 +1955,7 @@ mod test {
let mut doc = Rope::from("fn test() {}");
let transaction =
Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
- let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
+ let edits = generate_edits(&doc, transaction.changes());
transaction.apply(&mut doc);
assert_eq!(doc, "fn test(a: u32) {}");
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index dfc18fbe..2e34a986 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -22,7 +22,7 @@ pub enum Assoc {
}
// ChangeSpec = Change | ChangeSet | Vec<Change>
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ChangeSet {
pub(crate) changes: Vec<Operation>,
/// The required document length. Will refuse to apply changes unless it matches.
@@ -30,16 +30,6 @@ pub struct ChangeSet {
len_after: usize,
}
-impl Default for ChangeSet {
- fn default() -> Self {
- Self {
- changes: Vec::new(),
- len: 0,
- len_after: 0,
- }
- }
-}
-
impl ChangeSet {
pub fn with_capacity(capacity: usize) -> Self {
Self {
@@ -95,7 +85,7 @@ impl ChangeSet {
let new_last = match self.changes.as_mut_slice() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
- prev.push_tendril(&fragment);
+ prev.push_str(&fragment);
return;
}
[.., last @ Delete(_)] => std::mem::replace(last, Insert(fragment)),
@@ -199,7 +189,7 @@ impl ChangeSet {
// TODO: cover this with a test
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(j).unwrap();
- s.pop_front(pos as u32);
+ s.replace_range(0..pos, "");
head_a = Some(Insert(s));
head_b = changes_b.next();
}
@@ -221,9 +211,11 @@ impl ChangeSet {
Ordering::Greater => {
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(j).unwrap();
- let pos = pos as u32;
- changes.insert(s.subtendril(0, pos));
- head_a = Some(Insert(s.subtendril(pos, s.len() as u32 - pos)));
+ let mut before = s;
+ let after = before.split_off(pos);
+
+ changes.insert(before);
+ head_a = Some(Insert(after));
head_b = changes_b.next();
}
}
@@ -287,7 +279,7 @@ impl ChangeSet {
}
Delete(n) => {
let text = Cow::from(original_doc.slice(pos..pos + *n));
- changes.insert(Tendril::from_slice(&text));
+ changes.insert(Tendril::from(text.as_ref()));
pos += n;
}
Insert(s) => {
@@ -330,7 +322,7 @@ impl ChangeSet {
/// `true` when the set is empty.
#[inline]
pub fn is_empty(&self) -> bool {
- self.changes.is_empty()
+ self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
}
/// Map a position through the changes.
@@ -419,7 +411,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,
@@ -720,19 +712,19 @@ mod test {
#[test]
fn optimized_composition() {
let mut state = State::new("".into());
- let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('h'));
+ let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from("h"));
t1.apply(&mut state.doc);
state.selection = state.selection.clone().map(t1.changes());
- let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('e'));
+ let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from("e"));
t2.apply(&mut state.doc);
state.selection = state.selection.clone().map(t2.changes());
- let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
+ let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l"));
t3.apply(&mut state.doc);
state.selection = state.selection.clone().map(t3.changes());
- let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
+ let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l"));
t4.apply(&mut state.doc);
state.selection = state.selection.clone().map(t4.changes());
- let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('o'));
+ let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from("o"));
t5.apply(&mut state.doc);
state.selection = state.selection.clone().map(t5.changes());
@@ -771,7 +763,7 @@ mod test {
#[test]
fn combine_with_utf8() {
- const TEST_CASE: &'static str = "Hello, γ“γ‚Œγ―γƒ˜γƒͺックスエディターです!";
+ const TEST_CASE: &str = "Hello, γ“γ‚Œγ―γƒ˜γƒͺックスエディターです!";
let empty = Rope::from("");
let a = ChangeSet::new(&empty);