From cce19713fb970df1a96be7e76e3217e4e50efc42 Mon Sep 17 00:00:00 2001 From: gavynriebau Date: Mon, 16 Jan 2023 15:13:48 +0800 Subject: [PATCH 1/8] Fix for lost clipboard contents (#5424) (#5426) * Fix for lost clipboard contents (#5424) * PR feedback: Call "setsid" for all unix systems * PR Feedback: Only install libc for unix targets --- Cargo.lock | 1 + helix-view/Cargo.toml | 3 +++ helix-view/src/clipboard.rs | 21 ++++++++++++++++++--- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69ba84449..ede4ae0d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,6 +1274,7 @@ dependencies = [ "helix-lsp", "helix-tui", "helix-vcs", + "libc", "log", "once_cell", "serde", diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index e7a20496d..7d130317e 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -48,5 +48,8 @@ which = "4.2" [target.'cfg(windows)'.dependencies] clipboard-win = { version = "4.5", features = ["std"] } +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [dev-dependencies] helix-tui = { path = "../helix-tui" } diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index 4f83fb4dc..3c620c146 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -276,12 +276,27 @@ pub mod provider { let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null); let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null); - let mut child = Command::new(self.prg) + let mut command: Command = Command::new(self.prg); + + let mut command_mut: &mut Command = command .args(self.args) .stdin(stdin) .stdout(stdout) - .stderr(Stdio::null()) - .spawn()?; + .stderr(Stdio::null()); + + // Fix for https://github.com/helix-editor/helix/issues/5424 + if cfg!(unix) { + use std::os::unix::process::CommandExt; + + unsafe { + command_mut = command_mut.pre_exec(|| match libc::setsid() { + -1 => Err(std::io::Error::last_os_error()), + _ => Ok(()), + }); + } + } + + let mut child = command_mut.spawn()?; if let Some(input) = input { let mut stdin = child.stdin.take().context("stdin is missing")?; From d3e0f18c89c94c887d50d2487a4ee76eb96dda2b Mon Sep 17 00:00:00 2001 From: Itay123 <40892795+Itay123TheKing@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:18:13 +0200 Subject: [PATCH 2/8] Added opening files in the background with A-ret shortcut (#4435) --- helix-term/src/ui/picker.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index aad3f81ce..eb935e567 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,4 +1,5 @@ use crate::{ + alt, compositor::{Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, ui::{self, fuzzy_match::FuzzyQuery, EditorView}, @@ -619,6 +620,11 @@ impl Component for Picker { key!(Esc) | ctrl!('c') => { return close_fn; } + alt!(Enter) => { + if let Some(option) = self.selection() { + (self.callback_fn)(cx, option, Action::Load); + } + } key!(Enter) => { if let Some(option) = self.selection() { (self.callback_fn)(cx, option, Action::Replace); From 6f6334f3c65f66f72867e6500b3c4a76ed017dfe Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Mon, 16 Jan 2023 10:48:17 -0500 Subject: [PATCH 3/8] highlight(scala): update the Scala highlight queries (#5546) There have been a lot of changes in tree-sitter/tree-sitter-scala, including partial support for Scala 3 syntax and breaking changes in some of the nodes. This bumps up the grammar to the latest, and adjusts the queries. Co-authored-by: Anton Sviridov Co-authored-by: Chris Kipp --- languages.toml | 2 +- runtime/queries/scala/highlights.scm | 129 ++++++++++++++++++++------- 2 files changed, 100 insertions(+), 31 deletions(-) diff --git a/languages.toml b/languages.toml index 51b4efb2e..6d016902b 100644 --- a/languages.toml +++ b/languages.toml @@ -1064,7 +1064,7 @@ language-server = { command = "metals" } [[grammar]] name = "scala" -source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "140c96cf398693189d4e50f76d19ddfcd8a018f8" } +source = { git = "https://github.com/tree-sitter/tree-sitter-scala", rev = "db1c8c23d7996476a791db85a0d292084c19c232" } [[language]] name = "dockerfile" diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm index 50a6e18a6..073078750 100644 --- a/runtime/queries/scala/highlights.scm +++ b/runtime/queries/scala/highlights.scm @@ -33,6 +33,26 @@ (type_definition name: (type_identifier) @type) +(full_enum_case + name: (identifier) @type) + +(simple_enum_case + name: (identifier) @type) + +;; val/var definitions/declarations + +(val_definition + pattern: (identifier) @variable) + +(var_definition + pattern: (identifier) @variable) + +(val_declaration + name: (identifier) @variable) + +(var_declaration + name: (identifier) @variable) + ; method definition (class_definition @@ -48,7 +68,7 @@ (function_definition name: (identifier) @function.method))) -; imports +; imports/exports (import_declaration path: (identifier) @namespace) @@ -58,7 +78,15 @@ path: (identifier) @type) (#match? @type "^[A-Z]")) ((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) -((import_selectors (identifier) @type) (#match? @type "^[A-Z]")) +(export_declaration + path: (identifier) @namespace) +((stable_identifier (identifier) @namespace)) + +((export_declaration + path: (identifier) @type) (#match? @type "^[A-Z]")) +((stable_identifier (identifier) @type) (#match? @type "^[A-Z]")) + +((namespace_selectors (identifier) @type) (#match? @type "^[A-Z]")) ; method invocation @@ -66,10 +94,17 @@ (call_expression function: (identifier) @function) +(call_expression + function: (operator_identifier) @function) + (call_expression function: (field_expression field: (identifier) @function.method)) +(call_expression + function: (field_expression + field: (operator_identifier) @function.method)) + ((call_expression function: (identifier) @variable.other.member) (#match? @variable.other.member "^[A-Z]")) @@ -87,9 +122,15 @@ (function_definition name: (identifier) @function) +(function_definition + name: (operator_identifier) @function) + (parameter name: (identifier) @variable.parameter) +(binding + name: (identifier) @variable.parameter) + ; expressions @@ -109,7 +150,7 @@ (symbol_literal) @string.special.symbol - + [ (string) (character_literal) @@ -118,29 +159,50 @@ (interpolation "$" @punctuation.special) +; annotations + +(annotation) @attribute + ;; keywords +;; storage in TextMate scope lingo means field or type [ + (opaque_modifier) + (infix_modifier) + (transparent_modifier) + (open_modifier) "abstract" - "case" - "class" - "extends" "final" - "finally" -;; `forSome` existential types not implemented yet "implicit" "lazy" -;; `macro` not implemented yet - "object" "override" - "package" "private" "protected" "sealed" +] @keyword.storage.modifier + +[ + "class" + "enum" + "extension" + "given" + "object" + "package" "trait" "type" "val" "var" +] @keyword.storage.type + +[ + "as" + "derives" + "end" + "extends" +;; `forSome` existential types not implemented yet +;; `macro` not implemented yet +;; `throws` + "using" "with" ] @keyword @@ -152,33 +214,36 @@ "new" @keyword.operator [ - "else" - "if" - "match" - "try" - "catch" - "throw" + "case" + "catch" + "else" + "finally" + "if" + "match" + "then" + "throw" + "try" ] @keyword.control.conditional [ - "(" - ")" - "[" - "]" - "{" - "}" + "(" + ")" + "[" + "]" + "{" + "}" ] @punctuation.bracket [ - "." - "," + "." + "," ] @punctuation.delimiter [ - "do" - "for" - "while" - "yield" + "do" + "for" + "while" + "yield" ] @keyword.control.repeat "def" @keyword.function @@ -191,6 +256,8 @@ "import" @keyword.control.import +"export" @keyword.control.import + "return" @keyword.control.return (comment) @comment @@ -200,4 +267,6 @@ (case_block (case_clause ("case") @keyword.control.conditional)) -(identifier) @variable \ No newline at end of file +(identifier) @variable + +(operator_identifier) @operator From 425d7e5f1b67d2c1748b4b30091d88d1446aecc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Delafargue?= Date: Mon, 16 Jan 2023 11:13:03 +0100 Subject: [PATCH 4/8] doc: add a note about nested bindings in key remapping It was not clear (to me) that minor modes were configurable in the keymap configuration. --- book/src/remapping.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/book/src/remapping.md b/book/src/remapping.md index e89c66113..2eac8846f 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -25,6 +25,9 @@ j = { k = "normal_mode" } # Maps `jk` to exit insert mode ``` > NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command. +> NOTE: Bindings can be nested, to create (or edit) minor modes: `g = { a = "code_action"}` adds a new entry to +> the `goto` mode. + Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes `C-`, `S-` and `A-`. Special keys are encoded as follows: From 7bdba4a6bff99a89c0256889222f7bba5bfb1130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Delafargue?= Date: Mon, 16 Jan 2023 16:30:35 +0100 Subject: [PATCH 5/8] doc: add missing `whitespace.render` sub-key --- book/src/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index a35482e6e..f89ef5aed 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -216,7 +216,7 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r | Key | Description | Default | |-----|-------------|---------| -| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `tab`, and `newline`. | `"none"` | +| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline`. | `"none"` | | `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below | Example From 97083f88364e1455f42023dadadfb410fd476505 Mon Sep 17 00:00:00 2001 From: Ayoub Benali Date: Mon, 16 Jan 2023 17:03:03 +0100 Subject: [PATCH 6/8] Enable http server by default in Metals config (#5551) This is required to make commands like [doctor-run](https://scalameta.org/metals/docs/integrations/new-editor#run-doctor) work. It simply opens a browser to get general information about the build. Co-authored-by: Ayoub Benali --- languages.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 6d016902b..a66605b29 100644 --- a/languages.toml +++ b/languages.toml @@ -1061,6 +1061,7 @@ file-types = ["scala", "sbt", "sc"] comment-token = "//" indent = { tab-width = 2, unit = " " } language-server = { command = "metals" } +config = { "isHttpEnabled" = true } [[grammar]] name = "scala" @@ -2106,4 +2107,4 @@ formatter = { command = "dhall" , args = ["format"] } [[grammar]] name = "dhall" -source = { git = "https://github.com/jbellerb/tree-sitter-dhall", rev = "affb6ee38d629c9296749767ab832d69bb0d9ea8" } \ No newline at end of file +source = { git = "https://github.com/jbellerb/tree-sitter-dhall", rev = "affb6ee38d629c9296749767ab832d69bb0d9ea8" } From 60f84be40c1c488dacf823f791ca33f43b5d28d8 Mon Sep 17 00:00:00 2001 From: greg-enbala <66078183+greg-enbala@users.noreply.github.com> Date: Mon, 16 Jan 2023 11:15:23 -0500 Subject: [PATCH 7/8] Separate jump behavior from increment/decrement (#4123) increment/decrement (C-a/C-x) had some buggy behavior where selections could be offset incorrectly or the editor could panic with some edits that changed the number of characters in a number or date. These stemmed from the automatic jumping behavior which attempted to find the next date or integer to increment. The jumping behavior also complicated the code quite a bit and made the behavior somewhat difficult to predict when using many cursors. This change removes the automatic jumping behavior and only increments or decrements when the full text in a range of a selection is a number or date. This simplifies the code and fixes the panics and buggy behaviors from changing the number of characters. --- helix-core/src/increment/date_time.rs | 321 ++++------------ helix-core/src/increment/integer.rs | 235 ++++++++++++ helix-core/src/increment/mod.rs | 12 +- helix-core/src/increment/number.rs | 507 -------------------------- helix-term/src/commands.rs | 113 ++---- 5 files changed, 349 insertions(+), 839 deletions(-) create mode 100644 helix-core/src/increment/integer.rs delete mode 100644 helix-core/src/increment/number.rs diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs index 265242ce4..2980bb58b 100644 --- a/helix-core/src/increment/date_time.rs +++ b/helix-core/src/increment/date_time.rs @@ -1,114 +1,53 @@ -use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime}; use once_cell::sync::Lazy; use regex::Regex; -use ropey::RopeSlice; - -use std::borrow::Cow; -use std::cmp; use std::fmt::Write; -use super::Increment; -use crate::{Range, Tendril}; +/// Increment a Date or DateTime +/// +/// If just a Date is selected the day will be incremented. +/// If a DateTime is selected the second will be incremented. +pub fn increment(selected_text: &str, amount: i64) -> Option { + if selected_text.is_empty() { + return None; + } -#[derive(Debug, PartialEq, Eq)] -pub struct DateTimeIncrementor { - date_time: NaiveDateTime, - range: Range, - fmt: &'static str, - field: DateField, -} + FORMATS.iter().find_map(|format| { + let captures = format.regex.captures(selected_text)?; + if captures.len() - 1 != format.fields.len() { + return None; + } -impl DateTimeIncrementor { - pub fn from_range(text: RopeSlice, range: Range) -> Option { - let range = if range.is_empty() { - if range.anchor < text.len_chars() { - // Treat empty range as a cursor range. - range.put_cursor(text, range.anchor + 1, true) - } else { - // The range is empty and at the end of the text. - return None; + let date_time = captures.get(0)?; + let has_date = format.fields.iter().any(|f| f.unit.is_date()); + let has_time = format.fields.iter().any(|f| f.unit.is_time()); + let date_time = &selected_text[date_time.start()..date_time.end()]; + match (has_date, has_time) { + (true, true) => { + let date_time = NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?; + Some( + date_time + .checked_add_signed(Duration::minutes(amount))? + .format(format.fmt) + .to_string(), + ) } - } else { - range - }; - - FORMATS.iter().find_map(|format| { - let from = range.from().saturating_sub(format.max_len); - let to = (range.from() + format.max_len).min(text.len_chars()); - - let (from_in_text, to_in_text) = (range.from() - from, range.to() - from); - let text: Cow = text.slice(from..to).into(); - - let captures = format.regex.captures(&text)?; - if captures.len() - 1 != format.fields.len() { - return None; + (true, false) => { + let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?; + Some( + date.checked_add_signed(Duration::days(amount))? + .format(format.fmt) + .to_string(), + ) } - - let date_time = captures.get(0)?; - let offset = range.from() - from_in_text; - let range = Range::new(date_time.start() + offset, date_time.end() + offset); - - let field = captures - .iter() - .skip(1) - .enumerate() - .find_map(|(i, capture)| { - let capture = capture?; - let capture_range = capture.range(); - - if capture_range.contains(&from_in_text) - && capture_range.contains(&(to_in_text - 1)) - { - Some(format.fields[i]) - } else { - None - } - })?; - - let has_date = format.fields.iter().any(|f| f.unit.is_date()); - let has_time = format.fields.iter().any(|f| f.unit.is_time()); - - let date_time = &text[date_time.start()..date_time.end()]; - let date_time = match (has_date, has_time) { - (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?, - (true, false) => { - let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?; - - date.and_hms_opt(0, 0, 0).unwrap() - } - (false, true) => { - let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?; - - NaiveDate::from_ymd_opt(0, 1, 1).unwrap().and_time(time) - } - (false, false) => return None, - }; - - Some(DateTimeIncrementor { - date_time, - range, - fmt: format.fmt, - field, - }) - }) - } -} - -impl Increment for DateTimeIncrementor { - fn increment(&self, amount: i64) -> (Range, Tendril) { - let date_time = match self.field.unit { - DateUnit::Years => add_years(self.date_time, amount), - DateUnit::Months => add_months(self.date_time, amount), - DateUnit::Days => add_duration(self.date_time, Duration::days(amount)), - DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)), - DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)), - DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)), - DateUnit::AmPm => toggle_am_pm(self.date_time), + (false, true) => { + let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?; + let (adjusted_time, _) = time.overflowing_add_signed(Duration::minutes(amount)); + Some(adjusted_time.format(format.fmt).to_string()) + } + (false, false) => None, } - .unwrap_or(self.date_time); - - (self.range, date_time.format(self.fmt).to_string().into()) - } + }) } static FORMATS: Lazy> = Lazy::new(|| { @@ -144,7 +83,7 @@ impl Format { fn new(fmt: &'static str) -> Self { let mut remaining = fmt; let mut fields = Vec::new(); - let mut regex = String::new(); + let mut regex = "^".to_string(); let mut max_len = 0; while let Some(i) = remaining.find('%') { @@ -166,6 +105,7 @@ impl Format { write!(regex, "({})", field.regex).unwrap(); remaining = &after[spec_len..]; } + regex += "$"; let regex = Regex::new(®ex).unwrap(); @@ -305,155 +245,47 @@ impl DateUnit { } } -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_opt(y, m, 1).unwrap(); - - // ...is preceded by the last day of the original month. - d.pred_opt().unwrap().day() -} - -fn add_months(date_time: NaiveDateTime, amount: i64) -> Option { - let month = (date_time.month0() as i64).checked_add(amount)?; - let year = date_time.year() + i32::try_from(month / 12).ok()?; - let year = if month.is_negative() { year - 1 } else { year }; - - // Normalize month - let month = month % 12; - let month = if month.is_negative() { - month + 12 - } else { - month - } as u32 - + 1; - - let day = cmp::min(date_time.day(), ndays_in_month(year, month)); - - NaiveDate::from_ymd_opt(year, month, day).map(|date| date.and_time(date_time.time())) -} - -fn add_years(date_time: NaiveDateTime, amount: i64) -> Option { - let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?; - let ndays = ndays_in_month(year, date_time.month()); - - if date_time.day() > ndays { - NaiveDate::from_ymd_opt(year, date_time.month(), ndays) - .and_then(|date| date.succ_opt().map(|date| date.and_time(date_time.time()))) - } else { - date_time.with_year(year) - } -} - -fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option { - date_time.checked_add_signed(duration) -} - -fn toggle_am_pm(date_time: NaiveDateTime) -> Option { - if date_time.hour() < 12 { - add_duration(date_time, Duration::hours(12)) - } else { - add_duration(date_time, Duration::hours(-12)) - } -} - #[cfg(test)] mod test { use super::*; - use crate::Rope; #[test] fn test_increment_date_times() { let tests = [ // (original, cursor, amount, expected) - ("2020-02-28", 0, 1, "2021-02-28"), - ("2020-02-29", 0, 1, "2021-03-01"), - ("2020-01-31", 5, 1, "2020-02-29"), - ("2020-01-20", 5, 1, "2020-02-20"), - ("2021-01-01", 5, -1, "2020-12-01"), - ("2021-01-31", 5, -2, "2020-11-30"), - ("2020-02-28", 8, 1, "2020-02-29"), - ("2021-02-28", 8, 1, "2021-03-01"), - ("2021-02-28", 0, -1, "2020-02-28"), - ("2021-03-01", 0, -1, "2020-03-01"), - ("2020-02-29", 5, -1, "2020-01-29"), - ("2020-02-20", 5, -1, "2020-01-20"), - ("2020-02-29", 8, -1, "2020-02-28"), - ("2021-03-01", 8, -1, "2021-02-28"), - ("1980/12/21", 8, 100, "1981/03/31"), - ("1980/12/21", 8, -100, "1980/09/12"), - ("1980/12/21", 8, 1000, "1983/09/17"), - ("1980/12/21", 8, -1000, "1978/03/27"), - ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"), - ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"), - ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"), - ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"), - ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"), - ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"), - ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"), - ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"), - ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"), - ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"), - ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"), - ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"), - ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"), - ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"), - ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"), - ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"), - ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"), - ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"), - ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"), - ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"), - ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"), - ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"), - ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"), - ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"), - ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"), - ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"), - ("24-Nov-2021", 0, 1, "25-Nov-2021"), - ("24-Nov-2021", 3, 1, "24-Dec-2021"), - ("24-Nov-2021", 7, 1, "24-Nov-2022"), - ("2021 Nov 24", 0, 1, "2022 Nov 24"), - ("2021 Nov 24", 5, 1, "2021 Dec 24"), - ("2021 Nov 24", 9, 1, "2021 Nov 25"), - ("Nov 24, 2021", 0, 1, "Dec 24, 2021"), - ("Nov 24, 2021", 4, 1, "Nov 25, 2021"), - ("Nov 24, 2021", 8, 1, "Nov 24, 2022"), - ("7:21:53 am", 0, 1, "8:21:53 am"), - ("7:21:53 am", 3, 1, "7:22:53 am"), - ("7:21:53 am", 5, 1, "7:21:54 am"), - ("7:21:53 am", 8, 1, "7:21:53 pm"), - ("7:21:53 AM", 0, 1, "8:21:53 AM"), - ("7:21:53 AM", 3, 1, "7:22:53 AM"), - ("7:21:53 AM", 5, 1, "7:21:54 AM"), - ("7:21:53 AM", 8, 1, "7:21:53 PM"), - ("7:21 am", 0, 1, "8:21 am"), - ("7:21 am", 3, 1, "7:22 am"), - ("7:21 am", 5, 1, "7:21 pm"), - ("7:21 AM", 0, 1, "8:21 AM"), - ("7:21 AM", 3, 1, "7:22 AM"), - ("7:21 AM", 5, 1, "7:21 PM"), - ("23:24:23", 1, 1, "00:24:23"), - ("23:24:23", 3, 1, "23:25:23"), - ("23:24:23", 6, 1, "23:24:24"), - ("23:24", 1, 1, "00:24"), - ("23:24", 3, 1, "23:25"), + ("2020-02-28", 1, "2020-02-29"), + ("2020-02-29", 1, "2020-03-01"), + ("2020-01-31", 1, "2020-02-01"), + ("2020-01-20", 1, "2020-01-21"), + ("2021-01-01", -1, "2020-12-31"), + ("2021-01-31", -2, "2021-01-29"), + ("2020-02-28", 1, "2020-02-29"), + ("2021-02-28", 1, "2021-03-01"), + ("2021-03-01", -1, "2021-02-28"), + ("2020-02-29", -1, "2020-02-28"), + ("2020-02-20", -1, "2020-02-19"), + ("2021-03-01", -1, "2021-02-28"), + ("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"), + ("2021-11-24 07:12:23", 1, "2021-11-24 07:13:23"), + ("2021-11-24 07:12", 1, "2021-11-24 07:13"), + ("Wed Nov 24 2021", 1, "Thu Nov 25 2021"), + ("24-Nov-2021", 1, "25-Nov-2021"), + ("2021 Nov 24", 1, "2021 Nov 25"), + ("Nov 24, 2021", 1, "Nov 25, 2021"), + ("7:21:53 am", 1, "7:22:53 am"), + ("7:21:53 AM", 1, "7:22:53 AM"), + ("7:21 am", 1, "7:22 am"), + ("23:24:23", 1, "23:25:23"), + ("23:24", 1, "23:25"), + ("23:59", 1, "00:00"), + ("23:59:59", 1, "00:00:59"), ]; - for (original, cursor, amount, expected) in tests { - let rope = Rope::from_str(original); - let range = Range::new(cursor, cursor + 1); - assert_eq!( - DateTimeIncrementor::from_range(rope.slice(..), range) - .unwrap() - .increment(amount) - .1, - Tendril::from(expected) - ); + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); } } @@ -482,10 +314,7 @@ mod test { ]; for invalid in tests { - let rope = Rope::from_str(invalid); - let range = Range::new(0, 1); - - assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None) + assert_eq!(increment(invalid, 1), None) } } } diff --git a/helix-core/src/increment/integer.rs b/helix-core/src/increment/integer.rs new file mode 100644 index 000000000..30803e175 --- /dev/null +++ b/helix-core/src/increment/integer.rs @@ -0,0 +1,235 @@ +const SEPARATOR: char = '_'; + +/// Increment an integer. +/// +/// Supported bases: +/// 2 with prefix 0b +/// 8 with prefix 0o +/// 10 with no prefix +/// 16 with prefix 0x +/// +/// An integer can contain `_` as a separator but may not start or end with a separator. +/// Base 10 integers can go negative, but bases 2, 8, and 16 cannot. +/// All addition and subtraction is saturating. +pub fn increment(selected_text: &str, amount: i64) -> Option { + if selected_text.is_empty() + || selected_text.ends_with(SEPARATOR) + || selected_text.starts_with(SEPARATOR) + { + return None; + } + + let radix = if selected_text.starts_with("0x") { + 16 + } else if selected_text.starts_with("0o") { + 8 + } else if selected_text.starts_with("0b") { + 2 + } else { + 10 + }; + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec = selected_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == SEPARATOR { Some(i) } else { None }) + .collect(); + + let word: String = selected_text.chars().filter(|&c| c != SEPARATOR).collect(); + + let mut new_text = if radix == 10 { + let number = &word; + let value = i128::from_str_radix(number, radix).ok()?; + let new_value = value.saturating_add(amount as i128); + + let format_length = match (value.is_negative(), new_value.is_negative()) { + (true, false) => number.len() - 1, + (false, true) => number.len() + 1, + _ => number.len(), + } - separator_rtl_indexes.len(); + + if number.starts_with('0') || number.starts_with("-0") { + format!("{:01$}", new_value, format_length) + } else { + format!("{}", new_value) + } + } else { + let number = &word[2..]; + let value = u128::from_str_radix(number, radix).ok()?; + let new_value = (value as i128).saturating_add(amount as i128); + let new_value = if new_value < 0 { 0 } else { new_value }; + let format_length = selected_text.len() - 2 - separator_rtl_indexes.len(); + + match radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 16 => { + let (lower_count, upper_count): (usize, usize) = + number.chars().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: {}", 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().saturating_sub(rtl_index); + if new_index > 0 { + new_text.insert(new_index, SEPARATOR); + } + } + } + + // Add in additional separators if necessary. + if new_text.len() > selected_text.len() && !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 radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find(SEPARATOR) { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, SEPARATOR); + } + } + } + + Some(new_text) +} + +#[cfg(test)] +mod test { + use super::*; + + #[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 { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_increment_basic_hexadecimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0x0000"), + ("0xffffffffffffffff", 1, "0x10000000000000000"), + ("0xffffffffffffffff", 2, "0x10000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[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, "0o0000"), + ("0o1777777777777777777777", 1, "0o2000000000000000000000"), + ("0o1777777777777777777777", 2, "0o2000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[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, "0b0000"), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b10000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b10000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[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", -1, "0x0000_0000"), + ("0x0000_0000_0000", -1, "0x0000_0000_0000"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + assert_eq!(increment(original, amount).unwrap(), expected); + } + } + + #[test] + fn test_leading_and_trailing_separators_arent_a_match() { + assert_eq!(increment("9_", 1), None); + assert_eq!(increment("_9", 1), None); + assert_eq!(increment("_9_", 1), None); + } +} diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs index f59457748..f1978bde4 100644 --- a/helix-core/src/increment/mod.rs +++ b/helix-core/src/increment/mod.rs @@ -1,8 +1,10 @@ -pub mod date_time; -pub mod number; +mod date_time; +mod integer; -use crate::{Range, Tendril}; +pub fn integer(selected_text: &str, amount: i64) -> Option { + integer::increment(selected_text, amount) +} -pub trait Increment { - fn increment(&self, amount: i64) -> (Range, Tendril); +pub fn date_time(selected_text: &str, amount: i64) -> Option { + date_time::increment(selected_text, amount) } diff --git a/helix-core/src/increment/number.rs b/helix-core/src/increment/number.rs deleted file mode 100644 index 912687293..000000000 --- a/helix-core/src/increment/number.rs +++ /dev/null @@ -1,507 +0,0 @@ -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 + usize::from(c.is_ascii_lowercase()), - upper + usize::from(c.is_ascii_uppercase()), - ) - }); - 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, - Tendril::from(expected) - ); - } - } - - #[test] - fn test_increment_basic_hexadecimal_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, - Tendril::from(expected) - ); - } - } - - #[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, - Tendril::from(expected) - ); - } - } - - #[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, - Tendril::from(expected) - ); - } - } - - #[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, - Tendril::from(expected) - ); - } - } -} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 09c2e5df9..e196e71ee 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -11,9 +11,7 @@ pub use typed::*; use helix_core::{ comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, - increment::date_time::DateTimeIncrementor, - increment::{number::NumberIncrementor, Increment}, - indent, + increment, indent, indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, @@ -5028,57 +5026,25 @@ enum IncrementDirection { Increase, Decrease, } -/// Increment object under cursor by count. + +/// Increment objects within selections by count. fn increment(cx: &mut Context) { increment_impl(cx, IncrementDirection::Increase); } -/// Decrement object under cursor by count. +/// Decrement objects within selections by count. fn decrement(cx: &mut Context) { increment_impl(cx, IncrementDirection::Decrease); } -/// This function differs from find_next_char_impl in that it stops searching at the newline, but also -/// starts searching at the current character, instead of the next. -/// It does not want to start at the next character because this function is used for incrementing -/// number and we don't want to move forward if we're already on a digit. -fn find_next_char_until_newline( - text: RopeSlice, - char_matcher: M, - pos: usize, - _count: usize, - _inclusive: bool, -) -> Option { - // Since we send the current line to find_nth_next instead of the whole text, we need to adjust - // the position we send to this function so that it's relative to that line and its returned - // position since it's expected this function returns a global position. - let line_index = text.char_to_line(pos); - let pos_delta = text.line_to_char(line_index); - let pos = pos - pos_delta; - search::find_nth_next(text.line(line_index), char_matcher, pos, 1).map(|pos| pos + pos_delta) -} - -/// Decrement object under cursor by `amount`. +/// Increment objects within selections by `amount`. +/// A negative `amount` will decrement objects within selections. fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) { - // TODO: when incrementing or decrementing a number that gets a new digit or lose one, the - // selection is updated improperly. - find_char_impl( - cx.editor, - &find_next_char_until_newline, - true, - true, - char::is_ascii_digit, - 1, - ); - - // Increase by 1 if `IncrementDirection` is `Increase` - // Decrease by 1 if `IncrementDirection` is `Decrease` let sign = match increment_direction { IncrementDirection::Increase => 1, IncrementDirection::Decrease => -1, }; let mut amount = sign * cx.count() as i64; - // If the register is `#` then increase or decrease the `amount` by 1 per element let increase_by = if cx.register == Some('#') { sign } else { 0 }; @@ -5086,55 +5052,40 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) { let selection = doc.selection(view.id); let text = doc.text().slice(..); - let changes: Vec<_> = selection - .ranges() - .iter() - .filter_map(|range| { - let incrementor: Box = - if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) { - Box::new(incrementor) - } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) { - Box::new(incrementor) - } else { - return None; - }; + let mut new_selection_ranges = SmallVec::new(); + let mut cumulative_length_diff: i128 = 0; + let mut changes = vec![]; - let (range, new_text) = incrementor.increment(amount); + for range in selection { + let selected_text: Cow = range.fragment(text); + let new_from = ((range.from() as i128) + cumulative_length_diff) as usize; + let incremented = [increment::integer, increment::date_time] + .iter() + .find_map(|incrementor| incrementor(selected_text.as_ref(), amount)); - amount += increase_by; + amount += increase_by; - Some((range.from(), range.to(), Some(new_text))) - }) - .collect(); - - // Overlapping changes in a transaction will panic, so we need to find and remove them. - // For example, if there are cursors on each of the year, month, and day of `2021-11-29`, - // incrementing will give overlapping changes, with each change incrementing a different part of - // the date. Since these conflict with each other we remove these changes from the transaction - // so nothing happens. - let mut overlapping_indexes = HashSet::new(); - for (i, changes) in changes.windows(2).enumerate() { - if changes[0].1 > changes[1].0 { - overlapping_indexes.insert(i); - overlapping_indexes.insert(i + 1); + match incremented { + None => { + let new_range = Range::new( + new_from, + (range.to() as i128 + cumulative_length_diff) as usize, + ); + new_selection_ranges.push(new_range); + } + Some(new_text) => { + let new_range = Range::new(new_from, new_from + new_text.len()); + cumulative_length_diff += new_text.len() as i128 - selected_text.len() as i128; + new_selection_ranges.push(new_range); + changes.push((range.from(), range.to(), Some(new_text.into()))); + } } } - let changes: Vec<_> = changes - .into_iter() - .enumerate() - .filter_map(|(i, change)| { - if overlapping_indexes.contains(&i) { - None - } else { - Some(change) - } - }) - .collect(); if !changes.is_empty() { + let new_selection = Selection::new(new_selection_ranges, selection.primary_index()); let transaction = Transaction::change(doc.text(), changes.into_iter()); - let transaction = transaction.with_selection(selection.clone()); - + let transaction = transaction.with_selection(new_selection); apply_transaction(&transaction, doc, view); } } From 3cf5216dbd1b9c767e6a922f93f1a3b1a2955d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikodem=20Rabuli=C5=84ski?= Date: Mon, 16 Jan 2023 17:41:22 +0100 Subject: [PATCH 8/8] Commit to history after executing a command from the palette (#5294) --- helix-term/src/commands.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e196e71ee..7df53a48a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2504,7 +2504,22 @@ pub fn command_palette(cx: &mut Context) { on_next_key_callback: None, jobs: cx.jobs, }; + let focus = view!(ctx.editor).id; + command.execute(&mut ctx); + + if ctx.editor.tree.contains(focus) { + let config = ctx.editor.config(); + let mode = ctx.editor.mode(); + let view = view_mut!(ctx.editor, focus); + let doc = doc_mut!(ctx.editor, &view.doc); + + view.ensure_cursor_in_view(doc, config.scrolloff); + + if mode != Mode::Insert { + doc.append_changes_to_history(view); + } + } }); compositor.push(Box::new(overlayed(picker))); },