From 3e15aead4adc5139417230d15b38112cdc4f7043 Mon Sep 17 00:00:00 2001 From: George Rodrigues Date: Tue, 30 Nov 2021 21:11:25 -0300 Subject: Fix typo on docs (#1201) --- helix-core/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-core/src/lib.rs') diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index de7e95c1..8ef41ef3 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -158,7 +158,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); -- cgit v1.2.3-70-g09d2 From c1f6167e37909517676c30b0a80203739e8492a5 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Mon, 15 Nov 2021 19:51:10 -0700 Subject: Add support for dates for increment/decrement --- Cargo.lock | 1 + helix-core/Cargo.toml | 2 + helix-core/src/date.rs | 217 +++++++++++++++++++++++++++++++++++++++++++++ helix-core/src/lib.rs | 1 + helix-term/src/commands.rs | 28 ++++-- 5 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 helix-core/src/date.rs (limited to 'helix-core/src/lib.rs') diff --git a/Cargo.lock b/Cargo.lock index 5de6e610..47a6c01e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,6 +369,7 @@ name = "helix-core" version = "0.5.0" dependencies = [ "arc-swap", + "chrono", "etcetera", "helix-syntax", "log", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ea695d34..0a2a56d9 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -36,5 +36,7 @@ similar = "2.1" etcetera = "0.3" +chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } + [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/date.rs b/helix-core/src/date.rs new file mode 100644 index 00000000..1332670d --- /dev/null +++ b/helix-core/src/date.rs @@ -0,0 +1,217 @@ +use chrono::{Duration, NaiveDate}; + +use std::borrow::Cow; + +use ropey::RopeSlice; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, Tendril, +}; + +// Only support formats that aren't region specific. +static FORMATS: &[&str] = &["%Y-%m-%d", "%Y/%m/%d"]; + +// We don't want to parse ambiguous dates like 10/11/12 or 7/8/10. +// They must be YYYY-mm-dd or YYYY/mm/dd. +// So 2021-01-05 works, but 2021-1-5 doesn't. +const DATE_LENGTH: usize = 10; + +#[derive(Debug, PartialEq, Eq)] +pub struct DateIncrementor { + pub date: NaiveDate, + pub range: Range, + pub format: &'static str, +} + +impl DateIncrementor { + pub fn from_range(text: RopeSlice, range: Range) -> Option { + // Don't increment if the cursor is one right of the date text. + if text.char(range.from()).is_whitespace() { + return None; + } + + let range = textobject_word(text, range, TextObject::Inside, 1, true); + let text: Cow = text.slice(range.from()..range.to()).into(); + + let first = text.chars().next()?; + let last = text.chars().next_back()?; + + // Allow date strings in quotes. + let (range, text) = if first == last && (first == '"' || first == '\'') { + ( + Range::new(range.from() + 1, range.to() - 1), + Cow::from(&text[1..text.len() - 1]), + ) + } else { + (range, text) + }; + + if text.len() != DATE_LENGTH { + return None; + } + + FORMATS.iter().find_map(|format| { + NaiveDate::parse_from_str(&text, format) + .ok() + .map(|date| DateIncrementor { + date, + range, + format, + }) + }) + } + + pub fn incremented_text(&self, amount: i64) -> Tendril { + let incremented_date = self.date + Duration::days(amount); + incremented_date.format(self.format).to_string().into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_date_dashes() { + let rope = Rope::from_str("2021-11-15"); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_slashes() { + let rope = Rope::from_str("2021/11/15"); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + format: "%Y/%m/%d", + }) + ); + } + + #[test] + fn test_date_surrounded_by_spaces() { + let rope = Rope::from_str(" 2021-11-15 "); + let range = Range::point(10); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(3, 13), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_in_single_quotes() { + let rope = Rope::from_str("date = '2021-11-15'"); + let range = Range::point(10); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(8, 18), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_in_double_quotes() { + let rope = Rope::from_str("date = \"2021-11-15\""); + let range = Range::point(10); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(8, 18), + format: "%Y-%m-%d", + }) + ); + } + + #[test] + fn test_date_cursor_one_right_of_date() { + let rope = Rope::from_str("2021-11-15 "); + let range = Range::point(10); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_date_cursor_one_left_of_number() { + let rope = Rope::from_str(" 2021-11-15"); + let range = Range::point(0); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_invalid_dates() { + 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", + ]; + + for invalid in tests { + let rope = Rope::from_str(invalid); + let range = Range::point(0); + + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + } + + #[test] + fn test_increment_dates() { + let tests = [ + ("1980-12-21", 1, "1980-12-22"), + ("1980-12-21", -1, "1980-12-20"), + ("1980-12-21", 100, "1981-03-31"), + ("1980-12-21", -100, "1980-09-12"), + ("1980-12-21", 1000, "1983-09-17"), + ("1980-12-21", -1000, "1978-03-27"), + ("1980/12/21", 1, "1980/12/22"), + ("1980/12/21", -1, "1980/12/20"), + ("1980/12/21", 100, "1981/03/31"), + ("1980/12/21", -100, "1980/09/12"), + ("1980/12/21", 1000, "1983/09/17"), + ("1980/12/21", -1000, "1978/03/27"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 8ef41ef3..b16a716f 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,6 +1,7 @@ pub mod auto_pairs; pub mod chars; pub mod comment; +pub mod date; pub mod diagnostic; pub mod diff; pub mod graphemes; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 99d1432c..639bbd83 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,5 +1,7 @@ use helix_core::{ - comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, + comment, coords_at_pos, + date::DateIncrementor, + find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, indent, indent::IndentStyle, @@ -5802,13 +5804,23 @@ fn increment_impl(cx: &mut Context, amount: i64) { let text = doc.text(); let changes = selection.ranges().iter().filter_map(|range| { - let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?; - let new_text = incrementor.incremented_text(amount); - Some(( - incrementor.range.from(), - incrementor.range.to(), - Some(new_text), - )) + if let Some(incrementor) = DateIncrementor::from_range(text.slice(..), *range) { + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) + } else if let Some(incrementor) = NumberIncrementor::from_range(text.slice(..), *range) { + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) + } else { + None + } }); if changes.clone().count() > 0 { -- cgit v1.2.3-70-g09d2 From c9641fccedc51737a74ed47009279fa688462ea9 Mon Sep 17 00:00:00 2001 From: Jason Rodney Hansen Date: Sun, 21 Nov 2021 10:38:41 -0700 Subject: Add `Increment` trait --- helix-core/src/date.rs | 465 ---------------------------------- helix-core/src/increment/date.rs | 474 ++++++++++++++++++++++++++++++++++ helix-core/src/increment/mod.rs | 8 + helix-core/src/increment/number.rs | 507 +++++++++++++++++++++++++++++++++++++ helix-core/src/lib.rs | 3 +- helix-core/src/numbers.rs | 499 ------------------------------------ helix-term/src/commands.rs | 30 +-- 7 files changed, 1002 insertions(+), 984 deletions(-) delete mode 100644 helix-core/src/date.rs create mode 100644 helix-core/src/increment/date.rs create mode 100644 helix-core/src/increment/mod.rs create mode 100644 helix-core/src/increment/number.rs delete mode 100644 helix-core/src/numbers.rs (limited to 'helix-core/src/lib.rs') diff --git a/helix-core/src/date.rs b/helix-core/src/date.rs deleted file mode 100644 index c447ef70..00000000 --- a/helix-core/src/date.rs +++ /dev/null @@ -1,465 +0,0 @@ -use regex::Regex; - -use std::borrow::Cow; -use std::cmp; - -use ropey::RopeSlice; - -use crate::{Range, Tendril}; - -use chrono::{Datelike, Duration, NaiveDate}; - -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_days(date: NaiveDate, amount: i64) -> Option { - date.checked_add_signed(Duration::days(amount)) -} - -fn add_months(date: NaiveDate, amount: i64) -> Option { - let month = date.month0() as i64 + amount; - let year = date.year() + i32::try_from(month / 12).ok()?; - - // Normalize month - let month = month % 12; - let month = if month.is_negative() { - month + 13 - } else { - month + 1 - } as u32; - - let day = cmp::min(date.day(), ndays_in_month(year, month)); - - Some(NaiveDate::from_ymd(year, month, day)) -} - -fn add_years(date: NaiveDate, amount: i64) -> Option { - let year = i32::try_from(date.year() as i64 + amount).ok()?; - - let ndays = ndays_in_month(year, date.month()); - - if date.day() > ndays { - let d = NaiveDate::from_ymd(year, date.month(), ndays); - Some(d.succ()) - } else { - date.with_year(year) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -struct Format { - regex: &'static str, - separator: char, -} - -// Only support formats that aren't region specific. -static FORMATS: &[Format] = &[ - Format { - regex: r"(\d{4})-(\d{2})-(\d{2})", - separator: '-', - }, - Format { - regex: r"(\d{4})/(\d{2})/(\d{2})", - separator: '/', - }, -]; - -const DATE_LENGTH: usize = 10; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum DateField { - Year, - Month, - Day, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct DateIncrementor { - pub date: NaiveDate, - pub range: Range, - - field: DateField, - format: Format, -} - -impl DateIncrementor { - pub fn from_range(text: RopeSlice, range: Range) -> Option { - let range = if range.is_empty() { - if range.anchor < text.len_bytes() { - // 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 - }; - - let from = range.from().saturating_sub(DATE_LENGTH); - let to = (range.from() + DATE_LENGTH).min(text.len_chars()); - - let (from_in_text, to_in_text) = (range.from() - from, range.to() - from); - let text: Cow = text.slice(from..to).into(); - - FORMATS.iter().find_map(|&format| { - let re = Regex::new(format.regex).ok()?; - let captures = re.captures(&text)?; - - let date = captures.get(0)?; - let offset = range.from() - from_in_text; - let range = Range::new(date.start() + offset, date.end() + offset); - - let (year, month, day) = (captures.get(1)?, captures.get(2)?, captures.get(3)?); - let (year_range, month_range, day_range) = (year.range(), month.range(), day.range()); - - let field = if year_range.contains(&from_in_text) - && year_range.contains(&(to_in_text - 1)) - { - DateField::Year - } else if month_range.contains(&from_in_text) && month_range.contains(&(to_in_text - 1)) - { - DateField::Month - } else if day_range.contains(&from_in_text) && day_range.contains(&(to_in_text - 1)) { - DateField::Day - } else { - return None; - }; - - let date = NaiveDate::from_ymd_opt( - year.as_str().parse::().ok()?, - month.as_str().parse::().ok()?, - day.as_str().parse::().ok()?, - )?; - - Some(DateIncrementor { - date, - field, - range, - format, - }) - }) - } - - pub fn incremented_text(&self, amount: i64) -> Tendril { - let date = match self.field { - DateField::Year => add_years(self.date, amount), - DateField::Month => add_months(self.date, amount), - DateField::Day => add_days(self.date, amount), - } - .unwrap_or(self.date); - - format!( - "{:04}{}{:02}{}{:02}", - date.year(), - self.format.separator, - date.month(), - self.format.separator, - date.day() - ) - .into() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::Rope; - - #[test] - fn test_create_incrementor_for_year_with_dashes() { - let rope = Rope::from_str("2021-11-15"); - - for cursor in 0..=3 { - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Year, - format: FORMATS[0], - }) - ); - } - } - - #[test] - fn test_create_incrementor_for_month_with_dashes() { - let rope = Rope::from_str("2021-11-15"); - - for cursor in 5..=6 { - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Month, - format: FORMATS[0], - }) - ); - } - } - - #[test] - fn test_create_incrementor_for_day_with_dashes() { - let rope = Rope::from_str("2021-11-15"); - - for cursor in 8..=9 { - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Day, - format: FORMATS[0], - }) - ); - } - } - - #[test] - fn test_try_create_incrementor_on_dashes() { - let rope = Rope::from_str("2021-11-15"); - - for &cursor in &[4, 7] { - let range = Range::new(cursor, cursor + 1); - assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None,); - } - } - - #[test] - fn test_create_incrementor_for_year_with_slashes() { - let rope = Rope::from_str("2021/11/15"); - - for cursor in 0..=3 { - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Year, - format: FORMATS[1], - }) - ); - } - } - - #[test] - fn test_create_incrementor_for_month_with_slashes() { - let rope = Rope::from_str("2021/11/15"); - - for cursor in 5..=6 { - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Month, - format: FORMATS[1], - }) - ); - } - } - - #[test] - fn test_create_incrementor_for_day_with_slashes() { - let rope = Rope::from_str("2021/11/15"); - - for cursor in 8..=9 { - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Day, - format: FORMATS[1], - }) - ); - } - } - - #[test] - fn test_try_create_incrementor_on_slashes() { - let rope = Rope::from_str("2021/11/15"); - - for &cursor in &[4, 7] { - let range = Range::new(cursor, cursor + 1); - assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None,); - } - } - - #[test] - fn test_date_surrounded_by_spaces() { - let rope = Rope::from_str(" 2021-11-15 "); - let range = Range::new(3, 4); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(3, 13), - field: DateField::Year, - format: FORMATS[0], - }) - ); - } - - #[test] - fn test_date_in_single_quotes() { - let rope = Rope::from_str("date = '2021-11-15'"); - let range = Range::new(10, 11); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(8, 18), - field: DateField::Year, - format: FORMATS[0], - }) - ); - } - - #[test] - fn test_date_in_double_quotes() { - let rope = Rope::from_str("let date = \"2021-11-15\";"); - let range = Range::new(12, 13); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(12, 22), - field: DateField::Year, - format: FORMATS[0], - }) - ); - } - - #[test] - fn test_date_cursor_one_right_of_date() { - let rope = Rope::from_str("2021-11-15 "); - let range = Range::new(10, 11); - assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_date_cursor_one_left_of_number() { - let rope = Rope::from_str(" 2021-11-15"); - let range = Range::new(0, 1); - assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_date_empty_range_at_beginning() { - let rope = Rope::from_str("2021-11-15"); - let range = Range::point(0); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Year, - format: FORMATS[0], - }) - ); - } - - #[test] - fn test_date_empty_range_at_in_middle() { - let rope = Rope::from_str("2021-11-15"); - let range = Range::point(5); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range), - Some(DateIncrementor { - date: NaiveDate::from_ymd(2021, 11, 15), - range: Range::new(0, 10), - field: DateField::Month, - format: FORMATS[0], - }) - ); - } - - #[test] - fn test_date_empty_range_at_end() { - let rope = Rope::from_str("2021-11-15"); - let range = Range::point(10); - assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_invalid_dates() { - 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", - ]; - - for invalid in tests { - let rope = Rope::from_str(invalid); - let range = Range::new(0, 1); - - assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); - } - } - - #[test] - fn test_increment_dates() { - 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"), - ("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"), - ]; - - for (original, cursor, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateIncrementor::from_range(rope.slice(..), range) - .unwrap() - .incremented_text(amount), - expected.into() - ); - } - } -} diff --git a/helix-core/src/increment/date.rs b/helix-core/src/increment/date.rs new file mode 100644 index 00000000..05442990 --- /dev/null +++ b/helix-core/src/increment/date.rs @@ -0,0 +1,474 @@ +use regex::Regex; + +use std::borrow::Cow; +use std::cmp; + +use ropey::RopeSlice; + +use crate::{Range, Tendril}; + +use chrono::{Datelike, Duration, NaiveDate}; + +use super::Increment; + +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_days(date: NaiveDate, amount: i64) -> Option { + date.checked_add_signed(Duration::days(amount)) +} + +fn add_months(date: NaiveDate, amount: i64) -> Option { + let month = date.month0() as i64 + amount; + let year = date.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 + 13 + } else { + month + 1 + } as u32; + + let day = cmp::min(date.day(), ndays_in_month(year, month)); + + Some(NaiveDate::from_ymd(year, month, day)) +} + +fn add_years(date: NaiveDate, amount: i64) -> Option { + let year = i32::try_from(date.year() as i64 + amount).ok()?; + let ndays = ndays_in_month(year, date.month()); + + if date.day() > ndays { + let d = NaiveDate::from_ymd(year, date.month(), ndays); + Some(d.succ()) + } else { + date.with_year(year) + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Format { + regex: &'static str, + separator: char, +} + +// Only support formats that aren't region specific. +static FORMATS: &[Format] = &[ + Format { + regex: r"(\d{4})-(\d{2})-(\d{2})", + separator: '-', + }, + Format { + regex: r"(\d{4})/(\d{2})/(\d{2})", + separator: '/', + }, +]; + +const DATE_LENGTH: usize = 10; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum DateField { + Year, + Month, + Day, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DateIncrementor { + date: NaiveDate, + range: Range, + field: DateField, + format: Format, +} + +impl DateIncrementor { + pub fn from_range(text: RopeSlice, range: Range) -> Option { + let range = if range.is_empty() { + if range.anchor < text.len_bytes() { + // 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 + }; + + let from = range.from().saturating_sub(DATE_LENGTH); + let to = (range.from() + DATE_LENGTH).min(text.len_chars()); + + let (from_in_text, to_in_text) = (range.from() - from, range.to() - from); + let text: Cow = text.slice(from..to).into(); + + FORMATS.iter().find_map(|&format| { + let re = Regex::new(format.regex).ok()?; + let captures = re.captures(&text)?; + + let date = captures.get(0)?; + let offset = range.from() - from_in_text; + let range = Range::new(date.start() + offset, date.end() + offset); + + let (year, month, day) = (captures.get(1)?, captures.get(2)?, captures.get(3)?); + let (year_range, month_range, day_range) = (year.range(), month.range(), day.range()); + + let field = if year_range.contains(&from_in_text) + && year_range.contains(&(to_in_text - 1)) + { + DateField::Year + } else if month_range.contains(&from_in_text) && month_range.contains(&(to_in_text - 1)) + { + DateField::Month + } else if day_range.contains(&from_in_text) && day_range.contains(&(to_in_text - 1)) { + DateField::Day + } else { + return None; + }; + + let date = NaiveDate::from_ymd_opt( + year.as_str().parse::().ok()?, + month.as_str().parse::().ok()?, + day.as_str().parse::().ok()?, + )?; + + Some(DateIncrementor { + date, + field, + range, + format, + }) + }) + } +} + +impl Increment for DateIncrementor { + fn increment(&self, amount: i64) -> (Range, Tendril) { + let date = match self.field { + DateField::Year => add_years(self.date, amount), + DateField::Month => add_months(self.date, amount), + DateField::Day => add_days(self.date, amount), + } + .unwrap_or(self.date); + + ( + self.range, + format!( + "{:04}{}{:02}{}{:02}", + date.year(), + self.format.separator, + date.month(), + self.format.separator, + date.day() + ) + .into(), + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_create_incrementor_for_year_with_dashes() { + let rope = Rope::from_str("2021-11-15"); + + for cursor in 0..=3 { + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Year, + format: FORMATS[0], + }) + ); + } + } + + #[test] + fn test_create_incrementor_for_month_with_dashes() { + let rope = Rope::from_str("2021-11-15"); + + for cursor in 5..=6 { + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Month, + format: FORMATS[0], + }) + ); + } + } + + #[test] + fn test_create_incrementor_for_day_with_dashes() { + let rope = Rope::from_str("2021-11-15"); + + for cursor in 8..=9 { + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Day, + format: FORMATS[0], + }) + ); + } + } + + #[test] + fn test_try_create_incrementor_on_dashes() { + let rope = Rope::from_str("2021-11-15"); + + for &cursor in &[4, 7] { + let range = Range::new(cursor, cursor + 1); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None,); + } + } + + #[test] + fn test_create_incrementor_for_year_with_slashes() { + let rope = Rope::from_str("2021/11/15"); + + for cursor in 0..=3 { + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Year, + format: FORMATS[1], + }) + ); + } + } + + #[test] + fn test_create_incrementor_for_month_with_slashes() { + let rope = Rope::from_str("2021/11/15"); + + for cursor in 5..=6 { + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Month, + format: FORMATS[1], + }) + ); + } + } + + #[test] + fn test_create_incrementor_for_day_with_slashes() { + let rope = Rope::from_str("2021/11/15"); + + for cursor in 8..=9 { + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Day, + format: FORMATS[1], + }) + ); + } + } + + #[test] + fn test_try_create_incrementor_on_slashes() { + let rope = Rope::from_str("2021/11/15"); + + for &cursor in &[4, 7] { + let range = Range::new(cursor, cursor + 1); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None,); + } + } + + #[test] + fn test_date_surrounded_by_spaces() { + let rope = Rope::from_str(" 2021-11-15 "); + let range = Range::new(3, 4); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(3, 13), + field: DateField::Year, + format: FORMATS[0], + }) + ); + } + + #[test] + fn test_date_in_single_quotes() { + let rope = Rope::from_str("date = '2021-11-15'"); + let range = Range::new(10, 11); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(8, 18), + field: DateField::Year, + format: FORMATS[0], + }) + ); + } + + #[test] + fn test_date_in_double_quotes() { + let rope = Rope::from_str("let date = \"2021-11-15\";"); + let range = Range::new(12, 13); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(12, 22), + field: DateField::Year, + format: FORMATS[0], + }) + ); + } + + #[test] + fn test_date_cursor_one_right_of_date() { + let rope = Rope::from_str("2021-11-15 "); + let range = Range::new(10, 11); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_date_cursor_one_left_of_number() { + let rope = Rope::from_str(" 2021-11-15"); + let range = Range::new(0, 1); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_date_empty_range_at_beginning() { + let rope = Rope::from_str("2021-11-15"); + let range = Range::point(0); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Year, + format: FORMATS[0], + }) + ); + } + + #[test] + fn test_date_empty_range_at_in_middle() { + let rope = Rope::from_str("2021-11-15"); + let range = Range::point(5); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range), + Some(DateIncrementor { + date: NaiveDate::from_ymd(2021, 11, 15), + range: Range::new(0, 10), + field: DateField::Month, + format: FORMATS[0], + }) + ); + } + + #[test] + fn test_date_empty_range_at_end() { + let rope = Rope::from_str("2021-11-15"); + let range = Range::point(10); + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_invalid_dates() { + 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", + ]; + + for invalid in tests { + let rope = Rope::from_str(invalid); + let range = Range::new(0, 1); + + assert_eq!(DateIncrementor::from_range(rope.slice(..), range), None); + } + } + + #[test] + fn test_increment_dates() { + 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"), + ]; + + for (original, cursor, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::new(cursor, cursor + 1); + assert_eq!( + DateIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } +} diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs new file mode 100644 index 00000000..71a1f183 --- /dev/null +++ b/helix-core/src/increment/mod.rs @@ -0,0 +1,8 @@ +pub mod date; +pub mod number; + +use crate::{Range, Tendril}; + +pub trait Increment { + fn increment(&self, amount: i64) -> (Range, Tendril); +} diff --git a/helix-core/src/increment/number.rs b/helix-core/src/increment/number.rs new file mode 100644 index 00000000..a19b7e75 --- /dev/null +++ b/helix-core/src/increment/number.rs @@ -0,0 +1,507 @@ +use std::borrow::Cow; + +use ropey::RopeSlice; + +use super::Increment; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, Tendril, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct NumberIncrementor<'a> { + value: i64, + radix: u32, + range: Range, + + text: RopeSlice<'a>, +} + +impl<'a> NumberIncrementor<'a> { + /// Return information about number under rang if there is one. + pub fn from_range(text: RopeSlice, range: Range) -> Option { + // If the cursor is on the minus sign of a number we want to get the word textobject to the + // right of it. + let range = if range.to() < text.len_chars() + && range.to() - range.from() <= 1 + && text.char(range.from()) == '-' + { + Range::new(range.from() + 1, range.to() + 1) + } else { + range + }; + + let range = textobject_word(text, range, TextObject::Inside, 1, false); + + // If there is a minus sign to the left of the word object, we want to include it in the range. + let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { + range.extend(range.from() - 1, range.from()) + } else { + range + }; + + let word: String = text + .slice(range.from()..range.to()) + .chars() + .filter(|&c| c != '_') + .collect(); + let (radix, prefixed) = if word.starts_with("0x") { + (16, true) + } else if word.starts_with("0o") { + (8, true) + } else if word.starts_with("0b") { + (2, true) + } else { + (10, false) + }; + + let number = if prefixed { &word[2..] } else { &word }; + + let value = i128::from_str_radix(number, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + + let value = value as i64; + Some(NumberIncrementor { + range, + value, + radix, + text, + }) + } +} + +impl<'a> Increment for NumberIncrementor<'a> { + fn increment(&self, amount: i64) -> (Range, Tendril) { + let old_text: Cow = self.text.slice(self.range.from()..self.range.to()).into(); + let old_length = old_text.len(); + let new_value = self.value.wrapping_add(amount); + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec = old_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) + .collect(); + + let format_length = if self.radix == 10 { + match (self.value.is_negative(), new_value.is_negative()) { + (true, false) => old_length - 1, + (false, true) => old_length + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + } - separator_rtl_indexes.len(); + + let mut new_text = match self.radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { + format!("{:01$}", new_value, format_length) + } + 10 => format!("{}", new_value), + 16 => { + let (lower_count, upper_count): (usize, usize) = + old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { + ( + lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), + upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), + ) + }); + if upper_count > lower_count { + format!("0x{:01$X}", new_value, format_length) + } else { + format!("0x{:01$x}", new_value, format_length) + } + } + _ => unimplemented!("radix not supported: {}", self.radix), + }; + + // Add separators from original number. + for &rtl_index in &separator_rtl_indexes { + if rtl_index < new_text.len() { + let new_index = new_text.len() - rtl_index; + new_text.insert(new_index, '_'); + } + } + + // Add in additional separators if necessary. + if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { + let spacing = match separator_rtl_indexes.as_slice() { + [.., b, a] => a - b - 1, + _ => separator_rtl_indexes[0], + }; + + let prefix_length = if self.radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find('_') { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, '_'); + } + } + } + + (self.range, new_text.into()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_decimal_at_point() { + let rope = Rope::from_str("Test text 12345 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 15), + value: 12345, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_uppercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0x123ABCDEF more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 21), + value: 0x123ABCDEF, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_lowercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0xfa3b4e more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 18), + value: 0xfa3b4e, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_octal_at_point() { + let rope = Rope::from_str("Test text 0o1074312 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 0o1074312, + radix: 8, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_binary_at_point() { + let rope = Rope::from_str("Test text 0b10111010010101 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 26), + value: 0b10111010010101, + radix: 2, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_at_point() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_decimal_with_leading_zeroes_at_point() { + let rope = Rope::from_str("Test text 000045326 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 45326, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_cursor_on_minus_sign() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(10); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_start_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_end_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(2); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_surrounded_by_punctuation() { + let rope = Rope::from_str(",100;"); + let range = Range::point(1); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(1, 4), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_not_a_number_point() { + let rope = Rope::from_str("Test text 45326 more text."); + let range = Range::point(6); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_too_large_at_point() { + let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); + let range = Range::point(12); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_right_of_number() { + let rope = Rope::from_str("100 "); + let range = Range::point(3); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_left_of_number() { + let rope = Rope::from_str(" 100"); + let range = Range::point(0); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_increment_basic_decimal_numbers() { + let tests = [ + ("100", 1, "101"), + ("100", -1, "99"), + ("99", 1, "100"), + ("100", 1000, "1100"), + ("100", -1000, "-900"), + ("-1", 1, "0"), + ("-1", 2, "1"), + ("1", -1, "0"), + ("1", -2, "-1"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_hexadedimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0xffffffffffffffff"), + ("0xffffffffffffffff", 1, "0x0000000000000000"), + ("0xffffffffffffffff", 2, "0x0000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_octal_numbers() { + let tests = [ + ("0o0107", 1, "0o0110"), + ("0o0110", -1, "0o0107"), + ("0o0001", -1, "0o0000"), + ("0o7777", 1, "0o10000"), + ("0o1000", -1, "0o0777"), + ("0o0107", 10, "0o0121"), + ("0o0000", -1, "0o1777777777777777777777"), + ("0o1777777777777777777777", 1, "0o0000000000000000000000"), + ("0o1777777777777777777777", 2, "0o0000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_binary_numbers() { + let tests = [ + ("0b00000100", 1, "0b00000101"), + ("0b00000100", -1, "0b00000011"), + ("0b00000100", 2, "0b00000110"), + ("0b00000100", -2, "0b00000010"), + ("0b00000001", -1, "0b00000000"), + ("0b00111111", 10, "0b01001001"), + ("0b11111111", 1, "0b100000000"), + ("0b10000000", -1, "0b01111111"), + ( + "0b0000", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111111", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b0000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } + + #[test] + fn test_increment_with_separators() { + let tests = [ + ("999_999", 1, "1_000_000"), + ("1_000_000", -1, "999_999"), + ("-999_999", -1, "-1_000_000"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .increment(amount) + .1, + expected.into() + ); + } + } +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index b16a716f..4ae044cc 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -1,17 +1,16 @@ pub mod auto_pairs; pub mod chars; pub mod comment; -pub mod date; 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; diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs deleted file mode 100644 index e9f3c898..00000000 --- a/helix-core/src/numbers.rs +++ /dev/null @@ -1,499 +0,0 @@ -use std::borrow::Cow; - -use ropey::RopeSlice; - -use crate::{ - textobject::{textobject_word, TextObject}, - Range, Tendril, -}; - -#[derive(Debug, PartialEq, Eq)] -pub struct NumberIncrementor<'a> { - pub range: Range, - pub value: i64, - pub radix: u32, - - text: RopeSlice<'a>, -} - -impl<'a> NumberIncrementor<'a> { - /// Return information about number under rang if there is one. - pub fn from_range(text: RopeSlice, range: Range) -> Option { - // If the cursor is on the minus sign of a number we want to get the word textobject to the - // right of it. - let range = if range.to() < text.len_chars() - && range.to() - range.from() <= 1 - && text.char(range.from()) == '-' - { - Range::new(range.from() + 1, range.to() + 1) - } else { - range - }; - - let range = textobject_word(text, range, TextObject::Inside, 1, false); - - // If there is a minus sign to the left of the word object, we want to include it in the range. - let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { - range.extend(range.from() - 1, range.from()) - } else { - range - }; - - let word: String = text - .slice(range.from()..range.to()) - .chars() - .filter(|&c| c != '_') - .collect(); - let (radix, prefixed) = if word.starts_with("0x") { - (16, true) - } else if word.starts_with("0o") { - (8, true) - } else if word.starts_with("0b") { - (2, true) - } else { - (10, false) - }; - - let number = if prefixed { &word[2..] } else { &word }; - - let value = i128::from_str_radix(number, radix).ok()?; - if (value.is_positive() && value.leading_zeros() < 64) - || (value.is_negative() && value.leading_ones() < 64) - { - return None; - } - - let value = value as i64; - Some(NumberIncrementor { - range, - value, - radix, - text, - }) - } - - /// Add `amount` to the number and return the formatted text. - pub fn incremented_text(&self, amount: i64) -> Tendril { - let old_text: Cow = self.text.slice(self.range.from()..self.range.to()).into(); - let old_length = old_text.len(); - let new_value = self.value.wrapping_add(amount); - - // Get separator indexes from right to left. - let separator_rtl_indexes: Vec = old_text - .chars() - .rev() - .enumerate() - .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) - .collect(); - - let format_length = if self.radix == 10 { - match (self.value.is_negative(), new_value.is_negative()) { - (true, false) => old_length - 1, - (false, true) => old_length + 1, - _ => old_text.len(), - } - } else { - old_text.len() - 2 - } - separator_rtl_indexes.len(); - - let mut new_text = match self.radix { - 2 => format!("0b{:01$b}", new_value, format_length), - 8 => format!("0o{:01$o}", new_value, format_length), - 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { - format!("{:01$}", new_value, format_length) - } - 10 => format!("{}", new_value), - 16 => { - let (lower_count, upper_count): (usize, usize) = - old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { - ( - lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), - upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), - ) - }); - if upper_count > lower_count { - format!("0x{:01$X}", new_value, format_length) - } else { - format!("0x{:01$x}", new_value, format_length) - } - } - _ => unimplemented!("radix not supported: {}", self.radix), - }; - - // Add separators from original number. - for &rtl_index in &separator_rtl_indexes { - if rtl_index < new_text.len() { - let new_index = new_text.len() - rtl_index; - new_text.insert(new_index, '_'); - } - } - - // Add in additional separators if necessary. - if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { - let spacing = match separator_rtl_indexes.as_slice() { - [.., b, a] => a - b - 1, - _ => separator_rtl_indexes[0], - }; - - let prefix_length = if self.radix == 10 { 0 } else { 2 }; - if let Some(mut index) = new_text.find('_') { - while index - prefix_length > spacing { - index -= spacing; - new_text.insert(index, '_'); - } - } - } - - new_text.into() - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::Rope; - - #[test] - fn test_decimal_at_point() { - let rope = Rope::from_str("Test text 12345 more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 15), - value: 12345, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_uppercase_hexadecimal_at_point() { - let rope = Rope::from_str("Test text 0x123ABCDEF more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 21), - value: 0x123ABCDEF, - radix: 16, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_lowercase_hexadecimal_at_point() { - let rope = Rope::from_str("Test text 0xfa3b4e more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 18), - value: 0xfa3b4e, - radix: 16, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_octal_at_point() { - let rope = Rope::from_str("Test text 0o1074312 more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 19), - value: 0o1074312, - radix: 8, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_binary_at_point() { - let rope = Rope::from_str("Test text 0b10111010010101 more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 26), - value: 0b10111010010101, - radix: 2, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_negative_decimal_at_point() { - let rope = Rope::from_str("Test text -54321 more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 16), - value: -54321, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_decimal_with_leading_zeroes_at_point() { - let rope = Rope::from_str("Test text 000045326 more text."); - let range = Range::point(12); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 19), - value: 45326, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_negative_decimal_cursor_on_minus_sign() { - let rope = Rope::from_str("Test text -54321 more text."); - let range = Range::point(10); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(10, 16), - value: -54321, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_number_under_range_start_of_rope() { - let rope = Rope::from_str("100"); - let range = Range::point(0); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(0, 3), - value: 100, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_number_under_range_end_of_rope() { - let rope = Rope::from_str("100"); - let range = Range::point(2); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(0, 3), - value: 100, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_number_surrounded_by_punctuation() { - let rope = Rope::from_str(",100;"); - let range = Range::point(1); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range), - Some(NumberIncrementor { - range: Range::new(1, 4), - value: 100, - radix: 10, - text: rope.slice(..), - }) - ); - } - - #[test] - fn test_not_a_number_point() { - let rope = Rope::from_str("Test text 45326 more text."); - let range = Range::point(6); - assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_number_too_large_at_point() { - let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); - let range = Range::point(12); - assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_number_cursor_one_right_of_number() { - let rope = Rope::from_str("100 "); - let range = Range::point(3); - assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_number_cursor_one_left_of_number() { - let rope = Rope::from_str(" 100"); - let range = Range::point(0); - assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); - } - - #[test] - fn test_increment_basic_decimal_numbers() { - let tests = [ - ("100", 1, "101"), - ("100", -1, "99"), - ("99", 1, "100"), - ("100", 1000, "1100"), - ("100", -1000, "-900"), - ("-1", 1, "0"), - ("-1", 2, "1"), - ("1", -1, "0"), - ("1", -2, "-1"), - ]; - - for (original, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::point(0); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range) - .unwrap() - .incremented_text(amount), - expected.into() - ); - } - } - - #[test] - fn test_increment_basic_hexadedimal_numbers() { - let tests = [ - ("0x0100", 1, "0x0101"), - ("0x0100", -1, "0x00ff"), - ("0x0001", -1, "0x0000"), - ("0x0000", -1, "0xffffffffffffffff"), - ("0xffffffffffffffff", 1, "0x0000000000000000"), - ("0xffffffffffffffff", 2, "0x0000000000000001"), - ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), - ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), - ("0xabcdef1234567890", 1, "0xabcdef1234567891"), - ]; - - for (original, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::point(0); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range) - .unwrap() - .incremented_text(amount), - expected.into() - ); - } - } - - #[test] - fn test_increment_basic_octal_numbers() { - let tests = [ - ("0o0107", 1, "0o0110"), - ("0o0110", -1, "0o0107"), - ("0o0001", -1, "0o0000"), - ("0o7777", 1, "0o10000"), - ("0o1000", -1, "0o0777"), - ("0o0107", 10, "0o0121"), - ("0o0000", -1, "0o1777777777777777777777"), - ("0o1777777777777777777777", 1, "0o0000000000000000000000"), - ("0o1777777777777777777777", 2, "0o0000000000000000000001"), - ("0o1777777777777777777777", -1, "0o1777777777777777777776"), - ]; - - for (original, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::point(0); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range) - .unwrap() - .incremented_text(amount), - expected.into() - ); - } - } - - #[test] - fn test_increment_basic_binary_numbers() { - let tests = [ - ("0b00000100", 1, "0b00000101"), - ("0b00000100", -1, "0b00000011"), - ("0b00000100", 2, "0b00000110"), - ("0b00000100", -2, "0b00000010"), - ("0b00000001", -1, "0b00000000"), - ("0b00111111", 10, "0b01001001"), - ("0b11111111", 1, "0b100000000"), - ("0b10000000", -1, "0b01111111"), - ( - "0b0000", - -1, - "0b1111111111111111111111111111111111111111111111111111111111111111", - ), - ( - "0b1111111111111111111111111111111111111111111111111111111111111111", - 1, - "0b0000000000000000000000000000000000000000000000000000000000000000", - ), - ( - "0b1111111111111111111111111111111111111111111111111111111111111111", - 2, - "0b0000000000000000000000000000000000000000000000000000000000000001", - ), - ( - "0b1111111111111111111111111111111111111111111111111111111111111111", - -1, - "0b1111111111111111111111111111111111111111111111111111111111111110", - ), - ]; - - for (original, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::point(0); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range) - .unwrap() - .incremented_text(amount), - expected.into() - ); - } - } - - #[test] - fn test_increment_with_separators() { - let tests = [ - ("999_999", 1, "1_000_000"), - ("1_000_000", -1, "999_999"), - ("-999_999", -1, "-1_000_000"), - ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), - ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), - ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), - ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"), - ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"), - ("0b01111111_11111111", 1, "0b10000000_00000000"), - ("0b11111111_11111111", 1, "0b1_00000000_00000000"), - ]; - - for (original, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::point(0); - assert_eq!( - NumberIncrementor::from_range(rope.slice(..), range) - .unwrap() - .incremented_text(amount), - expected.into() - ); - } - } -} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 639bbd83..6329dec7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,14 +1,13 @@ use helix_core::{ - comment, coords_at_pos, - date::DateIncrementor, - find_first_non_whitespace_char, find_root, graphemes, + comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, + increment::date::DateIncrementor, + increment::{number::NumberIncrementor, Increment}, indent, indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, - numbers::NumberIncrementor, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, search, selection, surround, textobject, @@ -5804,23 +5803,18 @@ fn increment_impl(cx: &mut Context, amount: i64) { let text = doc.text(); let changes = selection.ranges().iter().filter_map(|range| { - if let Some(incrementor) = DateIncrementor::from_range(text.slice(..), *range) { - let new_text = incrementor.incremented_text(amount); - Some(( - incrementor.range.from(), - incrementor.range.to(), - Some(new_text), - )) + let incrementor: Option> = if let Some(incrementor) = + DateIncrementor::from_range(text.slice(..), *range) + { + Some(Box::new(incrementor)) } else if let Some(incrementor) = NumberIncrementor::from_range(text.slice(..), *range) { - let new_text = incrementor.incremented_text(amount); - Some(( - incrementor.range.from(), - incrementor.range.to(), - Some(new_text), - )) + Some(Box::new(incrementor)) } else { None - } + }; + + let (range, new_text) = incrementor?.increment(amount); + Some((range.from(), range.to(), Some(new_text))) }); if changes.clone().count() > 0 { -- cgit v1.2.3-70-g09d2 From 3156577fbf1a97e07e90e11b51c66155f122c3b7 Mon Sep 17 00:00:00 2001 From: ath3 Date: Sun, 12 Dec 2021 13:13:33 +0100 Subject: Open files with spaces in filename, allow opening multiple files (#1231) --- helix-core/src/lib.rs | 1 + helix-core/src/shellwords.rs | 164 +++++++++++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 145 +++++++++++++++++++------------------- 3 files changed, 239 insertions(+), 71 deletions(-) create mode 100644 helix-core/src/shellwords.rs (limited to 'helix-core/src/lib.rs') diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 4ae044cc..92a59f31 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -17,6 +17,7 @@ mod position; pub mod register; pub mod search; pub mod selection; +pub mod shellwords; mod state; pub mod surround; pub mod syntax; 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> { + enum State { + Normal, + NormalEscaped, + Quoted, + QuoteEscaped, + Dquoted, + DquoteEscaped, + } + + use State::*; + + let mut state = Normal; + let mut args: Vec> = 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-term/src/commands.rs b/helix-term/src/commands.rs index 87c5a63f..314cd11f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,7 +10,7 @@ use helix_core::{ movement::{self, Direction}, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, - search, selection, surround, textobject, + search, selection, shellwords, surround, textobject, unicode::width::UnicodeWidthChar, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -173,14 +173,14 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { MappableCommand::Typable { name, args, doc: _ } => { - let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect(); + let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, jobs: cx.jobs, scroll: None, }; - if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) { + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { cx.editor.set_error(format!("{}", e)); } } @@ -1963,13 +1963,13 @@ pub mod cmd { pub aliases: &'static [&'static str], pub doc: &'static str, // params, flags, helper, completer - pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>, + pub fun: fn(&mut compositor::Context, &[Cow], PromptEvent) -> anyhow::Result<()>, pub completer: Option, } fn quit( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { // last view and we have unsaved changes @@ -1984,7 +1984,7 @@ pub mod cmd { fn force_quit( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor.close(view!(cx.editor).id); @@ -1994,17 +1994,19 @@ pub mod cmd { fn open( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let path = args.get(0).context("wrong argument count")?; - let _ = cx.editor.open(path.into(), Action::Replace)?; + ensure!(!args.is_empty(), "wrong argument count"); + for arg in args { + let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?; + } Ok(()) } fn buffer_close( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let view = view!(cx.editor); @@ -2015,7 +2017,7 @@ pub mod cmd { fn force_buffer_close( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let view = view!(cx.editor); @@ -2024,15 +2026,12 @@ pub mod cmd { Ok(()) } - fn write_impl>( - cx: &mut compositor::Context, - path: Option

, - ) -> anyhow::Result<()> { + fn write_impl(cx: &mut compositor::Context, path: Option<&Cow>) -> anyhow::Result<()> { let jobs = &mut cx.jobs; let (_, doc) = current!(cx.editor); if let Some(ref path) = path { - doc.set_path(Some(path.as_ref())) + doc.set_path(Some(path.as_ref().as_ref())) .context("invalid filepath")?; } if doc.path().is_none() { @@ -2061,7 +2060,7 @@ pub mod cmd { fn write( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first()) @@ -2069,7 +2068,7 @@ pub mod cmd { fn new_file( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor.new_file(Action::Replace); @@ -2079,7 +2078,7 @@ pub mod cmd { fn format( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); @@ -2094,7 +2093,7 @@ pub mod cmd { } fn set_indent_style( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { use IndentStyle::*; @@ -2114,7 +2113,7 @@ pub mod cmd { // Attempt to parse argument as an indent style. let style = match args.get(0) { Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs), - Some(&"0") => Some(Tabs), + Some(Cow::Borrowed("0")) => Some(Tabs), Some(arg) => arg .parse::() .ok() @@ -2133,7 +2132,7 @@ pub mod cmd { /// Sets or reports the current document's line ending setting. fn set_line_ending( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { use LineEnding::*; @@ -2177,7 +2176,7 @@ pub mod cmd { fn earlier( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; @@ -2193,7 +2192,7 @@ pub mod cmd { fn later( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; @@ -2208,7 +2207,7 @@ pub mod cmd { fn write_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first())?; @@ -2217,7 +2216,7 @@ pub mod cmd { fn force_write_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first())?; @@ -2248,7 +2247,7 @@ pub mod cmd { fn write_all_impl( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, quit: bool, force: bool, @@ -2284,7 +2283,7 @@ pub mod cmd { fn write_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, false, false) @@ -2292,7 +2291,7 @@ pub mod cmd { fn write_all_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, true, false) @@ -2300,7 +2299,7 @@ pub mod cmd { fn force_write_all_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, true, true) @@ -2308,7 +2307,7 @@ pub mod cmd { fn quit_all_impl( editor: &mut Editor, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, force: bool, ) -> anyhow::Result<()> { @@ -2327,7 +2326,7 @@ pub mod cmd { fn quit_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { quit_all_impl(cx.editor, args, event, false) @@ -2335,7 +2334,7 @@ pub mod cmd { fn force_quit_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { quit_all_impl(cx.editor, args, event, true) @@ -2343,7 +2342,7 @@ pub mod cmd { fn cquit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let exit_code = args @@ -2362,7 +2361,7 @@ pub mod cmd { fn theme( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let theme = args.first().context("theme not provided")?; @@ -2371,7 +2370,7 @@ pub mod cmd { fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard) @@ -2379,20 +2378,18 @@ pub mod cmd { fn yank_joined_to_clipboard( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); - let separator = args - .first() - .copied() - .unwrap_or_else(|| doc.line_ending.as_str()); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard) } fn yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection) @@ -2400,20 +2397,18 @@ pub mod cmd { fn yank_joined_to_primary_clipboard( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); - let separator = args - .first() - .copied() - .unwrap_or_else(|| doc.line_ending.as_str()); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection) } fn paste_clipboard_after( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard) @@ -2421,7 +2416,7 @@ pub mod cmd { fn paste_clipboard_before( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard) @@ -2429,7 +2424,7 @@ pub mod cmd { fn paste_primary_clipboard_after( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection) @@ -2437,7 +2432,7 @@ pub mod cmd { fn paste_primary_clipboard_before( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection) @@ -2467,7 +2462,7 @@ pub mod cmd { fn replace_selections_with_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard) @@ -2475,7 +2470,7 @@ pub mod cmd { fn replace_selections_with_primary_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { replace_selections_with_clipboard_impl(cx, ClipboardType::Selection) @@ -2483,7 +2478,7 @@ pub mod cmd { fn show_clipboard_provider( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor @@ -2493,12 +2488,13 @@ pub mod cmd { fn change_current_directory( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let dir = helix_core::path::expand_tilde( args.first() .context("target directory not provided")? + .as_ref() .as_ref(), ); @@ -2516,7 +2512,7 @@ pub mod cmd { fn show_current_directory( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let cwd = std::env::current_dir().context("Couldn't get the new working directory")?; @@ -2528,7 +2524,7 @@ pub mod cmd { /// Sets the [`Document`]'s encoding.. fn set_encoding( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); @@ -2544,7 +2540,7 @@ pub mod cmd { /// Reload the [`Document`] from its source file. fn reload( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); @@ -2553,7 +2549,7 @@ pub mod cmd { fn tree_sitter_scopes( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); @@ -2567,15 +2563,18 @@ pub mod cmd { fn vsplit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let id = view!(cx.editor).doc; - if let Some(path) = args.get(0) { - cx.editor.open(path.into(), Action::VerticalSplit)?; - } else { + if args.is_empty() { cx.editor.switch(id, Action::VerticalSplit); + } else { + for arg in args { + cx.editor + .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; + } } Ok(()) @@ -2583,15 +2582,18 @@ pub mod cmd { fn hsplit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let id = view!(cx.editor).doc; - if let Some(path) = args.get(0) { - cx.editor.open(path.into(), Action::HorizontalSplit)?; - } else { + if args.is_empty() { cx.editor.switch(id, Action::HorizontalSplit); + } else { + for arg in args { + cx.editor + .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; + } } Ok(()) @@ -2599,7 +2601,7 @@ pub mod cmd { fn tutor( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let path = helix_core::runtime_dir().join("tutor.txt"); @@ -2611,7 +2613,7 @@ pub mod cmd { pub(super) fn goto_line_number( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { ensure!(!args.is_empty(), "Line number required"); @@ -2980,7 +2982,7 @@ fn command_mode(cx: &mut Context) { // If command is numeric, interpret as line number and go there. if parts.len() == 1 && parts[0].parse::().ok().is_some() { - if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) { + if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) { cx.editor.set_error(format!("{}", e)); } return; @@ -2988,7 +2990,8 @@ fn command_mode(cx: &mut Context) { // Handle typable commands if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { - if let Err(e) = (cmd.fun)(cx, &parts[1..], event) { + let args = shellwords::shellwords(input); + if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } } else { -- cgit v1.2.3-70-g09d2 From 4b0b1a5657b78693efe609647360de30264fcc92 Mon Sep 17 00:00:00 2001 From: Matouš Dzivjak Date: Sat, 25 Dec 2021 16:10:46 +0100 Subject: feat(ui): file encoding in statusline (#1355) * feat(ui): file encoding in statusline Display file encoding in statusline if the encoding isn't UTF-8. * Re-export encoding_rs from core From there it can be imported by other mods that rely on it.--- Cargo.lock | 2 +- helix-core/Cargo.toml | 1 + helix-core/src/lib.rs | 2 ++ helix-term/src/ui/editor.rs | 9 ++++++++- helix-view/Cargo.toml | 1 - helix-view/src/document.rs | 33 +++++++++++++++++---------------- 6 files changed, 29 insertions(+), 19 deletions(-) (limited to 'helix-core/src/lib.rs') diff --git a/Cargo.lock b/Cargo.lock index 27268feb..40afe4e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,6 +370,7 @@ version = "0.5.0" dependencies = [ "arc-swap", "chrono", + "encoding_rs", "etcetera", "helix-syntax", "log", @@ -471,7 +472,6 @@ dependencies = [ "chardetng", "clipboard-win", "crossterm", - "encoding_rs", "futures-util", "helix-core", "helix-lsp", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 839b07ac..3d7fe866 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -35,6 +35,7 @@ toml = "0.5" similar = "2.1" etcetera = "0.3" +encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 92a59f31..1c78e7c0 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; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 390b3759..dd050398 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -7,7 +7,7 @@ use crate::{ }; use helix_core::{ - coords_at_pos, + coords_at_pos, encoding, graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary}, movement::Direction, syntax::{self, HighlightEvent}, @@ -621,6 +621,13 @@ impl EditorView { base_style, )); + let enc = doc.encoding(); + if enc != encoding::UTF_8 { + right_side_text + .0 + .push(Span::styled(format!(" {} ", enc.name()), base_style)); + } + // Render to the statusline. surface.set_spans( viewport.x diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 9c3a83f8..121a518c 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -29,7 +29,6 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea slotmap = "1" -encoding_rs = "0.8" chardetng = "0.1" serde = { version = "1.0", features = ["derive"] } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index c71d1850..9652d7b3 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -9,6 +9,7 @@ use std::str::FromStr; use std::sync::Arc; use helix_core::{ + encoding, history::History, indent::{auto_detect_indent_style, IndentStyle}, line_ending::auto_detect_line_ending, @@ -74,7 +75,7 @@ pub struct Document { pub(crate) selections: HashMap, path: Option, - encoding: &'static encoding_rs::Encoding, + encoding: &'static encoding::Encoding, /// Current editing mode. pub mode: Mode, @@ -143,8 +144,8 @@ impl fmt::Debug for Document { /// be used to override encoding auto-detection. pub fn from_reader( reader: &mut R, - encoding: Option<&'static encoding_rs::Encoding>, -) -> Result<(Rope, &'static encoding_rs::Encoding), Error> { + encoding: Option<&'static encoding::Encoding>, +) -> Result<(Rope, &'static encoding::Encoding), Error> { // These two buffers are 8192 bytes in size each and are used as // intermediaries during the decoding process. Text read into `buf` // from `reader` is decoded into `buf_out` as UTF-8. Once either @@ -212,11 +213,11 @@ pub fn from_reader( total_read += read; total_written += written; match result { - encoding_rs::CoderResult::InputEmpty => { + encoding::CoderResult::InputEmpty => { debug_assert_eq!(slice.len(), total_read); break; } - encoding_rs::CoderResult::OutputFull => { + encoding::CoderResult::OutputFull => { debug_assert!(slice.len() > total_read); builder.append(&buf_str[..total_written]); total_written = 0; @@ -251,7 +252,7 @@ pub fn from_reader( /// replacement characters may appear in the encoded text. pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( writer: &'a mut W, - encoding: &'static encoding_rs::Encoding, + encoding: &'static encoding::Encoding, rope: &'a Rope, ) -> Result<(), Error> { // Text inside a `Rope` is stored as non-contiguous blocks of data called @@ -286,12 +287,12 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( total_read += read; total_written += written; match result { - encoding_rs::CoderResult::InputEmpty => { + encoding::CoderResult::InputEmpty => { debug_assert_eq!(chunk.len(), total_read); debug_assert!(buf.len() >= total_written); break; } - encoding_rs::CoderResult::OutputFull => { + encoding::CoderResult::OutputFull => { debug_assert!(chunk.len() > total_read); writer.write_all(&buf[..total_written]).await?; total_written = 0; @@ -322,8 +323,8 @@ use helix_lsp::lsp; use url::Url; impl Document { - pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self { - let encoding = encoding.unwrap_or(encoding_rs::UTF_8); + pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self { + let encoding = encoding.unwrap_or(encoding::UTF_8); let changes = ChangeSet::new(&text); let old_state = None; @@ -356,7 +357,7 @@ impl Document { /// overwritten with the `encoding` parameter. pub fn open( path: &Path, - encoding: Option<&'static encoding_rs::Encoding>, + encoding: Option<&'static encoding::Encoding>, theme: Option<&Theme>, config_loader: Option<&syntax::Loader>, ) -> Result { @@ -366,7 +367,7 @@ impl Document { std::fs::File::open(path).context(format!("unable to open {:?}", path))?; from_reader(&mut file, encoding)? } else { - let encoding = encoding.unwrap_or(encoding_rs::UTF_8); + let encoding = encoding.unwrap_or(encoding::UTF_8); (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding) }; @@ -548,7 +549,7 @@ impl Document { /// Sets the [`Document`]'s encoding with the encoding correspondent to `label`. pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> { - match encoding_rs::Encoding::for_label(label.as_bytes()) { + match encoding::Encoding::for_label(label.as_bytes()) { Some(encoding) => self.encoding = encoding, None => return Err(anyhow::anyhow!("unknown encoding")), } @@ -556,7 +557,7 @@ impl Document { } /// Returns the [`Document`]'s current encoding. - pub fn encoding(&self) -> &'static encoding_rs::Encoding { + pub fn encoding(&self) -> &'static encoding::Encoding { self.encoding } @@ -1123,7 +1124,7 @@ mod test { macro_rules! test_decode { ($label:expr, $label_override:expr) => { - let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap(); + let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap(); let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding"); let path = base_path.join(format!("{}_in.txt", $label)); let ref_path = base_path.join(format!("{}_in_ref.txt", $label)); @@ -1142,7 +1143,7 @@ mod test { macro_rules! test_encode { ($label:expr, $label_override:expr) => { - let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap(); + let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap(); let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding"); let path = base_path.join(format!("{}_out.txt", $label)); let ref_path = base_path.join(format!("{}_out_ref.txt", $label)); -- cgit v1.2.3-70-g09d2 From 8a019b423f5d51bcfc107da9cb50aa90eacff19d Mon Sep 17 00:00:00 2001 From: Alexis Mousset Date: Fri, 31 Dec 2021 09:06:54 +0100 Subject: Detect workspace root using language markers (#1370) * Detect workspace root using language markers * Avoid allocating root_markers * Update helix-core/src/lib.rs Co-authored-by: Blaž Hrastnik * Update helix-core/src/lib.rs Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com> Co-authored-by: Blaž Hrastnik Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com>--- helix-core/src/lib.rs | 32 ++++++++++++++++++++++++++------ helix-lsp/src/client.rs | 6 +++++- helix-lsp/src/lib.rs | 1 + helix-term/src/commands.rs | 3 ++- languages.toml | 2 +- 5 files changed, 35 insertions(+), 9 deletions(-) (limited to 'helix-core/src/lib.rs') diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 1c78e7c0..7fd23b97 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -39,8 +39,14 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option { line.chars().position(|ch| !ch.is_whitespace()) } -/// Find `.git` root. -pub fn find_root(root: Option<&str>) -> Option { +/// 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 { let current_dir = std::env::current_dir().expect("unable to determine current directory"); let root = match root { @@ -52,16 +58,30 @@ pub fn find_root(root: Option<&str>) -> Option { 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 { diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 43804daa..c80f70b5 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -31,6 +31,7 @@ pub struct Client { pub(crate) capabilities: OnceCell, offset_encoding: OffsetEncoding, config: Option, + root_markers: Vec, } impl Client { @@ -39,6 +40,7 @@ impl Client { cmd: &str, args: &[String], config: Option, + root_markers: Vec, id: usize, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { let process = Command::new(cmd) @@ -68,6 +70,7 @@ impl Client { capabilities: OnceCell::new(), offset_encoding: OffsetEncoding::Utf8, config, + root_markers, }; Ok((client, server_rx, initialize_notify)) @@ -225,7 +228,8 @@ impl Client { pub(crate) async fn initialize(&self) -> Result { // TODO: delay any requests that are triggered prior to initialize - let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok()); + let root = find_root(None, &self.root_markers) + .and_then(|root| lsp::Url::from_file_path(root).ok()); if self.config.is_some() { log::info!("Using custom LSP config: {}", self.config.as_ref().unwrap()); diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 8fb321bc..1eb1c151 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -326,6 +326,7 @@ impl Registry { &config.command, &config.args, language_config.config.clone(), + language_config.roots.clone(), id, )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6259ecb2..ed728115 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3154,7 +3154,8 @@ fn command_mode(cx: &mut Context) { } fn file_picker(cx: &mut Context) { - let root = find_root(None).unwrap_or_else(|| PathBuf::from("./")); + // We don't specify language markers, root will be the root of the current git repo + let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./")); let picker = ui::file_picker(root, &cx.editor.config); cx.push_layer(Box::new(picker)); } diff --git a/languages.toml b/languages.toml index 3d9bac7b..f088a3aa 100644 --- a/languages.toml +++ b/languages.toml @@ -3,7 +3,7 @@ name = "rust" scope = "source.rust" injection-regex = "rust" file-types = ["rs"] -roots = [] +roots = ["Cargo.toml", "Cargo.lock"] auto-format = true comment-token = "//" language-server = { command = "rust-analyzer" } -- cgit v1.2.3-70-g09d2 From f88c077f992bbfc5ea3623441a9e2b2a0e9ca2b2 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 7 Feb 2022 14:03:04 +0900 Subject: Replace tendril with smartstring Slightly smaller API surface, less dependencies. --- Cargo.lock | 56 ++++++++++------------------------- helix-core/Cargo.toml | 2 +- helix-core/src/auto_pairs.rs | 14 +++++---- helix-core/src/increment/date_time.rs | 2 +- helix-core/src/increment/number.rs | 10 +++---- helix-core/src/lib.rs | 5 +++- helix-core/src/transaction.rs | 24 ++++++++------- helix-lsp/src/client.rs | 2 +- helix-term/src/commands.rs | 32 +++++++++++--------- xtask/src/main.rs | 2 +- 10 files changed, 69 insertions(+), 80 deletions(-) (limited to 'helix-core/src/lib.rs') diff --git a/Cargo.lock b/Cargo.lock index 3253e6cd..360ebcfb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,16 +246,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" -dependencies = [ - "mac", - "new_debug_unreachable", -] - [[package]] name = "futures-core" version = "0.3.21" @@ -383,7 +373,7 @@ dependencies = [ "similar", "slotmap", "smallvec", - "tendril", + "smartstring", "toml", "tree-sitter", "unicode-general-category", @@ -606,12 +596,6 @@ dependencies = [ "url", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - [[package]] name = "matches" version = "0.1.9" @@ -655,12 +639,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - [[package]] name = "ntapi" version = "0.3.6" @@ -1010,6 +988,21 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "smartstring" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "str-buf" version = "1.0.5" @@ -1027,17 +1020,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tendril" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" -dependencies = [ - "futf", - "mac", - "utf-8", -] - [[package]] name = "thiserror" version = "1.0.30" @@ -1212,12 +1194,6 @@ dependencies = [ "serde", ] -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - [[package]] name = "version_check" version = "0.9.4" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 3a1f9b3e..7ff91cfd 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -17,7 +17,7 @@ helix-syntax = { version = "0.6", path = "../helix-syntax" } ropey = "1.3" smallvec = "1.8" -tendril = "0.4.2" +smartstring = "0.2.9" unicode-segmentation = "1.9" unicode-width = "0.1" unicode-general-category = "0.5" diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 40802680..f4359a34 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -214,7 +214,9 @@ fn handle_open( let change = match next_char { Some(ch) if !close_before.contains(ch) => { len_inserted = open.len_utf8(); - (cursor, cursor, Some(Tendril::from_char(open))) + let mut tendril = Tendril::new(); + tendril.push(open); + (cursor, cursor, Some(tendril)) } // None | Some(ch) if close_before.contains(ch) => {} _ => { @@ -252,7 +254,9 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> (cursor, cursor, None) // no-op } else { len_inserted += close.len_utf8(); - (cursor, cursor, Some(Tendril::from_char(close))) + 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); @@ -290,15 +294,15 @@ fn handle_same( // return transaction that moves past close (cursor, cursor, None) // no-op } else { - let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32); - pair.push_char(token); + let mut pair = Tendril::new(); + pair.push(token); // 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_char(token); + pair.push(token); } len_inserted += pair.len(); diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs index 1703c3ba..91fa5963 100644 --- a/helix-core/src/increment/date_time.rs +++ b/helix-core/src/increment/date_time.rs @@ -451,7 +451,7 @@ mod test { .unwrap() .increment(amount) .1, - expected.into() + Tendril::from(expected) ); } } diff --git a/helix-core/src/increment/number.rs b/helix-core/src/increment/number.rs index a19b7e75..57171f67 100644 --- a/helix-core/src/increment/number.rs +++ b/helix-core/src/increment/number.rs @@ -371,7 +371,7 @@ mod test { .unwrap() .increment(amount) .1, - expected.into() + Tendril::from(expected) ); } } @@ -398,7 +398,7 @@ mod test { .unwrap() .increment(amount) .1, - expected.into() + Tendril::from(expected) ); } } @@ -426,7 +426,7 @@ mod test { .unwrap() .increment(amount) .1, - expected.into() + Tendril::from(expected) ); } } @@ -472,7 +472,7 @@ mod test { .unwrap() .increment(amount) .1, - expected.into() + Tendril::from(expected) ); } } @@ -500,7 +500,7 @@ mod test { .unwrap() .increment(amount) .1, - expected.into() + Tendril::from(expected) ); } } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 7fd23b97..0b03ead8 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -212,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; #[doc(inline)] pub use {regex, tree_sitter}; diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 30995e8c..2e34a986 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -85,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)), @@ -189,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(); } @@ -211,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(); } } @@ -277,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) => { @@ -710,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()); diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index c80f70b5..15cbca0e 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -438,7 +438,7 @@ impl Client { changes.push(lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range::new(start, end)), - text: s.into(), + text: s.to_string(), range_length: None, }); } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d0882728..9918d26c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2769,7 +2769,7 @@ pub mod cmd { let mut fragments: Vec<_> = selection .fragments(text) - .map(|fragment| Tendril::from_slice(&fragment)) + .map(|fragment| Tendril::from(fragment.as_ref())) .collect(); fragments.sort_by(match reverse { @@ -3921,7 +3921,7 @@ fn try_restore_indent(doc: &mut Document, view_id: ViewId) { if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] = changes { - move_pos + inserted_str.len32() as usize == pos + move_pos + inserted_str.len() == pos && inserted_str.starts_with('\n') && inserted_str.chars().skip(1).all(char_is_whitespace) && pos == line_end_pos // ensure no characters exists after current position @@ -4513,7 +4513,8 @@ pub mod insert { #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option { let cursors = selection.clone().cursors(doc.slice(..)); - let t = Tendril::from_char(ch); + let mut t = Tendril::new(); + t.push(ch); let transaction = Transaction::insert(doc, &cursors, t); Some(transaction) } @@ -5015,12 +5016,12 @@ fn replace_with_yanked(cx: &mut Context) { let repeat = std::iter::repeat( values .last() - .map(|value| Tendril::from_slice(&value.repeat(count))) + .map(|value| Tendril::from(&value.repeat(count))) .unwrap(), ); let mut values = values .iter() - .map(|value| Tendril::from_slice(&value.repeat(count))) + .map(|value| Tendril::from(&value.repeat(count))) .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { @@ -5530,7 +5531,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) { let selection = doc.selection(view.id); let mut fragments: Vec<_> = selection .fragments(text) - .map(|fragment| Tendril::from_slice(&fragment)) + .map(|fragment| Tendril::from(fragment.as_ref())) .collect(); let group = count @@ -5891,8 +5892,12 @@ fn surround_add(cx: &mut Context) { let mut changes = Vec::with_capacity(selection.len() * 2); for range in selection.iter() { - changes.push((range.from(), range.from(), Some(Tendril::from_char(open)))); - changes.push((range.to(), range.to(), Some(Tendril::from_char(close)))); + let mut o = Tendril::new(); + o.push(open); + let mut c = Tendril::new(); + c.push(close); + changes.push((range.from(), range.from(), Some(o))); + changes.push((range.to(), range.to(), Some(c))); } let transaction = Transaction::change(doc.text(), changes.into_iter()); @@ -5921,11 +5926,9 @@ fn surround_replace(cx: &mut Context) { let transaction = Transaction::change( doc.text(), change_pos.iter().enumerate().map(|(i, &pos)| { - ( - pos, - pos + 1, - Some(Tendril::from_char(if i % 2 == 0 { open } else { close })), - ) + let mut t = Tendril::new(); + t.push(if i % 2 == 0 { open } else { close }); + (pos, pos + 1, Some(t)) }), ); doc.apply(&transaction, view.id); @@ -6065,8 +6068,9 @@ fn shell_impl( log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr)); } - let tendril = Tendril::try_from_byte_slice(&output.stdout) + let str = std::str::from_utf8(&output.stdout) .map_err(|_| anyhow!("Process did not output valid UTF-8"))?; + let tendril = Tendril::from(str); Ok((tendril, output.status.success())) } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 7256653a..d24a29cc 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -120,7 +120,7 @@ pub mod md_gen { } fn md_table_row(cols: &[String]) -> String { - "| ".to_owned() + &cols.join(" | ") + " |\n" + format!("| {} |\n", cols.join(" | ")) } fn md_mono(s: &str) -> String { -- cgit v1.2.3-70-g09d2