aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src
diff options
context:
space:
mode:
authorGokul Soumya2021-12-18 03:03:15 +0000
committerGokul Soumya2021-12-18 03:03:15 +0000
commitd4fb1d06333315a3c6e9dc7f0ad8055d91551e3a (patch)
treef43da59eec8b897359b84390b1c7d5b69f3bb169 /helix-core/src
parent016640f4fb6f620df13a2cab15e749d623197a51 (diff)
parent3ef115d4203fab93f7efe4f65d8dd63cc4535b91 (diff)
Merge branch 'master' into cursor-shape-new
Diffstat (limited to 'helix-core/src')
-rw-r--r--helix-core/src/auto_pairs.rs421
-rw-r--r--helix-core/src/diagnostic.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)30
-rw-r--r--helix-core/src/lib.rs5
-rw-r--r--helix-core/src/register.rs14
-rw-r--r--helix-core/src/selection.rs6
-rw-r--r--helix-core/src/shellwords.rs164
-rw-r--r--helix-core/src/surround.rs148
-rw-r--r--helix-core/src/syntax.rs7
-rw-r--r--helix-core/src/textobject.rs10
-rw-r--r--helix-core/src/transaction.rs16
13 files changed, 1142 insertions, 181 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index cc966852..c037afef 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -2,6 +2,7 @@
//! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction};
+use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +16,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,40 +28,44 @@ 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 (|) -> |
+// * do not reduce to cursors; use whole selections, and surround with pair
+// * 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);
+
+ let cursors = selection.clone().cursors(doc.slice(..));
+
for &(open, close) in PAIRS {
if open == ch {
if open == close {
- return handle_same(doc, selection, open);
+ return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
- return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
+ return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE));
}
}
if close == ch {
// && char_at pos == close
- return Some(handle_close(doc, selection, open, close));
+ return Some(handle_close(doc, &cursors, open, close));
}
}
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)
}
-// 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 +73,362 @@ 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 transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let start_head = start_range.head;
- 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 next = doc.get_char(start_head);
+ let end_head = start_head + offs + open.len_utf8();
+
+ let end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
match next {
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)))
+ offs += open.len_utf8();
+ (start_head, start_head, Some(Tendril::from_char(open)))
}
// 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);
-
- offs += 2;
-
- (pos, pos, Some(pair))
+ let pair = Tendril::from_iter([open, close]);
+ offs += open.len_utf8() + close.len_utf8();
+ (start_head, start_head, Some(pair))
}
}
});
- 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 start_head = start_range.head;
+ let next = doc.get_char(start_head);
+ let end_head = start_head + offs + close.len_utf8();
- 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 end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
if next == Some(close) {
- // return transaction that moves past close
- (pos, pos, None) // no-op
+ // return transaction that moves past close
+ (start_head, start_head, None) // no-op
} else {
offs += close.len_utf8();
+ (start_head, start_head, Some(Tendril::from_char(close)))
+ }
+ });
+
+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
+}
- // TODO: else return (use default handler that inserts close)
- (pos, pos, Some(Tendril::from_char(close)))
+/// 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;
+
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let start_head = start_range.head;
+ let end_head = start_head + offs + token.len_utf8();
+
+ // if selection, retain anchor, if cursor, move over
+ let end_anchor = if start_range.is_empty() {
+ end_head
+ } else {
+ start_range.anchor + offs
+ };
+
+ end_ranges.push(Range::new(end_anchor, end_head));
+
+ let next = doc.get_char(start_head);
+ let prev = prev_char(doc, start_head);
+
+ if next == Some(token) {
+ // return transaction that moves past close
+ (start_head, start_head, None) // no-op
+ } else {
+ let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
+ pair.push_char(token);
+
+ // for equal pairs, don't insert both open and close if either
+ // side has a non-pair char
+ if (next.is_none() || close_before.contains(next.unwrap()))
+ && (prev.is_none() || open_before.contains(prev.unwrap()))
+ {
+ pair.push_char(token);
+ }
+
+ offs += pair.len();
+ (start_head, start_head, Some(pair))
}
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ transaction.with_selection(Selection::new(end_ranges, selection.primary_index()))
}
-// 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;
+
+ 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::new(),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, close| format!("{}{}", open, close),
+ &Selection::single(1, 1),
+ );
+ }
+
+ /// [] ([])
+ /// [] -> 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::point(1), Range::point(4), Range::point(7),),
+ 0,
+ ),
+ );
+ }
+
+ // [TODO] broken until it works with selections
+ /// fo[o] -> append ( -> fo[o(])
+ #[ignore]
+ #[test]
+ fn test_append() {
+ test_hooks_with_pairs(
+ &Rope::from("foo"),
+ &Selection::single(2, 4),
+ PAIRS,
+ |open, close| format!("foo{}{}", open, close),
+ &Selection::single(2, 5),
+ );
+ }
+
+ /// ([]) -> insert ) -> ()[]
+ #[test]
+ fn test_insert_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}", open, close));
+
+ test_hooks(
+ &doc,
+ &Selection::single(2, 1),
+ *close,
+ &doc,
+ &Selection::point(2),
+ );
+ }
+ }
+
+ /// ([]) ()[]
+ /// ([]) -> 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),),
+ smallvec!(Range::point(2), Range::point(5), Range::point(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);
+ }
+ }
+
+ /// ([]) -> insert ( -> (([]))
+ #[test]
+ fn test_insert_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::point(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);
+ }
+ }
+
+ /// ([]) -> insert " -> ("[]")
+ #[test]
+ fn test_insert_nested_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::point(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);
+ }
+ }
+ }
+
+ /// []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::point(1),
+ )
+ }
+
+ // [TODO] broken until it works with selections
+ /// [wor]d -> insert ( -> ([wor]d
+ #[test]
+ #[ignore]
+ fn test_insert_open_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(0, 4),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(1, 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("word");
+ let sel = Selection::single(5, 4);
+ let expected_sel = Selection::point(5);
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ differing_pairs(),
+ |open, close| format!("word{}{}", open, close),
+ &expected_sel,
+ );
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ matching_pairs(),
+ |open, _| format!("word{}", open),
+ &expected_sel,
+ );
+ }
}
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index ad1ba16a..4fcf51c9 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,7 +1,7 @@
//! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Severity {
Error,
Warning,
@@ -17,7 +17,7 @@ pub struct Range {
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,
pub line: usize,
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs
new file mode 100644
index 00000000..e3cfe107
--- /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(DateField {
+ regex: r"\d{4}",
+ unit: DateUnit::Years,
+ max_len: 5,
+ }),
+ "y" => Some(DateField {
+ regex: r"\d\d",
+ unit: DateUnit::Years,
+ max_len: 2,
+ }),
+ "m" => Some(DateField {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Months,
+ max_len: 2,
+ }),
+ "d" => Some(DateField {
+ regex: r"[0-3]\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "-d" => Some(DateField {
+ regex: r"[1-3]?\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "a" => Some(DateField {
+ regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
+ unit: DateUnit::Days,
+ max_len: 3,
+ }),
+ "A" => Some(DateField {
+ regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
+ unit: DateUnit::Days,
+ max_len: 9,
+ }),
+ "b" | "h" => Some(DateField {
+ regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
+ unit: DateUnit::Months,
+ max_len: 3,
+ }),
+ "B" => Some(DateField {
+ regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
+ unit: DateUnit::Months,
+ max_len: 9,
+ }),
+ "H" => Some(DateField {
+ regex: r"[0-2]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "M" => Some(DateField {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Minutes,
+ max_len: 2,
+ }),
+ "S" => Some(DateField {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Seconds,
+ max_len: 2,
+ }),
+ "I" => Some(DateField {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "-I" => Some(DateField {
+ regex: r"1?\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "P" => Some(DateField {
+ regex: r"am|pm",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ "p" => Some(DateField {
+ 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,
+ expected.into()
+ );
+ }
+ }
+
+ #[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..a19b7e75 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,7 +369,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -392,7 +396,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -419,7 +424,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -464,7 +470,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
@@ -491,7 +498,8 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
+ .increment(amount)
+ .1,
expected.into()
);
}
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index de7e95c1..92a59f31 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -5,18 +5,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;
@@ -158,7 +159,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);
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index c5444eb7..b9eb497d 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -15,7 +15,11 @@ impl Register {
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
- Self { name, values }
+ if name == '_' {
+ Self::new(name)
+ } else {
+ Self { name, values }
+ }
}
pub const fn name(&self) -> char {
@@ -27,11 +31,15 @@ impl Register {
}
pub fn write(&mut self, values: Vec<String>) {
- self.values = values;
+ if self.name != '_' {
+ self.values = values;
+ }
}
pub fn push(&mut self, value: String) {
- self.values.push(value);
+ if self.name != '_' {
+ self.values.push(value);
+ }
}
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index b4d1dffa..116a1c7c 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -308,10 +308,10 @@ impl Range {
}
impl From<(usize, usize)> for Range {
- fn from(tuple: (usize, usize)) -> Self {
+ fn from((anchor, head): (usize, usize)) -> Self {
Self {
- anchor: tuple.0,
- head: tuple.1,
+ anchor,
+ head,
horiz: None,
}
}
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 32161b70..b53b0a78 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -1,4 +1,4 @@
-use crate::{search, Selection};
+use crate::{search, Range, Selection};
use ropey::RopeSlice;
pub const PAIRS: &[(char, char)] = &[
@@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
pub fn find_nth_pairs_pos(
text: RopeSlice,
ch: char,
- pos: usize,
+ range: Range,
n: usize,
) -> Option<(usize, usize)> {
- let (open, close) = get_pair(ch);
-
- if text.len_chars() < 2 || pos >= text.len_chars() {
+ if text.len_chars() < 2 || range.to() >= text.len_chars() {
return None;
}
+ let (open, close) = get_pair(ch);
+ let pos = range.cursor(text);
+
if open == close {
if Some(open) == text.get_char(pos) {
- // Special case: cursor is directly on a matching char.
- match pos {
- 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
- _ if (pos + 1) == text.len_chars() => {
- Some((search::find_nth_prev(text, open, pos, n)?, pos))
- }
- // We return no match because there's no way to know which
- // side of the char we should be searching on.
- _ => None,
- }
- } else {
- Some((
- search::find_nth_prev(text, open, pos, n)?,
- search::find_nth_next(text, close, pos, n)?,
- ))
+ // Cursor is directly on match char. We return no match
+ // because there's no way to know which side of the char
+ // we should be searching on.
+ return None;
}
+ Some((
+ search::find_nth_prev(text, open, pos, n)?,
+ search::find_nth_next(text, close, pos, n)?,
+ ))
} else {
Some((
find_nth_open_pair(text, open, close, pos, n)?,
@@ -160,8 +154,8 @@ pub fn get_surround_pos(
) -> Option<Vec<usize>> {
let mut change_pos = Vec::new();
- for range in selection {
- let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
+ for &range in selection {
+ let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return None;
}
@@ -178,67 +172,91 @@ mod test {
use ropey::Rope;
use smallvec::SmallVec;
- #[test]
- fn test_find_nth_pairs_pos() {
- let doc = Rope::from("some (text) here");
+ fn check_find_nth_pair_pos(
+ text: &str,
+ cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
+ ) {
+ let doc = Rope::from(text);
let slice = doc.slice(..);
- // cursor on [t]ext
- assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
- assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
- // cursor on so[m]e
- assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
- // cursor on bracket itself
- assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
+ for (cursor_pos, ch, n, expected_range) in cases {
+ let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
+ assert_eq!(
+ range, expected_range,
+ "Expected {:?}, got {:?}",
+ expected_range, range
+ );
+ }
}
#[test]
- fn test_find_nth_pairs_pos_skip() {
- let doc = Rope::from("(so (many (good) text) here)");
- let slice = doc.slice(..);
+ fn test_find_nth_pairs_pos() {
+ check_find_nth_pair_pos(
+ "some (text) here",
+ vec![
+ // cursor on [t]ext
+ (6, '(', 1, Some((5, 10))),
+ (6, ')', 1, Some((5, 10))),
+ // cursor on so[m]e
+ (2, '(', 1, None),
+ // cursor on bracket itself
+ (5, '(', 1, Some((5, 10))),
+ (10, '(', 1, Some((5, 10))),
+ ],
+ );
+ }
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
+ #[test]
+ fn test_find_nth_pairs_pos_skip() {
+ check_find_nth_pair_pos(
+ "(so (many (good) text) here)",
+ vec![
+ // cursor on go[o]d
+ (13, '(', 1, Some((10, 15))),
+ (13, '(', 2, Some((4, 21))),
+ (13, '(', 3, Some((0, 27))),
+ ],
+ );
}
#[test]
fn test_find_nth_pairs_pos_same() {
- let doc = Rope::from("'so 'many 'good' text' here'");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
- // cursor on the quotes
- assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
- // this is the best we can do since opening and closing pairs are same
- assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
+ check_find_nth_pair_pos(
+ "'so 'many 'good' text' here'",
+ vec![
+ // cursor on go[o]d
+ (13, '\'', 1, Some((10, 15))),
+ (13, '\'', 2, Some((4, 21))),
+ (13, '\'', 3, Some((0, 27))),
+ // cursor on the quotes
+ (10, '\'', 1, None),
+ ],
+ )
}
#[test]
fn test_find_nth_pairs_pos_step() {
- let doc = Rope::from("((so)((many) good (text))(here))");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
+ check_find_nth_pair_pos(
+ "((so)((many) good (text))(here))",
+ vec![
+ // cursor on go[o]d
+ (15, '(', 1, Some((5, 24))),
+ (15, '(', 2, Some((0, 31))),
+ ],
+ )
}
#[test]
fn test_find_nth_pairs_pos_mixed() {
- let doc = Rope::from("(so [many {good} text] here)");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
+ check_find_nth_pair_pos(
+ "(so [many {good} text] here)",
+ vec![
+ // cursor on go[o]d
+ (13, '{', 1, Some((10, 15))),
+ (13, '[', 1, Some((4, 21))),
+ (13, '(', 1, Some((0, 27))),
+ ],
+ )
}
#[test]
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index 142265a8..ef35fc75 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -50,7 +50,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)]
@@ -310,8 +310,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]));
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 24f063d4..21ceec04 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -114,7 +114,7 @@ pub fn textobject_surround(
ch: char,
count: usize,
) -> Range {
- surround::find_nth_pairs_pos(slice, ch, range.head, count)
+ surround::find_nth_pairs_pos(slice, ch, range, count)
.map(|(anchor, head)| match textobject {
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@@ -170,7 +170,7 @@ mod test {
#[test]
fn test_textobject_word() {
- // (text, [(cursor position, textobject, final range), ...])
+ // (text, [(char position, textobject, final range), ...])
let tests = &[
(
"cursor at beginning of doc",
@@ -269,7 +269,9 @@ mod test {
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range) = case;
- let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
+ // cursor is a single width selection
+ let range = Range::new(pos, pos + 1);
+ let result = textobject_word(slice, range, objtype, 1, false);
assert_eq!(
result,
expected_range.into(),
@@ -283,7 +285,7 @@ mod test {
#[test]
fn test_textobject_surround() {
- // (text, [(cursor position, textobject, final range, count), ...])
+ // (text, [(cursor position, textobject, final range, surround char, count), ...])
let tests = &[
(
"simple (single) surround pairs",
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index dfc18fbe..d8d389f3 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 {
@@ -330,7 +320,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 +409,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>,