From 03f35af9c153b1d0db1bd26eecd69bade55e23fd Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 6 Jan 2025 10:32:51 -0500 Subject: [PATCH 01/23] Format '--version' calendar version as 'YY.0M' We use calendar versioning which isn't supported by Cargo, so we need to add an extra leading zero to the month for releases between January and September to match our usual 'YY.0M' formatting. Closes #12414 --- helix-loader/build.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/helix-loader/build.rs b/helix-loader/build.rs index cfa3a9ad..22f2fa8f 100644 --- a/helix-loader/build.rs +++ b/helix-loader/build.rs @@ -6,14 +6,6 @@ const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR"); const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR"); const PATCH: &str = env!("CARGO_PKG_VERSION_PATCH"); -fn get_calver() -> String { - if PATCH == "0" { - format!("{MAJOR}.{MINOR}") - } else { - format!("{MAJOR}.{MINOR}.{PATCH}") - } -} - fn main() { let git_hash = Command::new("git") .args(["rev-parse", "HEAD"]) @@ -23,7 +15,17 @@ fn main() { .and_then(|x| String::from_utf8(x.stdout).ok()) .or_else(|| option_env!("HELIX_NIX_BUILD_REV").map(|s| s.to_string())); - let calver = get_calver(); + let minor = if MINOR.len() == 1 { + // Print single-digit months in '0M' format + format!("0{MINOR}") + } else { + MINOR.to_string() + }; + let calver = if PATCH == "0" { + format!("{MAJOR}.{minor}") + } else { + format!("{MAJOR}.{minor}.{PATCH}") + }; let version: Cow<_> = match &git_hash { Some(git_hash) => format!("{} ({})", calver, &git_hash[..8]).into(), None => calver.into(), From 217818681ea9bbc7f995c87f8794c46eeb012b1c Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 6 Jan 2025 12:39:53 -0500 Subject: [PATCH 02/23] Revert "refactor(shellwords)!: change arg handling strategy (#11149)" This reverts commit 64b38d1a28dfce4dabb502d395a42a842ec03ee9. --- helix-core/src/shellwords.rs | 1005 +++++++++--------------------- helix-term/src/commands.rs | 36 +- helix-term/src/commands/dap.rs | 2 - helix-term/src/commands/typed.rs | 725 ++++++++++++--------- helix-term/src/keymap.rs | 12 +- 5 files changed, 725 insertions(+), 1055 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index edfd9ad1..9d873c36 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -1,358 +1,6 @@ -use smartstring::{LazyCompact, SmartString}; use std::borrow::Cow; -/// A utility for parsing shell-like command lines. -/// -/// The `Shellwords` struct takes an input string and allows extracting the command and its arguments. -/// -/// # Features -/// -/// - Parses command and arguments from input strings. -/// - Supports single, double, and backtick quoted arguments. -/// - Respects backslash escaping in arguments. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use helix_core::shellwords::Shellwords; -/// let shellwords = Shellwords::from(":o helix-core/src/shellwords.rs"); -/// assert_eq!(":o", shellwords.command()); -/// assert_eq!("helix-core/src/shellwords.rs", shellwords.args().next().unwrap()); -/// ``` -/// -/// Empty command: -/// -/// ``` -/// # use helix_core::shellwords::Shellwords; -/// let shellwords = Shellwords::from(" "); -/// assert!(shellwords.command().is_empty()); -/// ``` -/// -/// # Iterator -/// -/// The `args` method returns a non-allocating iterator, `Args`, over the arguments of the input. -/// -/// ``` -/// # use helix_core::shellwords::Shellwords; -/// let shellwords = Shellwords::from(":o a b c"); -/// let mut args = shellwords.args(); -/// assert_eq!(Some("a"), args.next()); -/// assert_eq!(Some("b"), args.next()); -/// assert_eq!(Some("c"), args.next()); -/// assert_eq!(None, args.next()); -/// ``` -#[derive(Clone, Copy)] -pub struct Shellwords<'a> { - input: &'a str, -} - -impl<'a> From<&'a str> for Shellwords<'a> { - #[inline] - fn from(input: &'a str) -> Self { - Self { input } - } -} - -impl<'a> From<&'a String> for Shellwords<'a> { - #[inline] - fn from(input: &'a String) -> Self { - Self { input } - } -} - -impl<'a> From<&'a Cow<'a, str>> for Shellwords<'a> { - #[inline] - fn from(input: &'a Cow) -> Self { - Self { input } - } -} - -impl<'a> Shellwords<'a> { - #[inline] - #[must_use] - pub fn command(&self) -> &str { - self.input - .split_once(' ') - .map_or(self.input, |(command, _)| command) - } - - #[inline] - #[must_use] - pub fn args(&self) -> Args<'a> { - let args = self.input.split_once(' ').map_or("", |(_, args)| args); - Args::parse(args) - } - - #[inline] - pub fn input(&self) -> &str { - self.input - } - - /// Checks that the input ends with a whitespace character which is not escaped. - /// - /// # Examples - /// - /// ```rust - /// # use helix_core::shellwords::Shellwords; - /// assert_eq!(Shellwords::from(" ").ends_with_whitespace(), true); - /// assert_eq!(Shellwords::from(":open ").ends_with_whitespace(), true); - /// assert_eq!(Shellwords::from(":open foo.txt ").ends_with_whitespace(), true); - /// assert_eq!(Shellwords::from(":open").ends_with_whitespace(), false); - /// assert_eq!(Shellwords::from(":open a\\ ").ends_with_whitespace(), true); - /// assert_eq!(Shellwords::from(":open a\\ b.txt").ends_with_whitespace(), false); - /// ``` - #[inline] - pub fn ends_with_whitespace(&self) -> bool { - self.input.ends_with(' ') - } -} - -/// An iterator over an input string which yields arguments. -/// -/// Splits on whitespace, but respects quoted substrings (using double quotes, single quotes, or backticks). -#[derive(Debug, Clone)] -pub struct Args<'a> { - input: &'a str, - idx: usize, - start: usize, -} - -impl<'a> Args<'a> { - #[inline] - fn parse(input: &'a str) -> Self { - Self { - input, - idx: 0, - start: 0, - } - } - - #[inline] - pub fn is_empty(&self) -> bool { - self.input.is_empty() - } - - /// Returns the args exactly as input. - /// - /// # Examples - /// ``` - /// # use helix_core::shellwords::Args; - /// let args = Args::from(r#"sed -n "s/test t/not /p""#); - /// assert_eq!(r#"sed -n "s/test t/not /p""#, args.raw()); - /// - /// let args = Args::from(r#"cat "file name with space.txt""#); - /// assert_eq!(r#"cat "file name with space.txt""#, args.raw()); - /// ``` - #[inline] - pub fn raw(&self) -> &str { - self.input - } - - /// Returns the remainder of the args exactly as input. - /// - /// # Examples - /// ``` - /// # use helix_core::shellwords::Args; - /// let mut args = Args::from(r#"sed -n "s/test t/not /p""#); - /// assert_eq!("sed", args.next().unwrap()); - /// assert_eq!(r#"-n "s/test t/not /p""#, args.rest()); - /// ``` - /// - /// Never calling `next` and using `rest` is functionally equivalent to calling `raw`. - #[inline] - pub fn rest(&self) -> &str { - &self.input[self.idx..] - } - - /// Returns a reference to the `next()` value without advancing the iterator. - /// - /// Unlike `std::iter::Peakable::peek` this does not return a double reference, `&&str` - /// but a normal `&str`. - #[inline] - #[must_use] - pub fn peek(&self) -> Option<&str> { - self.clone().next() - } - - /// Returns the total number of arguments given in a command. - /// - /// This count is aware of all parsing rules for `Args`. - #[must_use] - pub fn arg_count(&self) -> usize { - Self { - input: self.input, - idx: 0, - start: 0, - } - .fold(0, |acc, _| acc + 1) - } - - /// Convenient function to return an empty `Args`. - /// - /// When used in any iteration, it will always return `None`. - #[inline(always)] - pub const fn empty() -> Self { - Self { - input: "", - idx: 0, - start: 0, - } - } -} - -impl<'a> Iterator for Args<'a> { - type Item = &'a str; - - #[inline] - #[allow(clippy::too_many_lines)] - fn next(&mut self) -> Option { - // The parser loop is split into three main blocks to handle different types of input processing: - // - // 1. Quote block: - // - Detects an unescaped quote character, either starting an in-quote scan or, if already in-quote, - // locating the closing quote to return the quoted argument. - // - Handles cases where mismatched quotes are ignored and when quotes appear as the last character. - // - // 2. Whitespace block: - // - Handles arguments separated by whitespace (space or tab), respecting quotes so quoted phrases - // remain grouped together. - // - Splits arguments by whitespace when outside of a quoted context and updates boundaries accordingly. - // - // 3. Catch-all block: - // - Handles any other character, updating the `is_escaped` status if a backslash is encountered, - // advancing the loop to the next character. - - let bytes = self.input.as_bytes(); - let mut in_quotes = false; - let mut quote = b'\0'; - let mut is_escaped = false; - - while self.idx < bytes.len() { - match bytes[self.idx] { - b'"' | b'\'' | b'`' if !is_escaped => { - if in_quotes { - // Found the proper closing quote, so can return the arg and advance the state along. - if bytes[self.idx] == quote { - let arg = Some(&self.input[self.start..self.idx]); - self.idx += 1; - self.start = self.idx; - return arg; - } - // If quote does not match the type of the opening quote, then do nothing and advance. - self.idx += 1; - } else if self.idx == bytes.len() - 1 { - // Special case for when a quote is the last input in args. - // e.g: :read "file with space.txt"" - // This preserves the quote as an arg: - // - `file with space` - // - `"` - let arg = Some(&self.input[self.idx..]); - self.idx = bytes.len(); - self.start = bytes.len(); - return arg; - } else { - // Found opening quote. - in_quotes = true; - // Kind of quote that was found. - quote = bytes[self.idx]; - - if self.start < self.idx { - // When part of the input ends in a quote, `one two" three`, this properly returns the `two` - // before advancing to the quoted arg for the next iteration: - // - `one` <- previous arg - // - `two` <- this step - // - ` three` <- next arg - let arg = Some(&self.input[self.start..self.idx]); - self.idx += 1; - self.start = self.idx; - return arg; - } - - // Advance after quote. - self.idx += 1; - // Exclude quote from arg output. - self.start = self.idx; - } - } - b' ' | b'\t' if !in_quotes => { - // Found a true whitespace separator that wasn't inside quotes. - - // Check if there is anything to return or if its just advancing over whitespace. - // `start` will only be less than `idx` when there is something to return. - if self.start < self.idx { - let arg = Some(&self.input[self.start..self.idx]); - self.idx += 1; - self.start = self.idx; - return arg; - } - - // Advance beyond the whitespace. - self.idx += 1; - - // This is where `start` will be set to the start of an arg boundary, either encountering a word - // boundary or a quote boundary. If it finds a quote, then it will be advanced again in that part - // of the code. Either way, all that remains for the check above will be to return a full arg. - self.start = self.idx; - } - _ => { - // If previous loop didn't find any backslash and was already escaped it will change to false - // as the backslash chain was broken. - // - // If the previous loop had no backslash escape, and found one this iteration, then its the start - // of an escape chain. - is_escaped = match (is_escaped, bytes[self.idx]) { - (false, b'\\') => true, // Set `is_escaped` if the current byte is a backslash - _ => false, //Reset `is_escaped` if it was true, otherwise keep `is_escaped` as false - }; - - // Advance to next `char`. - self.idx += 1; - } - } - } - - // Fallback that catches when the loop would have exited but failed to return the arg between start and the end. - if self.start < bytes.len() { - let arg = Some(&self.input[self.start..]); - self.start = bytes.len(); - return arg; - } - - // All args have been parsed. - None - } - - fn count(self) -> usize - where - Self: Sized, - { - panic!("use `arg_count` instead to get the number of arguments."); - } -} - -impl<'a> From<&'a String> for Args<'a> { - fn from(args: &'a String) -> Self { - Args::parse(args) - } -} - -impl<'a> From<&'a str> for Args<'a> { - fn from(args: &'a str) -> Self { - Args::parse(args) - } -} - -impl<'a> From<&'a Cow<'_, str>> for Args<'a> { - fn from(args: &'a Cow) -> Self { - Args::parse(args) - } -} - /// Auto escape for shellwords usage. -#[inline] -#[must_use] pub fn escape(input: Cow) -> Cow { if !input.chars().any(|x| x.is_ascii_whitespace()) { input @@ -365,141 +13,186 @@ pub fn escape(input: Cow) -> Cow { buf })) } else { - Cow::Owned(format!("\"{input}\"")) + Cow::Owned(format!("\"{}\"", input)) } } -/// Unescapes a string, converting escape sequences into their literal characters. -/// -/// This function handles the following escape sequences: -/// - `\\n` is converted to `\n` (newline) -/// - `\\t` is converted to `\t` (tab) -/// - `\\u{...}` is converted to the corresponding Unicode character -/// -/// Other escape sequences, such as `\\` followed by any character not listed above, will remain unchanged. -/// -/// If input is invalid, for example if there is invalid unicode, \u{999999999}, it will return the input as is. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use helix_core::shellwords::unescape; -/// let unescaped = unescape("hello\\nworld"); -/// assert_eq!("hello\nworld", unescaped); -/// ``` -/// -/// Unescaping tabs: -/// -/// ``` -/// # use helix_core::shellwords::unescape; -/// let unescaped = unescape("hello\\tworld"); -/// assert_eq!("hello\tworld", unescaped); -/// ``` -/// -/// Unescaping Unicode characters: -/// -/// ``` -/// # use helix_core::shellwords::unescape; -/// let unescaped = unescape("hello\\u{1f929}world"); -/// assert_eq!("hello\u{1f929}world", unescaped); -/// assert_eq!("hello🤩world", unescaped); -/// ``` -/// -/// Handling backslashes: -/// -/// ``` -/// # use helix_core::shellwords::unescape; -/// let unescaped = unescape(r"hello\\world"); -/// assert_eq!(r"hello\\world", unescaped); -/// -/// let unescaped = unescape(r"hello\\\\world"); -/// assert_eq!(r"hello\\\\world", unescaped); -/// ``` -/// -/// # Note -/// -/// This function is opinionated, with a clear purpose of handling user input, not a general or generic unescaping utility, and does not unescape sequences like `\\'` or `\\\"`, leaving them as is. -#[inline] -#[must_use] -pub fn unescape(input: &str) -> Cow<'_, str> { - enum State { - Normal, - Escaped, - Unicode, - } +enum State { + OnWhitespace, + Unquoted, + UnquotedEscaped, + Quoted, + QuoteEscaped, + Dquoted, + DquoteEscaped, +} - let mut unescaped = String::new(); - let mut state = State::Normal; - let mut is_escaped = false; - // NOTE: Max unicode code point is U+10FFFF for a maximum of 6 chars - let mut unicode = SmartString::::new_const(); +pub struct Shellwords<'a> { + state: State, + /// Shellwords where whitespace and escapes has been resolved. + words: Vec>, + /// The parts of the input that are divided into shellwords. This can be + /// used to retrieve the original text for a given word by looking up the + /// same index in the Vec as the word in `words`. + parts: Vec<&'a str>, +} - for (idx, ch) in input.char_indices() { - match state { - State::Normal => match ch { - '\\' => { - if !is_escaped { - // PERF: As not every separator will be escaped, we use `String::new` as that has no initial - // allocation. If an escape is found, then we reserve capacity thats the len of the separator, - // as the new unescaped string will be at least that long. - unescaped.reserve(input.len()); - if idx > 0 { - // First time finding an escape, so all prior chars can be added to the new unescaped - // version if its not the very first char found. - unescaped.push_str(&input[0..idx]); +impl<'a> From<&'a str> for Shellwords<'a> { + fn from(input: &'a str) -> Self { + use State::*; + + let mut state = Unquoted; + let mut words = Vec::new(); + let mut parts = Vec::new(); + let mut escaped = String::with_capacity(input.len()); + + let mut part_start = 0; + let mut unescaped_start = 0; + let mut end = 0; + + for (i, c) in input.char_indices() { + state = match state { + OnWhitespace => match c { + '"' => { + end = i; + Dquoted + } + '\'' => { + end = i; + Quoted + } + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + UnquotedEscaped + } else { + OnWhitespace } } - state = State::Escaped; - is_escaped = true; - } - _ => { - if is_escaped { - unescaped.push(ch); + c if c.is_ascii_whitespace() => { + end = i; + OnWhitespace } - } - }, - State::Escaped => { - match ch { - 'n' => unescaped.push('\n'), - 't' => unescaped.push('\t'), - 'u' => { - state = State::Unicode; - continue; + _ => Unquoted, + }, + Unquoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + UnquotedEscaped + } else { + Unquoted + } } - // Uncomment if you want to handle '\\' to '\' - // '\\' => unescaped.push('\\'), - _ => { - unescaped.push('\\'); - unescaped.push(ch); + c if c.is_ascii_whitespace() => { + end = i; + OnWhitespace } - } - state = State::Normal; + _ => Unquoted, + }, + UnquotedEscaped => Unquoted, + Quoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + QuoteEscaped + } else { + Quoted + } + } + '\'' => { + end = i; + OnWhitespace + } + _ => Quoted, + }, + QuoteEscaped => Quoted, + Dquoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + DquoteEscaped + } else { + Dquoted + } + } + '"' => { + end = i; + OnWhitespace + } + _ => Dquoted, + }, + DquoteEscaped => Dquoted, + }; + + let c_len = c.len_utf8(); + if i == input.len() - c_len && end == 0 { + end = i + c_len; } - State::Unicode => match ch { - '{' => continue, - '}' => { - let Ok(digit) = u32::from_str_radix(&unicode, 16) else { - return input.into(); - }; - let Some(point) = char::from_u32(digit) else { - return input.into(); - }; - unescaped.push(point); - // Might be more unicode to unescape so clear for reuse. - unicode.clear(); - state = State::Normal; + + if end > 0 { + let esc_trim = escaped.trim(); + let inp = &input[unescaped_start..end]; + + if !(esc_trim.is_empty() && inp.trim().is_empty()) { + if esc_trim.is_empty() { + words.push(inp.into()); + parts.push(inp); + } else { + words.push([escaped, inp.into()].concat().into()); + parts.push(&input[part_start..end]); + escaped = "".to_string(); + } } - _ => unicode.push(ch), - }, + unescaped_start = i + 1; + part_start = i + 1; + end = 0; + } + } + + debug_assert!(words.len() == parts.len()); + + Self { + state, + words, + parts, } } +} - if is_escaped { - unescaped.into() - } else { - input.into() +impl<'a> Shellwords<'a> { + /// Checks that the input ends with a whitespace character which is not escaped. + /// + /// # Examples + /// + /// ```rust + /// use helix_core::shellwords::Shellwords; + /// assert_eq!(Shellwords::from(" ").ends_with_whitespace(), true); + /// assert_eq!(Shellwords::from(":open ").ends_with_whitespace(), true); + /// assert_eq!(Shellwords::from(":open foo.txt ").ends_with_whitespace(), true); + /// assert_eq!(Shellwords::from(":open").ends_with_whitespace(), false); + /// #[cfg(unix)] + /// assert_eq!(Shellwords::from(":open a\\ ").ends_with_whitespace(), false); + /// #[cfg(unix)] + /// assert_eq!(Shellwords::from(":open a\\ b.txt").ends_with_whitespace(), false); + /// ``` + pub fn ends_with_whitespace(&self) -> bool { + matches!(self.state, State::OnWhitespace) + } + + /// Returns the list of shellwords calculated from the input string. + pub fn words(&self) -> &[Cow<'a, str>] { + &self.words + } + + /// Returns a list of strings which correspond to [`Self::words`] but represent the original + /// text in the input string - including escape characters - without separating whitespace. + pub fn parts(&self) -> &[&'a str] { + &self.parts } } @@ -508,202 +201,114 @@ mod test { use super::*; #[test] - fn base() { + #[cfg(windows)] + fn test_normal() { let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; let shellwords = Shellwords::from(input); - let args = vec![ - "single_word", - "twó", - "wörds", - r"\three\", - r#"\"with\"#, - r"escaping\\", - ]; - - assert_eq!(":o", shellwords.command()); - assert_eq!(args, shellwords.args().collect::>()); - } - - #[test] - fn should_have_empty_args() { - let shellwords = Shellwords::from(":quit"); - assert!( - shellwords.args().is_empty(), - "args: `{}`", - shellwords.args().next().unwrap() - ); - assert!(shellwords.args().next().is_none()); - } - - #[test] - fn should_return_empty_command() { - let shellwords = Shellwords::from(" "); - assert!(shellwords.command().is_empty()); - } - - #[test] - fn should_support_unicode_args() { - assert_eq!( - Shellwords::from(":sh echo 𒀀").args().collect::>(), - &["echo", "𒀀"] - ); - assert_eq!( - Shellwords::from(":sh echo 𒀀 hello world𒀀") - .args() - .collect::>(), - &["echo", "𒀀", "hello", "world𒀀"] - ); - } - - #[test] - fn should_preserve_quote_if_last_argument() { - let sh = Shellwords::from(r#":read "file with space.txt"""#); - let mut args = sh.args(); - assert_eq!("file with space.txt", args.next().unwrap()); - assert_eq!(r#"""#, args.next().unwrap()); - } - - #[test] - fn should_return_rest_of_non_closed_quote_as_one_argument() { - let sh = Shellwords::from(r":rename 'should be one \'argument"); - assert_eq!(r"should be one \'argument", sh.args().next().unwrap()); - } - - #[test] - fn should_respect_escaped_quote_in_what_looks_like_non_closed_arg() { - let sh = Shellwords::from(r":rename 'should be one \\'argument"); - let mut args = sh.args(); - assert_eq!(r"should be one \\", args.next().unwrap()); - assert_eq!(r"argument", args.next().unwrap()); - } - - #[test] - fn should_split_args() { - assert_eq!(Shellwords::from(":o a").args().collect::>(), &["a"]); - assert_eq!( - Shellwords::from(":o a\\ ").args().collect::>(), - &["a\\"] - ); - } - - #[test] - fn should_parse_args_even_with_leading_whitespace() { - // Three spaces - assert_eq!( - Shellwords::from(":o a").args().collect::>(), - &["a"] - ); - } - - #[test] - fn should_peek_next_arg_and_not_consume() { - let mut args = Shellwords::from(":o a").args(); - - assert_eq!(Some("a"), args.peek()); - assert_eq!(Some("a"), args.next()); - assert_eq!(None, args.next()); - } - - #[test] - fn should_parse_single_quotes_while_respecting_escapes() { - let quoted = - r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#; - let shellwords = Shellwords::from(quoted); - let result = shellwords.args().collect::>(); + let result = shellwords.words().to_vec(); let expected = vec![ - "single_word", - "twó wörds", - "", - " ", - r#"\three\' \"with\ escaping\\"#, - "quote incomplete", + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó"), + Cow::from("wörds"), + Cow::from("\\three\\"), + Cow::from("\\"), + Cow::from("with\\ escaping\\\\"), ]; + // TODO test is_owned and is_borrowed, once they get stabilized. assert_eq!(expected, result); } - #[test] - fn should_parse_double_quotes_while_respecting_escapes() { - let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#; - let shellwords = Shellwords::from(dquoted); - let result = shellwords.args().collect::>(); - let expected = vec![ - "single_word", - "twó wörds", - "", - " ", - r#"\three\' \"with\ escaping\\"#, - "dquote incomplete", - ]; - assert_eq!(expected, result); - } - - #[test] - fn should_respect_escapes_with_mixed_quotes() { - let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#; - let shellwords = Shellwords::from(dquoted); - let result = shellwords.args().collect::>(); - let expected = vec![ - "single_word", - "twó wörds", - r#"\three\' \"with\ escaping\\"#, - "no space before", - "and after", - "$#%^@", - "%^&(%^", - r")(*&^%", - r"a\\\\\b", - // Last ' is important, as if the user input an accidental quote at the end, this should be checked in - // commands where there should only be one input and return an error rather than silently succeed. - "'", - ]; - assert_eq!(expected, result); - } - - #[test] - fn should_return_rest() { - let input = r#":set statusline.center ["file-type","file-encoding"]"#; - let shellwords = Shellwords::from(input); - let mut args = shellwords.args(); - assert_eq!(":set", shellwords.command()); - assert_eq!(Some("statusline.center"), args.next()); - assert_eq!(r#"["file-type","file-encoding"]"#, args.rest()); - } - - #[test] - fn should_return_no_args() { - let mut args = Args::parse(""); - assert!(args.next().is_none()); - assert!(args.is_empty()); - assert!(args.arg_count() == 0); - } - - #[test] - fn should_leave_escaped_quotes() { - let input = r#"\" \` \' \"with \'with \`with"#; - let result = Args::parse(input).collect::>(); - assert_eq!(r#"\""#, result[0]); - assert_eq!(r"\`", result[1]); - assert_eq!(r"\'", result[2]); - assert_eq!(r#"\"with"#, result[3]); - assert_eq!(r"\'with", result[4]); - assert_eq!(r"\`with", result[5]); - } - - #[test] - fn should_leave_literal_newline_alone() { - let result = Args::parse(r"\n").collect::>(); - assert_eq!(r"\n", result[0]); - } - - #[test] - fn should_leave_literal_unicode_alone() { - let result = Args::parse(r"\u{C}").collect::>(); - assert_eq!(r"\u{C}", result[0]); - } - #[test] #[cfg(unix)] - fn should_escape_unix() { + fn test_normal() { + let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; + let shellwords = Shellwords::from(input); + let result = shellwords.words().to_vec(); + 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] + #[cfg(unix)] + fn test_quoted() { + let quoted = + r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#; + let shellwords = Shellwords::from(quoted); + let result = shellwords.words().to_vec(); + 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] + #[cfg(unix)] + fn test_dquoted() { + let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#; + let shellwords = Shellwords::from(dquoted); + let result = shellwords.words().to_vec(); + 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] + #[cfg(unix)] + fn test_mixed() { + let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#; + let shellwords = Shellwords::from(dquoted); + let result = shellwords.words().to_vec(); + 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); + } + + #[test] + fn test_lists() { + let input = + r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "quotes"]'"#; + let shellwords = Shellwords::from(input); + let result = shellwords.words().to_vec(); + let expected = vec![ + Cow::from(":set"), + Cow::from("statusline.center"), + Cow::from(r#"["file-type","file-encoding"]"#), + Cow::from(r#"["list", "in", "quotes"]"#), + ]; + assert_eq!(expected, result); + } + + #[test] + #[cfg(unix)] + fn test_escaping_unix() { assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar")); assert_eq!(escape("foo bar".into()), Cow::Borrowed("foo\\ bar")); assert_eq!(escape("foo\tbar".into()), Cow::Borrowed("foo\\\tbar")); @@ -711,79 +316,35 @@ mod test { #[test] #[cfg(windows)] - fn should_escape_windows() { + fn test_escaping_windows() { assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar")); assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\"")); } #[test] - fn should_unescape_newline() { - let unescaped = unescape("hello\\nworld"); - assert_eq!("hello\nworld", unescaped); + #[cfg(unix)] + fn test_parts() { + assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]); + assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\ "]); } #[test] - fn should_unescape_tab() { - let unescaped = unescape("hello\\tworld"); - assert_eq!("hello\tworld", unescaped); + #[cfg(windows)] + fn test_parts() { + assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]); + assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\"]); } #[test] - fn should_unescape_unicode() { - let unescaped = unescape("hello\\u{1f929}world"); - assert_eq!("hello\u{1f929}world", unescaped, "char: 🤩 "); - assert_eq!("hello🤩world", unescaped); - } - - #[test] - fn should_return_original_input_due_to_bad_unicode() { - let unescaped = unescape("hello\\u{999999999}world"); - assert_eq!("hello\\u{999999999}world", unescaped); - } - - #[test] - fn should_not_unescape_slash() { - let unescaped = unescape(r"hello\\world"); - assert_eq!(r"hello\\world", unescaped); - - let unescaped = unescape(r"hello\\\\world"); - assert_eq!(r"hello\\\\world", unescaped); - } - - #[test] - fn should_not_unescape_slash_single_quote() { - let unescaped = unescape("\\'"); - assert_eq!(r"\'", unescaped); - } - - #[test] - fn should_not_unescape_slash_double_quote() { - let unescaped = unescape("\\\""); - assert_eq!(r#"\""#, unescaped); - } - - #[test] - fn should_not_change_anything() { - let unescaped = unescape("'"); - assert_eq!("'", unescaped); - let unescaped = unescape(r#"""#); - assert_eq!(r#"""#, unescaped); - } - - #[test] - fn should_only_unescape_newline_not_slash_single_quote() { - let unescaped = unescape("\\n\'"); - assert_eq!("\n'", unescaped); - let unescaped = unescape("\\n\\'"); - assert_eq!("\n\\'", unescaped); - } - - #[test] - fn should_unescape_args() { - // 1f929: 🤩 - let args = Args::parse(r#"'hello\u{1f929} world' '["hello", "\u{1f929}", "world"]'"#) - .collect::>(); - assert_eq!("hello\u{1f929} world", unescape(args[0])); - assert_eq!(r#"["hello", "🤩", "world"]"#, unescape(args[1])); + fn test_multibyte_at_end() { + assert_eq!(Shellwords::from("𒀀").parts(), &["𒀀"]); + assert_eq!( + Shellwords::from(":sh echo 𒀀").parts(), + &[":sh", "echo", "𒀀"] + ); + assert_eq!( + Shellwords::from(":sh echo 𒀀 hello world𒀀").parts(), + &[":sh", "echo", "𒀀", "hello", "world𒀀"] + ); } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 06d892ad..a93fa445 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -30,9 +30,7 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex}, search::{self, CharMatcher}, - selection, - shellwords::{self, Args}, - surround, + selection, shellwords, surround, syntax::{BlockCommentToken, LanguageServerFeature}, text_annotations::{Overlay, TextAnnotations}, textobject, @@ -209,7 +207,7 @@ use helix_view::{align_view, Align}; pub enum MappableCommand { Typable { name: String, - args: String, + args: Vec, doc: String, }, Static { @@ -244,17 +242,15 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { + let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, jobs: cx.jobs, scroll: None, }; - - if let Err(err) = - (command.fun)(&mut cx, Args::from(args), PromptEvent::Validate) - { - cx.editor.set_error(format!("{err}")); + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { + cx.editor.set_error(format!("{}", e)); } } } @@ -625,15 +621,21 @@ impl std::str::FromStr for MappableCommand { fn from_str(s: &str) -> Result { if let Some(suffix) = s.strip_prefix(':') { - let (name, args) = suffix.split_once(' ').unwrap_or((suffix, "")); + let mut typable_command = suffix.split(' ').map(|arg| arg.trim()); + let name = typable_command + .next() + .ok_or_else(|| anyhow!("Expected typable command name"))?; + let args = typable_command + .map(|s| s.to_owned()) + .collect::>(); typed::TYPABLE_COMMAND_MAP .get(name) .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), doc: format!(":{} {:?}", cmd.name, args), - args: args.to_string(), + args, }) - .ok_or_else(|| anyhow!("No TypableCommand named '{}'", name)) + .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s)) } else if let Some(suffix) = s.strip_prefix('@') { helix_view::input::parse_macro(suffix).map(|keys| Self::Macro { name: s.to_string(), @@ -3252,7 +3254,7 @@ pub fn command_palette(cx: &mut Context) { .iter() .map(|cmd| MappableCommand::Typable { name: cmd.name.to_owned(), - args: String::new(), + args: Vec::new(), doc: cmd.doc.to_owned(), }), ); @@ -4326,19 +4328,13 @@ fn yank_joined_impl(editor: &mut Editor, separator: &str, register: char) { let (view, doc) = current!(editor); let text = doc.text().slice(..); - let separator = if separator.is_empty() { - doc.line_ending.as_str() - } else { - separator - }; - let selection = doc.selection(view.id); let selections = selection.len(); let joined = selection .fragments(text) .fold(String::new(), |mut acc, fragment| { if !acc.is_empty() { - acc.push_str(&shellwords::unescape(separator)); + acc.push_str(separator); } acc.push_str(&fragment); acc diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index a35fa23a..83dd936c 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -109,7 +109,6 @@ fn dap_callback( jobs.callback(callback); } -// TODO: transition to `shellwords::Args` instead of `Option>>` pub fn dap_start_impl( cx: &mut compositor::Context, name: Option<&str>, @@ -313,7 +312,6 @@ pub fn dap_restart(cx: &mut Context) { ); } -// TODO: transition to `shellwords::Args` instead of `Vec` fn debug_parameter_prompt( completions: Vec, config_name: String, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ffe58adf..078bb800 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -13,7 +13,6 @@ use helix_stdx::path::home_dir; use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::editor::{CloseError, ConfigEvent}; use serde_json::Value; -use shellwords::Args; use ui::completers::{self, Completer}; #[derive(Clone)] @@ -22,17 +21,17 @@ pub struct TypableCommand { pub aliases: &'static [&'static str], pub doc: &'static str, // params, flags, helper, completer - pub fun: fn(&mut compositor::Context, Args, PromptEvent) -> anyhow::Result<()>, + pub fun: fn(&mut compositor::Context, &[Cow], PromptEvent) -> anyhow::Result<()>, /// What completion methods, if any, does this command have? pub signature: CommandSignature, } impl TypableCommand { fn completer_for_argument_number(&self, n: usize) -> &Completer { - self.signature - .positional_args - .get(n) - .unwrap_or(&self.signature.var_args) + match self.signature.positional_args.get(n) { + Some(completer) => completer, + _ => &self.signature.var_args, + } } } @@ -68,7 +67,7 @@ impl CommandSignature { } } -fn quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn quit(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { log::debug!("quitting..."); if event != PromptEvent::Validate { @@ -79,7 +78,7 @@ fn quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow: // last view and we have unsaved changes if cx.editor.tree.views().count() == 1 { - buffers_remaining_impl(cx.editor)?; + buffers_remaining_impl(cx.editor)? } cx.block_try_flush_writes()?; @@ -88,7 +87,11 @@ fn quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow: Ok(()) } -fn force_quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn force_quit( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -101,13 +104,12 @@ fn force_quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a Ok(()) } -fn open(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(!args.is_empty(), ":open needs at least one argument"); - + ensure!(!args.is_empty(), "wrong argument count"); for arg in args { let (path, pos) = args::parse_file(arg); let path = helix_stdx::path::expand_tilde(path); @@ -173,7 +175,7 @@ fn buffer_close_by_ids_impl( Ok(()) } -fn buffer_gather_paths_impl(editor: &mut Editor, args: Args) -> Vec { +fn buffer_gather_paths_impl(editor: &mut Editor, args: &[Cow]) -> Vec { // No arguments implies current document if args.is_empty() { let doc_id = view!(editor).doc; @@ -184,7 +186,7 @@ fn buffer_gather_paths_impl(editor: &mut Editor, args: Args) -> Vec let mut document_ids = vec![]; for arg in args { let doc_id = editor.documents().find_map(|doc| { - let arg_path = Some(Path::new(arg)); + let arg_path = Some(Path::new(arg.as_ref())); if doc.path().map(|p| p.as_path()) == arg_path || doc.relative_path() == arg_path { Some(doc.id()) } else { @@ -210,7 +212,7 @@ fn buffer_gather_paths_impl(editor: &mut Editor, args: Args) -> Vec fn buffer_close( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -223,7 +225,7 @@ fn buffer_close( fn force_buffer_close( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -245,7 +247,7 @@ fn buffer_gather_others_impl(editor: &mut Editor) -> Vec { fn buffer_close_others( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -258,7 +260,7 @@ fn buffer_close_others( fn force_buffer_close_others( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -275,7 +277,7 @@ fn buffer_gather_all_impl(editor: &mut Editor) -> Vec { fn buffer_close_all( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -288,7 +290,7 @@ fn buffer_close_all( fn force_buffer_close_all( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -301,7 +303,7 @@ fn force_buffer_close_all( fn buffer_next( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -314,7 +316,7 @@ fn buffer_next( fn buffer_previous( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -325,10 +327,15 @@ fn buffer_previous( Ok(()) } -fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) -> anyhow::Result<()> { +fn write_impl( + cx: &mut compositor::Context, + path: Option<&Cow>, + force: bool, +) -> anyhow::Result<()> { let config = cx.editor.config(); let jobs = &mut cx.jobs; let (view, doc) = current!(cx.editor); + let path = path.map(AsRef::as_ref); if config.insert_final_newline { insert_final_newline(doc, view.id); @@ -370,36 +377,40 @@ fn insert_final_newline(doc: &mut Document, view_id: ViewId) { } } -fn write(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn write( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.next(), false) + write_impl(cx, args.first(), false) } fn force_write( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.next(), true) + write_impl(cx, args.first(), true) } fn write_buffer_close( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.next(), false)?; + write_impl(cx, args.first(), false)?; let document_ids = buffer_gather_paths_impl(cx.editor, args); buffer_close_by_ids_impl(cx, &document_ids, false) @@ -407,20 +418,24 @@ fn write_buffer_close( fn force_write_buffer_close( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.next(), true)?; + write_impl(cx, args.first(), true)?; let document_ids = buffer_gather_paths_impl(cx.editor, args); buffer_close_by_ids_impl(cx, &document_ids, false) } -fn new_file(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn new_file( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -430,7 +445,11 @@ fn new_file(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> an Ok(()) } -fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn format( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -447,7 +466,7 @@ fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyh fn set_indent_style( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -468,9 +487,9 @@ fn set_indent_style( } // Attempt to parse argument as an indent style. - let style = match args.next() { + let style = match args.first() { 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() @@ -489,7 +508,7 @@ fn set_indent_style( /// Sets or reports the current document's line ending setting. fn set_line_ending( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -520,7 +539,7 @@ fn set_line_ending( } let arg = args - .next() + .first() .context("argument missing")? .to_ascii_lowercase(); @@ -559,12 +578,16 @@ fn set_line_ending( Ok(()) } -fn earlier(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn earlier( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let uk = args.raw().parse::().map_err(|s| anyhow!(s))?; + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.earlier(view, uk); @@ -575,13 +598,16 @@ fn earlier(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyh Ok(()) } -fn later(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn later( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let uk = args.raw().parse::().map_err(|s| anyhow!(s))?; - + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.later(view, uk); if !success { @@ -593,30 +619,30 @@ fn later(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow fn write_quit( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.next(), false)?; + write_impl(cx, args.first(), false)?; cx.block_try_flush_writes()?; - quit(cx, Args::empty(), event) + quit(cx, &[], event) } fn force_write_quit( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - write_impl(cx, args.next(), true)?; + write_impl(cx, args.first(), true)?; cx.block_try_flush_writes()?; - force_quit(cx, Args::empty(), event) + force_quit(cx, &[], event) } /// Results in an error if there are modified buffers remaining and sets editor @@ -723,7 +749,11 @@ pub fn write_all_impl( Ok(()) } -fn write_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn write_all( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -733,7 +763,7 @@ fn write_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> a fn force_write_all( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -745,7 +775,7 @@ fn force_write_all( fn write_all_quit( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -757,7 +787,7 @@ fn write_all_quit( fn force_write_all_quit( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -782,31 +812,41 @@ fn quit_all_impl(cx: &mut compositor::Context, force: bool) -> anyhow::Result<() Ok(()) } -fn quit_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { - if event != PromptEvent::Validate { - return Ok(()); - } - quit_all_impl(cx, false) -} - -fn force_quit_all( +fn quit_all( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + + quit_all_impl(cx, false) +} + +fn force_quit_all( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + quit_all_impl(cx, true) } -fn cquit(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn cquit( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } let exit_code = args - .next() + .first() .and_then(|code| code.parse::().ok()) .unwrap_or(1); @@ -816,7 +856,7 @@ fn cquit(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> an fn force_cquit( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -824,7 +864,7 @@ fn force_cquit( } let exit_code = args - .next() + .first() .and_then(|code| code.parse::().ok()) .unwrap_or(1); cx.editor.exit_code = exit_code; @@ -832,7 +872,11 @@ fn force_cquit( quit_all_impl(cx, true) } -fn theme(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn theme( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { let true_color = cx.editor.config.load().true_color || crate::true_color(); match event { PromptEvent::Abort => { @@ -842,7 +886,7 @@ fn theme(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> an if args.is_empty() { // Ensures that a preview theme gets cleaned up if the user backspaces until the prompt is empty. cx.editor.unset_theme_preview(); - } else if let Some(theme_name) = args.next() { + } else if let Some(theme_name) = args.first() { if let Ok(theme) = cx.editor.theme_loader.load(theme_name) { if !(true_color || theme.is_16_color()) { bail!("Unsupported theme: theme requires true color support"); @@ -852,7 +896,7 @@ fn theme(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> an }; } PromptEvent::Validate => { - if let Some(theme_name) = args.next() { + if let Some(theme_name) = args.first() { let theme = cx .editor .theme_loader @@ -875,142 +919,168 @@ fn theme(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> an fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + yank_primary_selection_impl(cx.editor, '+'); Ok(()) } -fn yank_joined(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn yank_joined( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + + ensure!(args.len() <= 1, ":yank-join takes at most 1 argument"); + + let doc = doc!(cx.editor); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); let register = cx.editor.selected_register.unwrap_or('"'); - yank_joined_impl(cx.editor, args.raw(), register); + yank_joined_impl(cx.editor, separator, register); Ok(()) } fn yank_joined_to_clipboard( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - yank_joined_impl(cx.editor, args.raw(), '+'); + + let doc = doc!(cx.editor); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); + yank_joined_impl(cx.editor, separator, '+'); Ok(()) } fn yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + yank_primary_selection_impl(cx.editor, '*'); Ok(()) } fn yank_joined_to_primary_clipboard( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - yank_joined_impl(cx.editor, args.raw(), '*'); + let doc = doc!(cx.editor); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); + yank_joined_impl(cx.editor, separator, '*'); Ok(()) } fn paste_clipboard_after( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + paste(cx.editor, '+', Paste::After, 1); Ok(()) } fn paste_clipboard_before( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + paste(cx.editor, '+', Paste::Before, 1); Ok(()) } fn paste_primary_clipboard_after( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + paste(cx.editor, '*', Paste::After, 1); Ok(()) } fn paste_primary_clipboard_before( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + paste(cx.editor, '*', Paste::Before, 1); Ok(()) } fn replace_selections_with_clipboard( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + replace_with_yanked_impl(cx.editor, '+', 1); Ok(()) } fn replace_selections_with_primary_clipboard( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + replace_with_yanked_impl(cx.editor, '*', 1); Ok(()) } fn show_clipboard_provider( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + cx.editor .set_status(cx.editor.registers.clipboard_provider_name()); Ok(()) @@ -1018,14 +1088,14 @@ fn show_clipboard_provider( fn change_current_directory( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let dir = match args.next() { + let dir = match args.first().map(AsRef::as_ref) { Some("-") => cx .editor .get_last_cwd() @@ -1047,7 +1117,7 @@ fn change_current_directory( fn show_current_directory( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1060,7 +1130,7 @@ fn show_current_directory( if cwd.exists() { cx.editor.set_status(message); } else { - cx.editor.set_error(format!("{message} (deleted)")); + cx.editor.set_error(format!("{} (deleted)", message)); } Ok(()) } @@ -1068,7 +1138,7 @@ fn show_current_directory( /// Sets the [`Document`]'s encoding.. fn set_encoding( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1076,7 +1146,7 @@ fn set_encoding( } let doc = doc_mut!(cx.editor); - if let Some(label) = args.next() { + if let Some(label) = args.first() { doc.set_encoding(label) } else { let encoding = doc.encoding().name().to_owned(); @@ -1088,7 +1158,7 @@ fn set_encoding( /// Shows info about the character under the primary cursor. fn get_character_info( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1211,7 +1281,11 @@ fn get_character_info( } /// Reload the [`Document`] from its source file. -fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn reload( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1230,7 +1304,11 @@ fn reload(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyh Ok(()) } -fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn reload_all( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1286,7 +1364,11 @@ fn reload_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> } /// Update the [`Document`] if it has been modified. -fn update(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn update( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1301,7 +1383,7 @@ fn update(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyho fn lsp_workspace_command( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1357,8 +1439,7 @@ fn lsp_workspace_command( }; cx.jobs.callback(callback); } else { - let command = args.raw().to_string(); - + let command = args.join(" "); let matches: Vec<_> = ls_id_commands .filter(|(_ls_id, c)| *c == &command) .collect(); @@ -1392,7 +1473,7 @@ fn lsp_workspace_command( fn lsp_restart( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1438,7 +1519,11 @@ fn lsp_restart( Ok(()) } -fn lsp_stop(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn lsp_stop( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1465,7 +1550,7 @@ fn lsp_stop(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> an fn tree_sitter_scopes( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1498,7 +1583,7 @@ fn tree_sitter_scopes( fn tree_sitter_highlight_name( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { fn find_highlight_at_cursor( @@ -1571,50 +1656,81 @@ fn tree_sitter_highlight_name( Ok(()) } -fn vsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn vsplit( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); - } else if args.is_empty() { + } + + if args.is_empty() { split(cx.editor, Action::VerticalSplit); } else { for arg in args { - cx.editor.open(&PathBuf::from(arg), Action::VerticalSplit)?; + cx.editor + .open(&PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; } } + Ok(()) } -fn hsplit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn hsplit( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); - } else if args.is_empty() { + } + + if args.is_empty() { split(cx.editor, Action::HorizontalSplit); } else { for arg in args { cx.editor - .open(&PathBuf::from(arg), Action::HorizontalSplit)?; + .open(&PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; } } + Ok(()) } -fn vsplit_new(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn vsplit_new( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + cx.editor.new_file(Action::VerticalSplit); + Ok(()) } -fn hsplit_new(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn hsplit_new( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + cx.editor.new_file(Action::HorizontalSplit); + Ok(()) } -fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn debug_eval( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1628,10 +1744,9 @@ fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a }; // TODO: support no frame_id - let frame_id = debugger.stack_frames[&thread_id][frame].id; - let expression = args.raw().to_string(); - let response = helix_lsp::block_on(debugger.eval(expression, Some(frame_id)))?; + let frame_id = debugger.stack_frames[&thread_id][frame].id; + let response = helix_lsp::block_on(debugger.eval(args.join(" "), Some(frame_id)))?; cx.editor.set_status(response.result); } Ok(()) @@ -1639,33 +1754,47 @@ fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a fn debug_start( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - dap_start_impl(cx, args.next(), None, Some(args.map(Into::into).collect())) + + let mut args = args.to_owned(); + let name = match args.len() { + 0 => None, + _ => Some(args.remove(0)), + }; + dap_start_impl(cx, name.as_deref(), None, Some(args)) } fn debug_remote( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let address = args.next().map(|addr| addr.parse()).transpose()?; - dap_start_impl( - cx, - args.next(), - address, - Some(args.map(Into::into).collect()), - ) + + let mut args = args.to_owned(); + let address = match args.len() { + 0 => None, + _ => Some(args.remove(0).parse()?), + }; + let name = match args.len() { + 0 => None, + _ => Some(args.remove(0)), + }; + dap_start_impl(cx, name.as_deref(), address, Some(args)) } -fn tutor(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn tutor( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -1689,7 +1818,7 @@ fn abort_goto_line_number_preview(cx: &mut compositor::Context) { fn update_goto_line_number_preview( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], ) -> anyhow::Result<()> { cx.editor.last_selection.get_or_insert_with(|| { let (view, doc) = current!(cx.editor); @@ -1697,7 +1826,7 @@ fn update_goto_line_number_preview( }); let scrolloff = cx.editor.config().scrolloff; - let line = args.next().unwrap().parse::()?; + let line = args[0].parse::()?; goto_line_without_jumplist(cx.editor, NonZeroUsize::new(line)); let (view, doc) = current!(cx.editor); @@ -1708,7 +1837,7 @@ fn update_goto_line_number_preview( pub(super) fn goto_line_number( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { match event { @@ -1744,18 +1873,18 @@ pub(super) fn goto_line_number( // Fetch the current value of a config option and output as status. fn get_option( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - if args.arg_count() != 1 { + if args.len() != 1 { anyhow::bail!("Bad arguments. Usage: `:get key`"); } - let key = args.next().unwrap().to_lowercase(); + let key = &args[0].to_lowercase(); let key_error = || anyhow::anyhow!("Unknown key `{}`", key); let config = serde_json::json!(cx.editor.config().deref()); @@ -1770,61 +1899,46 @@ fn get_option( /// example to disable smart case search, use `:set search.smart-case false`. fn set_option( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let Some(key) = args.next().map(|arg| arg.to_lowercase()) else { - anyhow::bail!("Bad arguments. Usage: `:set key field`, didn't provide `key`"); - }; - - let field = args.rest(); - - if field.is_empty() { - anyhow::bail!("Bad arguments. Usage: `:set key field`, didn't provide `field`"); + if args.len() != 2 { + anyhow::bail!("Bad arguments. Usage: `:set key field`"); } + let (key, arg) = (&args[0].to_lowercase(), &args[1]); - let mut config = serde_json::json!(&*cx.editor.config()); + let key_error = || anyhow::anyhow!("Unknown key `{}`", key); + let field_error = |_| anyhow::anyhow!("Could not parse field `{}`", arg); + + let mut config = serde_json::json!(&cx.editor.config().deref()); let pointer = format!("/{}", key.replace('.', "/")); - let value = config - .pointer_mut(&pointer) - .ok_or_else(|| anyhow::anyhow!("Unknown key `{key}`"))?; + let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; *value = if value.is_string() { // JSON strings require quotes, so we can't .parse() directly - Value::String(field.to_string()) + Value::String(arg.to_string()) } else { - field - .parse() - .map_err(|err| anyhow::anyhow!("Could not parse field `{field}`: {err}"))? + arg.parse().map_err(field_error)? }; - - let config = serde_json::from_value(config).expect( - "`Config` was already deserialized, serialization is just a 'repacking' and should be valid", - ); + let config = serde_json::from_value(config).map_err(field_error)?; cx.editor .config_events .0 .send(ConfigEvent::Update(config))?; - - cx.editor - .set_status(format!("'{key}' is now set to {field}")); - Ok(()) } /// Toggle boolean config option at runtime. Access nested values by dot -/// syntax. -/// Example: -/// - `:toggle search.smart-case` (bool) -/// - `:toggle line-number relative absolute` (string) +/// syntax, for example to toggle smart case search, use `:toggle search.smart- +/// case`. fn toggle_option( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1834,98 +1948,73 @@ fn toggle_option( if args.is_empty() { anyhow::bail!("Bad arguments. Usage: `:toggle key [values]?`"); } - let key = args.next().unwrap().to_lowercase(); + let key = &args[0].to_lowercase(); - let mut config = serde_json::json!(&*cx.editor.config()); + let key_error = || anyhow::anyhow!("Unknown key `{}`", key); + + let mut config = serde_json::json!(&cx.editor.config().deref()); let pointer = format!("/{}", key.replace('.', "/")); - let value = config - .pointer_mut(&pointer) - .ok_or_else(|| anyhow::anyhow!("Unknown key `{}`", key))?; + let value = config.pointer_mut(&pointer).ok_or_else(key_error)?; *value = match value { Value::Bool(ref value) => { ensure!( - args.next().is_none(), + args.len() == 1, "Bad arguments. For boolean configurations use: `:toggle key`" ); Value::Bool(!value) } Value::String(ref value) => { ensure!( - // key + arguments - args.arg_count() >= 3, + args.len() > 2, "Bad arguments. For string configurations use: `:toggle key val1 val2 ...`", ); Value::String( - args.clone() + args[1..] + .iter() .skip_while(|e| *e != value) .nth(1) - .unwrap_or_else(|| args.nth(1).unwrap()) + .unwrap_or_else(|| &args[1]) .to_string(), ) } Value::Number(ref value) => { ensure!( - // key + arguments - args.arg_count() >= 3, + args.len() > 2, "Bad arguments. For number configurations use: `:toggle key val1 val2 ...`", ); - let value = value.to_string(); - Value::Number( - args.clone() - .skip_while(|e| *e != value) + args[1..] + .iter() + .skip_while(|&e| value.to_string() != *e.to_string()) .nth(1) - .unwrap_or_else(|| args.nth(1).unwrap()) + .unwrap_or_else(|| &args[1]) .parse()?, ) } - Value::Array(value) => { - let mut lists = serde_json::Deserializer::from_str(args.rest()).into_iter::(); - - let (Some(first), Some(second)) = - (lists.next().transpose()?, lists.next().transpose()?) - else { - anyhow::bail!( - "Bad arguments. For list configurations use: `:toggle key [...] [...]`", - ) - }; - - match (&first, &second) { - (Value::Array(list), Value::Array(_)) => { - if list == value { - second - } else { - first - } - } - _ => anyhow::bail!("values must be lists"), - } - } - Value::Null | Value::Object(_) => { + Value::Null | Value::Object(_) | Value::Array(_) => { anyhow::bail!("Configuration {key} does not support toggle yet") } }; let status = format!("'{key}' is now set to {value}"); - let config = serde_json::from_value(config)?; + let config = serde_json::from_value(config) + .map_err(|err| anyhow::anyhow!("Cannot parse `{:?}`, {}", &args, err))?; cx.editor .config_events .0 .send(ConfigEvent::Update(config))?; - cx.editor.set_status(status); - Ok(()) } /// Change the language of the current buffer at runtime. fn language( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -1934,22 +2023,21 @@ fn language( if args.is_empty() { let doc = doc!(cx.editor); - let language = doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); + let language = &doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); cx.editor.set_status(language.to_string()); return Ok(()); } - if args.arg_count() != 1 { + if args.len() != 1 { anyhow::bail!("Bad arguments. Usage: `:set-language language`"); } let doc = doc_mut!(cx.editor); - let language_id = args.next().unwrap(); - if language_id == DEFAULT_LANGUAGE_NAME { - doc.set_language(None, None); + if args[0] == DEFAULT_LANGUAGE_NAME { + doc.set_language(None, None) } else { - doc.set_language_by_language_id(language_id, cx.editor.syn_loader.clone())?; + doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone())?; } doc.detect_indent_and_line_ending(); @@ -1962,25 +2050,31 @@ fn language( Ok(()) } -fn sort(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn sort(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + sort_impl(cx, args, false) } fn sort_reverse( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + sort_impl(cx, args, true) } -fn sort_impl(cx: &mut compositor::Context, _args: Args, reverse: bool) -> anyhow::Result<()> { +fn sort_impl( + cx: &mut compositor::Context, + _args: &[Cow], + reverse: bool, +) -> anyhow::Result<()> { let scrolloff = cx.editor.config().scrolloff; let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -2012,7 +2106,11 @@ fn sort_impl(cx: &mut compositor::Context, _args: Args, reverse: bool) -> anyhow Ok(()) } -fn reflow(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn reflow( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -2026,7 +2124,7 @@ fn reflow(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> a // - The configured text-width for this language in languages.toml // - The configured text-width in the config.toml let text_width: usize = args - .next() + .first() .map(|num| num.parse::()) .transpose()? .or_else(|| doc.language_config().and_then(|config| config.text_width)) @@ -2051,7 +2149,7 @@ fn reflow(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> a fn tree_sitter_subtree( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2090,12 +2188,13 @@ fn tree_sitter_subtree( fn open_config( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + cx.editor .open(&helix_loader::config_file(), Action::Replace)?; Ok(()) @@ -2103,28 +2202,34 @@ fn open_config( fn open_workspace_config( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + cx.editor .open(&helix_loader::workspace_config_file(), Action::Replace)?; Ok(()) } -fn open_log(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn open_log( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + cx.editor.open(&helix_loader::log_file(), Action::Replace)?; Ok(()) } fn refresh_config( cx: &mut compositor::Context, - _args: Args, + _args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2137,58 +2242,62 @@ fn refresh_config( fn append_output( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + ensure!(!args.is_empty(), "Shell command required"); - let cmd = helix_core::shellwords::unescape(args.raw()); - shell(cx, &cmd, &ShellBehavior::Append); + shell(cx, &args.join(" "), &ShellBehavior::Append); Ok(()) } fn insert_output( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + ensure!(!args.is_empty(), "Shell command required"); - let cmd = helix_core::shellwords::unescape(args.raw()); - shell(cx, &cmd, &ShellBehavior::Insert); + shell(cx, &args.join(" "), &ShellBehavior::Insert); Ok(()) } -fn pipe_to(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn pipe_to( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { pipe_impl(cx, args, event, &ShellBehavior::Ignore) } -fn pipe(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn pipe(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { pipe_impl(cx, args, event, &ShellBehavior::Replace) } fn pipe_impl( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, behavior: &ShellBehavior, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } + ensure!(!args.is_empty(), "Shell command required"); - let cmd = helix_core::shellwords::unescape(args.raw()); - shell(cx, &cmd, behavior); + shell(cx, &args.join(" "), behavior); Ok(()) } fn run_shell_command( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2196,8 +2305,7 @@ fn run_shell_command( } let shell = cx.editor.config().shell.clone(); - - let args = helix_core::shellwords::unescape(args.raw()).into_owned(); + let args = args.join(" "); let callback = async move { let output = shell_impl_async(&shell, &args, None).await?; @@ -2225,7 +2333,7 @@ fn run_shell_command( fn reset_diff_change( cx: &mut compositor::Context, - args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { @@ -2233,8 +2341,10 @@ fn reset_diff_change( } ensure!(args.is_empty(), ":reset-diff-change takes no arguments"); - let scrolloff = cx.editor.config().scrolloff; - let (view, doc) = current!(cx.editor); + let editor = &mut cx.editor; + let scrolloff = editor.config().scrolloff; + + let (view, doc) = current!(editor); let Some(handle) = doc.diff_handle() else { bail!("Diff is not available in the current buffer") }; @@ -2276,42 +2386,40 @@ fn reset_diff_change( fn clear_register( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!( - args.arg_count() <= 1, - ":clear-register takes at most 1 argument" - ); - + ensure!(args.len() <= 1, ":clear-register takes at most 1 argument"); if args.is_empty() { cx.editor.registers.clear(); cx.editor.set_status("All registers cleared"); return Ok(()); } - let register = args.next().unwrap(); - ensure!( - register.chars().count() == 1, - format!("Invalid register {register}") + args[0].chars().count() == 1, + format!("Invalid register {}", args[0]) ); - - let register = register.chars().next().unwrap_or_default(); + let register = args[0].chars().next().unwrap_or_default(); if cx.editor.registers.remove(register) { - cx.editor.set_status(format!("Register {register} cleared")); + cx.editor + .set_status(format!("Register {} cleared", register)); } else { cx.editor - .set_error(format!("Register {register} not found")); + .set_error(format!("Register {} not found", register)); } Ok(()) } -fn redraw(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn redraw( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -2332,22 +2440,20 @@ fn redraw(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyh fn move_buffer( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - ensure!(args.arg_count() == 1, format!(":move takes one argument")); - - let old_path = doc!(cx.editor) + ensure!(args.len() == 1, format!(":move takes one argument")); + let doc = doc!(cx.editor); + let old_path = doc .path() .context("Scratch buffer cannot be moved. Use :write instead")? .clone(); - - let new_path = args.next().unwrap(); - + let new_path = args.first().unwrap().to_string(); if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) { bail!("Could not move file: {err}"); } @@ -2356,14 +2462,14 @@ fn move_buffer( fn yank_diagnostic( cx: &mut compositor::Context, - mut args: Args, + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } - let reg = match args.next() { + let reg = match args.first() { Some(s) => { ensure!(s.chars().count() == 1, format!("Invalid register {s}")); s.chars().next().unwrap() @@ -2394,7 +2500,7 @@ fn yank_diagnostic( Ok(()) } -fn read(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> anyhow::Result<()> { +fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { if event != PromptEvent::Validate { return Ok(()); } @@ -2403,10 +2509,10 @@ fn read(cx: &mut compositor::Context, mut args: Args, event: PromptEvent) -> any let (view, doc) = current!(cx.editor); ensure!(!args.is_empty(), "file name is expected"); - ensure!(args.arg_count() == 1, "only the file name is expected"); + ensure!(args.len() == 1, "only the file name is expected"); - let filename = args.next().unwrap(); - let path = helix_stdx::path::expand_tilde(Path::new(filename)); + let filename = args.first().unwrap(); + let path = helix_stdx::path::expand_tilde(PathBuf::from(filename.to_string())); ensure!( path.exists() && path.is_file(), @@ -3069,11 +3175,9 @@ pub(super) fn command_mode(cx: &mut Context) { Some(':'), |editor: &Editor, input: &str| { let shellwords = Shellwords::from(input); - let command = shellwords.command(); + let words = shellwords.words(); - if command.is_empty() - || (shellwords.args().next().is_none() && !shellwords.ends_with_whitespace()) - { + if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) { fuzzy_match( input, TYPABLE_COMMAND_LIST.iter().map(|command| command.name), @@ -3085,61 +3189,67 @@ pub(super) fn command_mode(cx: &mut Context) { } else { // Otherwise, use the command's completer and the last shellword // as completion input. - let (word, len) = shellwords - .args() - .last() - .map_or(("", 0), |last| (last, last.len())); + let (word, word_len) = if words.len() == 1 || shellwords.ends_with_whitespace() { + (&Cow::Borrowed(""), 0) + } else { + (words.last().unwrap(), words.last().unwrap().len()) + }; - TYPABLE_COMMAND_MAP - .get(command) - .map(|tc| tc.completer_for_argument_number(argument_number_of(&shellwords))) - .map_or_else(Vec::new, |completer| { - completer(editor, word) - .into_iter() - .map(|(range, mut file)| { - file.content = shellwords::escape(file.content); + let argument_number = argument_number_of(&shellwords); - // offset ranges to input - let offset = input.len() - len; - let range = (range.start + offset)..; - (range, file) - }) - .collect() - }) + if let Some(completer) = TYPABLE_COMMAND_MAP + .get(&words[0] as &str) + .map(|tc| tc.completer_for_argument_number(argument_number)) + { + completer(editor, word) + .into_iter() + .map(|(range, mut file)| { + file.content = shellwords::escape(file.content); + + // offset ranges to input + let offset = input.len() - word_len; + let range = (range.start + offset)..; + (range, file) + }) + .collect() + } else { + Vec::new() + } } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let shellwords = Shellwords::from(input); - let command = shellwords.command(); - - if command.is_empty() { + let parts = input.split_whitespace().collect::>(); + if parts.is_empty() { return; } - // If input is `:NUMBER`, interpret as line number and go there. - if command.parse::().is_ok() { - if let Err(err) = typed::goto_line_number(cx, Args::from(command), event) { - cx.editor.set_error(format!("{err}")); + // 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) = typed::goto_line_number(cx, &[Cow::from(parts[0])], event) { + cx.editor.set_error(format!("{}", e)); } return; } // Handle typable commands - if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(command) { - if let Err(err) = (cmd.fun)(cx, shellwords.args(), event) { - cx.editor.set_error(format!("{err}")); + if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { + let shellwords = Shellwords::from(input); + let args = shellwords.words(); + + if let Err(e) = (cmd.fun)(cx, &args[1..], event) { + cx.editor.set_error(format!("{}", e)); } } else if event == PromptEvent::Validate { - cx.editor.set_error(format!("no such command: '{command}'")); + cx.editor + .set_error(format!("no such command: '{}'", parts[0])); } }, ); - prompt.doc_fn = Box::new(|input: &str| { - let shellwords = Shellwords::from(input); + let part = input.split(' ').next().unwrap_or_default(); if let Some(typed::TypableCommand { doc, aliases, .. }) = - typed::TYPABLE_COMMAND_MAP.get(shellwords.command()) + typed::TYPABLE_COMMAND_MAP.get(part) { if aliases.is_empty() { return Some((*doc).into()); @@ -3156,10 +3266,11 @@ pub(super) fn command_mode(cx: &mut Context) { } fn argument_number_of(shellwords: &Shellwords) -> usize { - shellwords - .args() - .arg_count() - .saturating_sub(1 - usize::from(shellwords.ends_with_whitespace())) + if shellwords.ends_with_whitespace() { + shellwords.words().len().saturating_sub(1) + } else { + shellwords.words().len().saturating_sub(2) + } } #[test] diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index aa9cafd3..020ecaf4 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -597,14 +597,18 @@ mod tests { let expectation = KeyTrie::Node(KeyTrieNode::new( "", hashmap! { - key => KeyTrie::Sequence(vec![ + key => KeyTrie::Sequence(vec!{ MappableCommand::select_all, MappableCommand::Typable { name: "pipe".to_string(), - args: String::from("sed -E 's/\\s+$//g'"), - doc: String::new(), + args: vec!{ + "sed".to_string(), + "-E".to_string(), + "'s/\\s+$//g'".to_string() + }, + doc: "".to_string(), }, - ]) + }) }, vec![key], )); From 5616f1d66d735e1995d4a55dba42fcd571216200 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 6 Jan 2025 14:18:26 -0500 Subject: [PATCH 03/23] changelog: Add missing breaking change for display-messages config --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7a1388..3658cd78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ Packaging: As always, a big thank you to all of the contributors! This release saw changes from 171 contributors. +Breaking changes: + +* The `editor.lsp.display-messages` key now controls messages sent with the LSP `window/showMessage` notification rather than progress messages. If you want to enable progress messages you should now enable the `editor.lsp.display-progress-messages` key instead. ([#5535](https://github.com/helix-editor/helix/pull/5535)) + Features: * Big refactor for `Picker`s ([#9647](https://github.com/helix-editor/helix/pull/9647), [#11209](https://github.com/helix-editor/helix/pull/11209), [#11216](https://github.com/helix-editor/helix/pull/11216), [#11211](https://github.com/helix-editor/helix/pull/11211), [#11343](https://github.com/helix-editor/helix/pull/11343), [#11406](https://github.com/helix-editor/helix/pull/11406)) From e698b20245bf46711575a76d459eb04dd3913f2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:34:21 -0500 Subject: [PATCH 04/23] build(deps): bump the rust-dependencies group with 3 updates (#12437) --- Cargo.lock | 13 +++++++------ Cargo.toml | 1 + helix-loader/Cargo.toml | 2 +- helix-stdx/Cargo.toml | 2 +- helix-term/Cargo.toml | 4 ++-- helix-vcs/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bb0a95f..d284dd7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.2.6" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" +checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" dependencies = [ "shlex", ] @@ -1996,9 +1996,9 @@ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" -version = "5.3.1" +version = "5.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ecd52f0b8d15c40ce4820aa251ed5de032e5d91fab27f7db2f40d42a8bdf69c" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" dependencies = [ "is-wsl", "libc", @@ -2450,12 +2450,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.14.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", diff --git a/Cargo.toml b/Cargo.toml index ddf02c4e..ace374a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ tree-sitter = { version = "0.22" } nucleo = "0.5.0" slotmap = "1.0.7" thiserror = "2.0" +tempfile = "3.15.0" [workspace.package] version = "25.1.0" diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index b87a9184..d97bf9d1 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -30,7 +30,7 @@ log = "0.4" # cloning/compiling tree-sitter grammars cc = { version = "1" } threadpool = { version = "1.0" } -tempfile = "3.14.0" +tempfile.workspace = true dunce = "1.0.5" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index 84313b5b..dd943eeb 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -28,4 +28,4 @@ windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Securit rustix = { version = "0.38", features = ["fs"] } [dev-dependencies] -tempfile = "3.14" +tempfile.workspace = true diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index e8c2db24..83d6ccc9 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -59,7 +59,7 @@ content_inspector = "0.2.4" thiserror.workspace = true # opening URLs -open = "5.3.1" +open = "5.3.2" url = "2.5.4" # config @@ -85,5 +85,5 @@ helix-loader = { path = "../helix-loader" } [dev-dependencies] smallvec = "1.13" indoc = "2.0.5" -tempfile = "3.14.0" +tempfile.workspace = true same-file = "1.0.1" diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 93b9def8..eb1fc880 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -29,4 +29,4 @@ log = "0.4" git = ["gix"] [dev-dependencies] -tempfile = "3.14" +tempfile.workspace = true diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 6f71fa05..82e5fd92 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -28,7 +28,7 @@ bitflags = "2.6" anyhow = "1" crossterm = { version = "0.28", optional = true } -tempfile = "3.14" +tempfile.workspace = true # Conversion traits once_cell = "1.20" From a0bd39d40e983c9fdcd4909b0077b3ac8bf43ad6 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 7 Jan 2025 09:17:59 -0500 Subject: [PATCH 05/23] book: Document editor.lsp.display-progress-messages config option Connects #5535 --- book/src/editor.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/book/src/editor.md b/book/src/editor.md index feec09fd..b731b3d1 100644 --- a/book/src/editor.md +++ b/book/src/editor.md @@ -143,7 +143,8 @@ The following statusline elements can be configured: | Key | Description | Default | | --- | ----------- | ------- | | `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` | -| `display-messages` | Display LSP progress messages below statusline[^1] | `false` | +| `display-messages` | Display LSP `window/showMessage` messages below statusline[^1] | `true` | +| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` | | `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` | | `display-inlay-hints` | Display inlay hints[^2] | `false` | | `display-signature-help-docs` | Display docs under signature help popup | `true` | From 917174e546ca20e13538510a700c7c80f759d12c Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 7 Jan 2025 14:59:44 -0500 Subject: [PATCH 06/23] Fix blank buffer picker preview on doc with no views Reproduction: * `hx` * Open any file in a split (`f` and choose anything with ``) * Close the split with `q` * Open up the buffer picker and look the file you opened previously Previously the preview was empty in this case because the Document's `selections` hashmap was empty and we returned early, giving `None` instead of a FileLocation. Instead when the Document is not currently open in any view we can show the document but with no range highlighted. --- helix-term/src/commands.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a93fa445..3c93ae7f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3044,12 +3044,11 @@ fn buffer_picker(cx: &mut Context) { }) .with_preview(|editor, meta| { let doc = &editor.documents.get(&meta.id)?; - let &view_id = doc.selections().keys().next()?; - let line = doc - .selection(view_id) - .primary() - .cursor_line(doc.text().slice(..)); - Some((meta.id.into(), Some((line, line)))) + let lines = doc.selections().values().next().map(|selection| { + let cursor_line = selection.primary().cursor_line(doc.text().slice(..)); + (cursor_line, cursor_line) + }); + Some((meta.id.into(), lines)) }); cx.push_layer(Box::new(overlaid(picker))); } From 931dd9c1dcb55b20c26b71d58425b38784fdeb9a Mon Sep 17 00:00:00 2001 From: rhogenson <05huvhec@duck.com> Date: Wed, 8 Jan 2025 06:42:41 -0800 Subject: [PATCH 07/23] Fix a typo in join_selections (#12452) Co-authored-by: Rose Hogenson --- helix-term/src/commands.rs | 2 +- helix-term/tests/test/commands.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3c93ae7f..3b906487 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4780,7 +4780,7 @@ fn join_selections_impl(cx: &mut Context, select_space: bool) { changes.reserve(lines.len()); let first_line_idx = slice.line_to_char(start); - let first_line_idx = skip_while(slice, first_line_idx, |ch| matches!(ch, ' ' | 't')) + let first_line_idx = skip_while(slice, first_line_idx, |ch| matches!(ch, ' ' | '\t')) .unwrap_or(first_line_idx); let first_line = slice.slice(first_line_idx..); let mut current_comment_token = comment_tokens diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 32badaa4..3e2d4b52 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -665,6 +665,14 @@ async fn test_join_selections_comment() -> anyhow::Result<()> { )) .await?; + test(( + "#[|\t// Join comments +\t// with indent]#", + ":lang goJ", + "#[|\t// Join comments with indent]#", + )) + .await?; + Ok(()) } From a83c23bb037a2db26f3e2fe519de22425e95a7d8 Mon Sep 17 00:00:00 2001 From: Rob Gonnella Date: Wed, 8 Jan 2025 13:36:40 -0500 Subject: [PATCH 08/23] Run formatter from Document directory (#12315) Co-authored-by: Rob Gonnella --- helix-view/src/document.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 29fd736a..edbc96b0 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -774,7 +774,13 @@ impl Document { { use std::process::Stdio; let text = self.text().clone(); + let mut process = tokio::process::Command::new(&fmt_cmd); + + if let Some(doc_dir) = self.path.as_ref().and_then(|path| path.parent()) { + process.current_dir(doc_dir); + } + process .args(fmt_args) .stdin(Stdio::piped()) From 9721144e03a6c4e221c86408d34ce929972a36a5 Mon Sep 17 00:00:00 2001 From: Evan Richter Date: Fri, 10 Jan 2025 08:03:04 -0700 Subject: [PATCH 09/23] language support: CodeQL (#12470) --- book/src/generated/lang-support.md | 1 + languages.toml | 16 ++++ runtime/queries/codeql/highlights.scm | 104 +++++++++++++++++++++++++ runtime/queries/codeql/textobjects.scm | 16 ++++ 4 files changed, 137 insertions(+) create mode 100644 runtime/queries/codeql/highlights.scm create mode 100644 runtime/queries/codeql/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 45f69a54..7ec7ec5a 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -23,6 +23,7 @@ | circom | ✓ | | | `circom-lsp` | | clojure | ✓ | | | `clojure-lsp` | | cmake | ✓ | ✓ | ✓ | `cmake-language-server` | +| codeql | ✓ | ✓ | | `codeql` | | comment | ✓ | | | | | common-lisp | ✓ | | ✓ | `cl-lsp` | | cpon | ✓ | | ✓ | | diff --git a/languages.toml b/languages.toml index 50e7d823..504613a1 100644 --- a/languages.toml +++ b/languages.toml @@ -21,6 +21,7 @@ cl-lsp = { command = "cl-lsp", args = [ "stdio" ] } clangd = { command = "clangd" } clojure-lsp = { command = "clojure-lsp" } cmake-language-server = { command = "cmake-language-server" } +codeql = { command = "codeql", args = ["execute", "language-server", "--check-errors=ON_CHANGE"] } crystalline = { command = "crystalline", args = ["--stdio"] } cs = { command = "cs", args = ["launch", "--contrib", "smithy-language-server", "--", "0"] } csharp-ls = { command = "csharp-ls" } @@ -4033,3 +4034,18 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "nginx" source = { git = "https://gitlab.com/joncoole/tree-sitter-nginx", rev = "b4b61db443602b69410ab469c122c01b1e685aa0" } + +[[language]] +name = "codeql" +scope = "source.ql" +file-types = ["ql", "qll"] +comment-token = "//" +block-comment-tokens = { start = "/*", end = "*/" } +indent = { tab-width = 2, unit = " " } +injection-regex = "codeql" +grammar = "ql" +language-servers = ["codeql"] + +[[grammar]] +name = "ql" +source = { git = "https://github.com/tree-sitter/tree-sitter-ql", rev = "1fd627a4e8bff8c24c11987474bd33112bead857" } diff --git a/runtime/queries/codeql/highlights.scm b/runtime/queries/codeql/highlights.scm new file mode 100644 index 00000000..aed7b538 --- /dev/null +++ b/runtime/queries/codeql/highlights.scm @@ -0,0 +1,104 @@ +[ + "and" + "any" + "as" + "asc" + "avg" + "by" + "class" + "concat" + "count" + "desc" + "else" + "exists" + "extends" + "forall" + "forex" + "from" + "if" + "implements" + "implies" + "import" + "in" + "instanceof" + "max" + "min" + "module" + "newtype" + "not" + "or" + "order" + "rank" + "select" + "strictconcat" + "strictcount" + "strictsum" + "sum" + "then" + "where" + + (false) + (predicate) + (result) + (specialId) + (super) + (this) + (true) +] @keyword + +[ + "boolean" + "float" + "int" + "date" + "string" +] @type.builtin + +(annotName) @attribute + +[ + "<" + "<=" + "=" + ">" + ">=" + "-" + "!=" + "/" + "*" + "%" + "+" + "::" +] @operator + +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @punctuation.bracket + +[ + "," + "|" +] @punctuation.delimiter + +(className) @type + +(varName) @variable + +(integer) @constant.numeric.integer +(float) @constant.numeric.float + +(string) @string + +(aritylessPredicateExpr (literalId) @function) +(predicateName) @function + +[ + (line_comment) + (block_comment) + (qldoc) +] @comment diff --git a/runtime/queries/codeql/textobjects.scm b/runtime/queries/codeql/textobjects.scm new file mode 100644 index 00000000..8ca02e3c --- /dev/null +++ b/runtime/queries/codeql/textobjects.scm @@ -0,0 +1,16 @@ +(qldoc) @comment.around +(block_comment) @comment.around +(line_comment) @comment.inside +(line_comment)+ @comment.around + +(classlessPredicate + ((varDecl) @parameter.inside . ","?) @parameter.around + (body "{" (_)* @function.inside "}")) @function.around +(memberPredicate + ((varDecl) @parameter.inside . ","?) @parameter.around + (body "{" (_)* @function.inside "}")) @function.around + +(dataclass + ("{" (_)* @class.inside "}")?) @class.around +(datatype) @class.around +(datatypeBranch) @class.around From b26903cd13fdf8976bda43abbc5e85a50130d12f Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 11 Jan 2025 08:42:17 -0500 Subject: [PATCH 10/23] Add comment tokens for JSONC Fixes #12491 --- languages.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/languages.toml b/languages.toml index 504613a1..e593b2ed 100644 --- a/languages.toml +++ b/languages.toml @@ -488,6 +488,8 @@ name = "jsonc" scope = "source.json" injection-regex = "jsonc" file-types = ["jsonc", { glob = "tsconfig.json" }] +comment-token = "//" +block-comment-tokens = { start = "/*", end = "*/" } grammar = "json" language-servers = [ "vscode-json-language-server" ] auto-format = true From 8f5f818c8837c8888abb9e83fdeecdb67f9c260a Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+nik-rev@users.noreply.github.com> Date: Sat, 11 Jan 2025 16:49:39 +0000 Subject: [PATCH 11/23] fix(highlights): recognize `!` as the never type (#12485) Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> --- runtime/queries/rust/highlights.scm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index b3a0f4d7..5cfbff59 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -311,6 +311,8 @@ ((identifier) @type (#match? @type "^[A-Z]")) +(never_type "!" @type) + ; ------- ; Functions ; ------- @@ -453,6 +455,7 @@ ; Remaining Identifiers ; ------- +; We do not style ? as an operator on purpose as it allows styling ? differently, as many highlighters do. @operator.special might have been a better scope, but @special is already documented so the change would break themes (including the intent of the default theme) "?" @special (type_identifier) @type From e440e54e7910673d5c5de4773da9796a6dbe2c87 Mon Sep 17 00:00:00 2001 From: Kirawi <67773714+kirawi@users.noreply.github.com> Date: Sat, 11 Jan 2025 11:52:13 -0500 Subject: [PATCH 12/23] pin to `ubuntu-22.04` for releases (#12464) --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b483e3af..d1c9bc03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,17 +61,17 @@ jobs: build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc include: - build: x86_64-linux - os: ubuntu-latest + os: ubuntu-22.04 rust: stable target: x86_64-unknown-linux-gnu cross: false - build: aarch64-linux - os: ubuntu-latest + os: ubuntu-22.04 rust: stable target: aarch64-unknown-linux-gnu cross: true # - build: riscv64-linux - # os: ubuntu-latest + # os: ubuntu-22.04 # rust: stable # target: riscv64gc-unknown-linux-gnu # cross: true From a539199666a0f49126682c0b423319d045a9ae23 Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+nik-rev@users.noreply.github.com> Date: Sat, 11 Jan 2025 20:59:03 +0000 Subject: [PATCH 13/23] feat(highlights): add more built-in functions for `ecma`, `rust` and `haskell` (#12488) Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> --- runtime/queries/ecma/highlights.scm | 27 ++- runtime/queries/haskell/highlights.scm | 249 +++++++++++++++++++++++++ runtime/queries/rust/highlights.scm | 8 + 3 files changed, 282 insertions(+), 2 deletions(-) diff --git a/runtime/queries/ecma/highlights.scm b/runtime/queries/ecma/highlights.scm index dc8ce5e7..b8df07a5 100644 --- a/runtime/queries/ecma/highlights.scm +++ b/runtime/queries/ecma/highlights.scm @@ -16,8 +16,31 @@ (#match? @variable.builtin "^(arguments|module|console|window|document)$") (#is-not? local)) -((identifier) @function.builtin - (#eq? @function.builtin "require") +(call_expression + (identifier) @function.builtin + (#any-of? @function.builtin + "eval" + "fetch" + "isFinite" + "isNaN" + "parseFloat" + "parseInt" + "decodeURI" + "decodeURIComponent" + "encodeURI" + "encodeURIComponent" + "require" + "alert" + "prompt" + "btoa" + "atob" + "confirm" + "structuredClone" + "setTimeout" + "clearTimeout" + "setInterval" + "clearInterval" + "queueMicrotask") (#is-not? local)) ; Function and method definitions diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm index 3d416de8..8f079185 100644 --- a/runtime/queries/haskell/highlights.scm +++ b/runtime/queries/haskell/highlights.scm @@ -7,6 +7,255 @@ (char) @constant.character (string) @string +(exp_apply + (exp_name + (variable) @function.builtin + (#any-of? @function.builtin + ; built in functions from the Haskell prelude (https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelude.html) + ; basic data types + "not" + "maybe" + "either" + + ; tuples + "fst" + "snd" + "curry" + "uncurry" + + ; Ord + "compare" + "min" + "max" + + ; Enum + "succ" + "pred" + "toEnum" + "fromEnum" + "enumFrom" + "enumFromThen" + "enumFromThenTo" + + ; Num + "negate" + "abs" + "signum" + "fromInteger" + + ; Real + "toRational" + + ; Integral + "quot" + "rem" + "div" + "mod" + "quotRem" + "divMod" + "toInteger" + + ; Fractional + "recip" + "fromRational" + + ; Floating + "exp" + "log" + "sqrt" + "logBase" + "sin" + "cos" + "tan" + "asin" + "acos" + "atan" + "sinh" + "cosh" + "tanh" + "asinh" + "acosh" + "atanh" + + ; RealFrac + "properFraction" + "truncate" + "round" + "ceiling" + "floor" + + ; RealFloat + "floatRadix" + "floatDigits" + "floatRange" + "decodeFloat" + "encodeFloat" + "exponent" + "significand" + "scaleFloat" + "isNaN" + "isInfinite" + "isDenormalized" + "isNegativeZero" + "isIEEE" + "atan2" + + ; Numeric functions + "subtract" + "even" + "odd" + "gcd" + "lcm" + "fromIntegral" + "realToFrac" + + ; Monoid + "mempty" + "mconcat" + "mappend" + + ; Functor + "fmap" + + ; Applicative + "liftA2" + "pure" + + ; Monad + "return" + + ; MonadFail + "fail" + "mapM_" + "sequence_" + + ; Foldable + "foldMap" + "foldr" + "foldl" + "foldl'" + "foldr1" + "foldl1" + "elem" + "maximum" + "minimum" + "sum" + "product" + + ; Traversable + "traverse" + "sequenceA" + "mapM" + "sequence" + + ; miscellaneous + "id" + "const" + "flip" + "until" + "asTypeOf" + "error" + "errorWithoutStackTrace" + "undefined" + + ; List + "map" + "filter" + "head" + "last" + "tail" + "init" + "null" + "length" + "reverse" + + ; Foldable + "and" + "or" + "any" + "all" + "concat" + "concatMap" + + ; Building lists + "scanl" + "scanl1" + "scanr" + "scanr1" + + ; Infinite lists + "iterate" + "repeat" + "replicate" + "cycle" + + ; Sublists + "take" + "drop" + "takeWhile" + "dropWhile" + "span" + "break" + "splitAt" + + ; Searching lists + "notElem" + "lookup" + + ; zipping and unzipping + "zip" + "zip3" + "zipWith" + "zipWith3" + "unzip" + "unzip3" + + ; String + "lines" + "words" + "unlines" + "unwords" + + ; Converting to String + "show" + "showList" + "shows" + "showChar" + "showString" + "showParen" + + ; Converting from String + "readsPrec" + "readList" + "reads" + "readParen" + "read" + "lex" + + ; Input and output + "putChar" + "putStr" + "putStrLn" + "print" + "getChar" + "getLine" + "getContents" + "interact" + + ; Files + "readFile" + "writeFile" + "appendFile" + "readIO" + "readLn" + + ; Exception handling + "ioError" + "userError") + ) +) + + (con_unit) @constant.builtin ; unit, as in () (comment) @comment diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 5cfbff59..898bde6a 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -63,6 +63,14 @@ (#any-of? @type.enum.variant.builtin "Some" "None" "Ok" "Err")) +(call_expression + (identifier) @function.builtin + (#any-of? @function.builtin + "drop" + "size_of" + "size_of_val" + "align_of" + "align_of_val")) ((type_identifier) @type.builtin (#any-of? From b05971f17863c0887c4da2ec891ef5e30ff8577d Mon Sep 17 00:00:00 2001 From: meator Date: Sat, 11 Jan 2025 22:12:46 +0100 Subject: [PATCH 14/23] Add .clang-tidy highlighting (#12498) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index e593b2ed..a83a6f0d 100644 --- a/languages.toml +++ b/languages.toml @@ -1350,7 +1350,7 @@ source = { git = "https://github.com/ikatyang/tree-sitter-vue", rev = "91fe27547 [[language]] name = "yaml" scope = "source.yaml" -file-types = ["yml", "yaml", { glob = ".prettierrc" }, { glob = ".clangd" }, { glob = ".clang-format" }] +file-types = ["yml", "yaml", { glob = ".prettierrc" }, { glob = ".clangd" }, { glob = ".clang-format" }, { glob = ".clang-tidy" }] comment-token = "#" indent = { tab-width = 2, unit = " " } language-servers = [ "yaml-language-server", "ansible-language-server" ] From 0f2ce303c5cac34ba88469d3ee13d44967f3e903 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 11 Jan 2025 20:38:29 -0500 Subject: [PATCH 15/23] Add directory name to `:cd` errors For example `:cd README.md` would say "Not a directory" but would not print the directory name. Now the error message includes some context about the operation and requested directory. --- helix-term/src/commands/typed.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 078bb800..4a2546d7 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1105,7 +1105,12 @@ fn change_current_directory( None => Cow::Owned(home_dir()?), }; - cx.editor.set_cwd(&dir)?; + cx.editor.set_cwd(&dir).map_err(|err| { + anyhow!( + "Could not change working directory to '{}': {err}", + dir.display() + ) + })?; cx.editor.set_status(format!( "Current working directory is now {}", From e01775a6677df12a94e99938b52771974af7c30c Mon Sep 17 00:00:00 2001 From: Nikita Revenco <154856872+nik-rev@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:40:19 +0000 Subject: [PATCH 16/23] fix: unable to detect Color completion item hex code for some LSPs (#12501) Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com> --- helix-term/src/ui/completion.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index adacfad3..030085af 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -92,8 +92,11 @@ impl menu::Item for CompletionItem { value, .. }) => value, }; - Color::from_hex(text) + // Language servers which send Color completion items tend to include a 6 + // digit hex code at the end for the color. The extra 1 digit is for the '#' + text.get(text.len().checked_sub(7)?..) }) + .and_then(Color::from_hex) .map_or("color".into(), |color| { Spans::from(vec![ Span::raw("color "), From 367ccc1c64249cc1b3be8a254cdf087beaf56e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lan=20Cr=C3=ADstoffer?= Date: Mon, 13 Jan 2025 14:43:02 +0100 Subject: [PATCH 17/23] Fix a bug in matlab indentation and updates the grammar commit hash to latest (#12518) --- languages.toml | 2 +- runtime/queries/matlab/indents.scm | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index a83a6f0d..01841727 100644 --- a/languages.toml +++ b/languages.toml @@ -2900,7 +2900,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "matlab" -source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "6071891a8c39600203eba20513666cf93b4d650a" } +source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "b0a0198b182574cd3ca0447264c83331901b9338" } [[language]] name = "ponylang" diff --git a/runtime/queries/matlab/indents.scm b/runtime/queries/matlab/indents.scm index b2a8e55d..4aacccd8 100644 --- a/runtime/queries/matlab/indents.scm +++ b/runtime/queries/matlab/indents.scm @@ -1,4 +1,5 @@ [ + (arguments_statement) (if_statement) (for_statement) (while_statement) From 134aebf8ccc6a2f204213f74f3d265a33679ae96 Mon Sep 17 00:00:00 2001 From: "Taylor C. Richberger" Date: Mon, 13 Jan 2025 06:45:38 -0700 Subject: [PATCH 18/23] add `rockspec` to lua file types (#12516) --- languages.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/languages.toml b/languages.toml index 01841727..51c1a110 100644 --- a/languages.toml +++ b/languages.toml @@ -1295,7 +1295,7 @@ formatter = { command = "dune", args = ["format-dune-file"] } name = "lua" injection-regex = "lua" scope = "source.lua" -file-types = ["lua"] +file-types = ["lua", "rockspec"] shebangs = ["lua", "luajit"] roots = [".luarc.json", ".luacheckrc", ".stylua.toml", "selene.toml", ".git"] comment-token = "--" From 60bff8feeec6d5b1881b26b9fdbcdaf9afd1749d Mon Sep 17 00:00:00 2001 From: TornaxO7 Date: Mon, 13 Jan 2025 15:14:30 +0100 Subject: [PATCH 19/23] Fix `open_{below, above}` behaviour with multiple cursors (#12465) --- helix-term/src/commands.rs | 6 +- helix-term/tests/test/commands/insert.rs | 121 +++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3b906487..755a7dc0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -3474,6 +3474,7 @@ fn open(cx: &mut Context, open: Open) { let text = doc.text().slice(..); let contents = doc.text(); let selection = doc.selection(view.id); + let mut offs = 0; let mut ranges = SmallVec::with_capacity(selection.len()); @@ -3550,7 +3551,7 @@ fn open(cx: &mut Context, open: Open) { let text = text.repeat(count); // calculate new selection ranges - let pos = above_next_line_end_index + above_next_line_end_width; + let pos = offs + above_next_line_end_index + above_next_line_end_width; let comment_len = continue_comment_token .map(|token| token.len() + 1) // `+ 1` for the extra space added .unwrap_or_default(); @@ -3563,6 +3564,9 @@ fn open(cx: &mut Context, open: Open) { )); } + // update the offset for the next range + offs += text.chars().count(); + ( above_next_line_end_index, above_next_line_end_index, diff --git a/helix-term/tests/test/commands/insert.rs b/helix-term/tests/test/commands/insert.rs index f7aa4a02..f23876df 100644 --- a/helix-term/tests/test/commands/insert.rs +++ b/helix-term/tests/test/commands/insert.rs @@ -184,6 +184,127 @@ async fn test_open_above() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_open_above_with_multiple_cursors() -> anyhow::Result<()> { + // the primary cursor is also in the top line + test(( + indoc! {"#[H|]#elix + #(i|)#s + #(c|)#ool"}, + "O", + indoc! { + "#[\n|]# + Helix + #(\n|)# + is + #(\n|)# + cool + " + }, + )) + .await?; + + // now with some additional indentation + test(( + indoc! {"····#[H|]#elix + ····#(i|)#s + ····#(c|)#ool"} + .replace("·", " "), + ":indent-style 4O", + indoc! { + "····#[\n|]# + ····Helix + ····#(\n|)# + ····is + ····#(\n|)# + ····cool + " + } + .replace("·", " "), + )) + .await?; + + // the first line is within a comment, the second not. + // However, if we open above, the first newly added line should start within a comment + // while the other should be a normal line + test(( + indoc! {"fn main() { + // #[VIP|]# comment + l#(e|)#t yes = false; + }"}, + ":lang rustO", + indoc! {"fn main() { + // #[\n|]# + // VIP comment + #(\n|)# + let yes = false; + }"}, + )) + .await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_open_below_with_multiple_cursors() -> anyhow::Result<()> { + // the primary cursor is also in the top line + test(( + indoc! {"#[H|]#elix + #(i|)#s + #(c|)#ool"}, + "o", + indoc! {"Helix + #[\n|]# + is + #(\n|)# + cool + #(\n|)# + " + }, + )) + .await?; + + // now with some additional indentation + test(( + indoc! {"····#[H|]#elix + ····#(i|)#s + ····#(c|)#ool"} + .replace("·", " "), + ":indent-style 4o", + indoc! { + "····Helix + ····#[\n|]# + ····is + ····#(\n|)# + ····cool + ····#(\n|)# + " + } + .replace("·", " "), + )) + .await?; + + // the first line is within a comment, the second not. + // However, if we open below, the first newly added line should start within a comment + // while the other should be a normal line + test(( + indoc! {"fn main() { + // #[VIP|]# comment + l#(e|)#t yes = false; + }"}, + ":lang rusto", + indoc! {"fn main() { + // VIP comment + // #[\n|]# + let yes = false; + #(\n|)# + }"}, + )) + .await?; + + Ok(()) +} + /// NOTE: To make the `open_above` comment-aware, we're setting the language for each test to rust. #[tokio::test(flavor = "multi_thread")] async fn test_open_above_with_comments() -> anyhow::Result<()> { From 3d772afc8b6e2c775d4a4352292240582174e670 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 23:09:54 +0000 Subject: [PATCH 20/23] build(deps): bump the rust-dependencies group with 6 updates Bumps the rust-dependencies group with 6 updates: | Package | From | To | | --- | --- | --- | | [thiserror](https://github.com/dtolnay/thiserror) | `2.0.9` | `2.0.11` | | [bitflags](https://github.com/bitflags/bitflags) | `2.6.0` | `2.7.0` | | [serde_json](https://github.com/serde-rs/json) | `1.0.134` | `1.0.135` | | [tokio](https://github.com/tokio-rs/tokio) | `1.42.0` | `1.43.0` | | [rustix](https://github.com/bytecodealliance/rustix) | `0.38.42` | `0.38.43` | | [cc](https://github.com/rust-lang/cc-rs) | `1.2.7` | `1.2.9` | Updates `thiserror` from 2.0.9 to 2.0.11 - [Release notes](https://github.com/dtolnay/thiserror/releases) - [Commits](https://github.com/dtolnay/thiserror/compare/2.0.9...2.0.11) Updates `bitflags` from 2.6.0 to 2.7.0 - [Release notes](https://github.com/bitflags/bitflags/releases) - [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md) - [Commits](https://github.com/bitflags/bitflags/compare/2.6.0...2.7.0) Updates `serde_json` from 1.0.134 to 1.0.135 - [Release notes](https://github.com/serde-rs/json/releases) - [Commits](https://github.com/serde-rs/json/compare/v1.0.134...v1.0.135) Updates `tokio` from 1.42.0 to 1.43.0 - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.42.0...tokio-1.43.0) Updates `rustix` from 0.38.42 to 0.38.43 - [Release notes](https://github.com/bytecodealliance/rustix/releases) - [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGELOG.md) - [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.42...v0.38.43) Updates `cc` from 1.2.7 to 1.2.9 - [Release notes](https://github.com/rust-lang/cc-rs/releases) - [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md) - [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.7...cc-v1.2.9) --- updated-dependencies: - dependency-name: thiserror dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: bitflags dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: serde_json dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor dependency-group: rust-dependencies - dependency-name: rustix dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies - dependency-name: cc dependency-type: direct:production update-type: version-update:semver-patch dependency-group: rust-dependencies ... Signed-off-by: dependabot[bot] --- Cargo.lock | 114 ++++++++++++++++++------------------- helix-core/Cargo.toml | 2 +- helix-lsp-types/Cargo.toml | 4 +- helix-lsp/Cargo.toml | 2 +- helix-stdx/Cargo.toml | 2 +- helix-tui/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d284dd7d..d7ed00e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,9 +101,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "bstr" @@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.2.7" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" dependencies = [ "shlex", ] @@ -530,7 +530,7 @@ dependencies = [ "gix-worktree", "once_cell", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -543,7 +543,7 @@ dependencies = [ "gix-date", "gix-utils", "itoa", - "thiserror 2.0.9", + "thiserror 2.0.11", "winnow", ] @@ -560,7 +560,7 @@ dependencies = [ "gix-trace", "kstring", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", "unicode-bom", ] @@ -570,7 +570,7 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53" dependencies = [ - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -579,7 +579,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7" dependencies = [ - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -605,7 +605,7 @@ dependencies = [ "gix-features", "gix-hash", "memmap2", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -624,7 +624,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", "unicode-bom", "winnow", ] @@ -639,7 +639,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -651,7 +651,7 @@ dependencies = [ "bstr", "itoa", "jiff", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -672,7 +672,7 @@ dependencies = [ "gix-traverse", "gix-worktree", "imara-diff", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -692,7 +692,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -708,7 +708,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -726,7 +726,7 @@ dependencies = [ "once_cell", "prodash", "sha1_smol", - "thiserror 2.0.9", + "thiserror 2.0.11", "walkdir", ] @@ -748,7 +748,7 @@ dependencies = [ "gix-trace", "gix-utils", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -781,7 +781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" dependencies = [ "faster-hex", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -833,7 +833,7 @@ dependencies = [ "memmap2", "rustix", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -864,7 +864,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", "winnow", ] @@ -886,7 +886,7 @@ dependencies = [ "gix-quote", "parking_lot", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -904,7 +904,7 @@ dependencies = [ "gix-path", "memmap2", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -916,7 +916,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -928,7 +928,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -941,7 +941,7 @@ dependencies = [ "gix-trace", "home", "once_cell", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -956,7 +956,7 @@ dependencies = [ "gix-config-value", "gix-glob", "gix-path", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -974,7 +974,7 @@ dependencies = [ "gix-transport", "gix-utils", "maybe-async", - "thiserror 2.0.9", + "thiserror 2.0.11", "winnow", ] @@ -986,7 +986,7 @@ checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63" dependencies = [ "bstr", "gix-utils", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1006,7 +1006,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror 2.0.9", + "thiserror 2.0.11", "winnow", ] @@ -1021,7 +1021,7 @@ dependencies = [ "gix-revision", "gix-validate", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1036,7 +1036,7 @@ dependencies = [ "gix-hash", "gix-object", "gix-revwalk", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1051,7 +1051,7 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1075,7 +1075,7 @@ dependencies = [ "bstr", "gix-hash", "gix-lock", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1098,7 +1098,7 @@ dependencies = [ "gix-pathspec", "gix-worktree", "portable-atomic", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1113,7 +1113,7 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1149,7 +1149,7 @@ dependencies = [ "gix-quote", "gix-sec", "gix-url", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1166,7 +1166,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1179,7 +1179,7 @@ dependencies = [ "gix-features", "gix-path", "percent-encoding", - "thiserror 2.0.9", + "thiserror 2.0.11", "url", ] @@ -1201,7 +1201,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937" dependencies = [ "bstr", - "thiserror 2.0.9", + "thiserror 2.0.11", ] [[package]] @@ -1335,7 +1335,7 @@ dependencies = [ "log", "serde", "serde_json", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", ] @@ -1391,7 +1391,7 @@ dependencies = [ "serde", "serde_json", "slotmap", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-stream", ] @@ -1466,7 +1466,7 @@ dependencies = [ "smallvec", "tempfile", "termini", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-stream", "toml", @@ -1533,7 +1533,7 @@ dependencies = [ "serde_json", "slotmap", "tempfile", - "thiserror 2.0.9", + "thiserror 2.0.11", "tokio", "tokio-stream", "toml", @@ -2213,9 +2213,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.42" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ "bitflags", "errno", @@ -2267,9 +2267,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", @@ -2493,11 +2493,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl 2.0.11", ] [[package]] @@ -2513,9 +2513,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -2558,9 +2558,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -2576,9 +2576,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index d245ec13..5dea37b0 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -36,7 +36,7 @@ tree-sitter.workspace = true once_cell = "1.20" arc-swap = "1" regex = "1" -bitflags = "2.6" +bitflags = "2.7" ahash = "0.8.11" hashbrown = { version = "0.14.5", features = ["raw"] } dunce = "1.0" diff --git a/helix-lsp-types/Cargo.toml b/helix-lsp-types/Cargo.toml index fa3f3aba..e863da8d 100644 --- a/helix-lsp-types/Cargo.toml +++ b/helix-lsp-types/Cargo.toml @@ -21,9 +21,9 @@ keywords = ["language", "server", "lsp", "vscode", "lsif"] license = "MIT" [dependencies] -bitflags = "2.6.0" +bitflags = "2.7.0" serde = { version = "1.0.217", features = ["derive"] } -serde_json = "1.0.134" +serde_json = "1.0.135" serde_repr = "0.1" url = {version = "2.5.4", features = ["serde"]} diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index c68097ee..12da18e9 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -26,7 +26,7 @@ globset = "0.4.15" log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.43", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.17" parking_lot = "0.12.3" arc-swap = "1" diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index dd943eeb..b9102cec 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -17,7 +17,7 @@ etcetera = "0.8" ropey = { version = "1.6.1", default-features = false } which = "7.0" regex-cursor = "0.1.4" -bitflags = "2.6" +bitflags = "2.7" once_cell = "1.19" regex-automata = "0.4.9" diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 96f008a0..6da97aad 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -18,7 +18,7 @@ default = ["crossterm"] helix-view = { path = "../helix-view", features = ["term"] } helix-core = { path = "../helix-core" } -bitflags = "2.6" +bitflags = "2.7" cassowary = "0.3" unicode-segmentation = "1.12" crossterm = { version = "0.28", optional = true } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 82e5fd92..268fb732 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -24,7 +24,7 @@ helix-lsp = { path = "../helix-lsp" } helix-dap = { path = "../helix-dap" } helix-vcs = { path = "../helix-vcs" } -bitflags = "2.6" +bitflags = "2.7" anyhow = "1" crossterm = { version = "0.28", optional = true } From 27bb2447db69b6f72a61fc4183035c5ba162d7b2 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 13 Jan 2025 18:26:31 -0500 Subject: [PATCH 21/23] Use a workspace dependency for bitflags --- Cargo.toml | 1 + helix-core/Cargo.toml | 2 +- helix-lsp-types/Cargo.toml | 2 +- helix-stdx/Cargo.toml | 2 +- helix-tui/Cargo.toml | 2 +- helix-view/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ace374a3..0ae0b41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ nucleo = "0.5.0" slotmap = "1.0.7" thiserror = "2.0" tempfile = "3.15.0" +bitflags = "2.7" [workspace.package] version = "25.1.0" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 5dea37b0..b8821c41 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -36,7 +36,7 @@ tree-sitter.workspace = true once_cell = "1.20" arc-swap = "1" regex = "1" -bitflags = "2.7" +bitflags.workspace = true ahash = "0.8.11" hashbrown = { version = "0.14.5", features = ["raw"] } dunce = "1.0" diff --git a/helix-lsp-types/Cargo.toml b/helix-lsp-types/Cargo.toml index e863da8d..9f9900fe 100644 --- a/helix-lsp-types/Cargo.toml +++ b/helix-lsp-types/Cargo.toml @@ -21,7 +21,7 @@ keywords = ["language", "server", "lsp", "vscode", "lsif"] license = "MIT" [dependencies] -bitflags = "2.7.0" +bitflags.workspace = true serde = { version = "1.0.217", features = ["derive"] } serde_json = "1.0.135" serde_repr = "0.1" diff --git a/helix-stdx/Cargo.toml b/helix-stdx/Cargo.toml index b9102cec..d575a28f 100644 --- a/helix-stdx/Cargo.toml +++ b/helix-stdx/Cargo.toml @@ -17,7 +17,7 @@ etcetera = "0.8" ropey = { version = "1.6.1", default-features = false } which = "7.0" regex-cursor = "0.1.4" -bitflags = "2.7" +bitflags.workspace = true once_cell = "1.19" regex-automata = "0.4.9" diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 6da97aad..97765800 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -18,7 +18,7 @@ default = ["crossterm"] helix-view = { path = "../helix-view", features = ["term"] } helix-core = { path = "../helix-core" } -bitflags = "2.7" +bitflags.workspace = true cassowary = "0.3" unicode-segmentation = "1.12" crossterm = { version = "0.28", optional = true } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 268fb732..b40f2589 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -24,7 +24,7 @@ helix-lsp = { path = "../helix-lsp" } helix-dap = { path = "../helix-dap" } helix-vcs = { path = "../helix-vcs" } -bitflags = "2.7" +bitflags.workspace = true anyhow = "1" crossterm = { version = "0.28", optional = true } From f69659c5bedd5401d24faae43137ef9d20967361 Mon Sep 17 00:00:00 2001 From: Robin Heggelund Hansen Date: Tue, 14 Jan 2025 15:26:56 +0100 Subject: [PATCH 22/23] Add support for the Gren programming language (#12525) --- book/src/generated/lang-support.md | 1 + languages.toml | 14 +++++ runtime/queries/gren/highlights.scm | 81 ++++++++++++++++++++++++++++ runtime/queries/gren/locals.scm | 14 +++++ runtime/queries/gren/tags.scm | 19 +++++++ runtime/queries/gren/textobjects.scm | 56 +++++++++++++++++++ 6 files changed, 185 insertions(+) create mode 100644 runtime/queries/gren/highlights.scm create mode 100644 runtime/queries/gren/locals.scm create mode 100644 runtime/queries/gren/tags.scm create mode 100644 runtime/queries/gren/textobjects.scm diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 7ec7ec5a..376080b5 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -81,6 +81,7 @@ | gowork | ✓ | | | `gopls` | | gpr | ✓ | | | `ada_language_server` | | graphql | ✓ | ✓ | | `graphql-lsp` | +| gren | ✓ | ✓ | | | | groovy | ✓ | | | | | gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` | | hare | ✓ | | | | diff --git a/languages.toml b/languages.toml index 51c1a110..68b28001 100644 --- a/languages.toml +++ b/languages.toml @@ -4051,3 +4051,17 @@ language-servers = ["codeql"] [[grammar]] name = "ql" source = { git = "https://github.com/tree-sitter/tree-sitter-ql", rev = "1fd627a4e8bff8c24c11987474bd33112bead857" } + +[[language]] +name = "gren" +scope = "source.gren" +injection-regex = "gren" +file-types = ["gren"] +roots = ["gren.json"] +comment-tokens = "--" +block-comment-tokens = { start = "{-", end = "-}" } +indent = { tab-width = 4, unit = " " } + +[[grammar]] +name = "gren" +source = { git = "https://github.com/MaeBrooks/tree-sitter-gren", rev = "76554f4f2339f5a24eed19c58f2079b51c694152" } diff --git a/runtime/queries/gren/highlights.scm b/runtime/queries/gren/highlights.scm new file mode 100644 index 00000000..d38523cf --- /dev/null +++ b/runtime/queries/gren/highlights.scm @@ -0,0 +1,81 @@ +; Keywords +[ + "if" + "then" + "else" + "let" + "in" + ] @keyword.control +(when) @keyword.control +(is) @keyword.control + +(colon) @keyword.operator +(backslash) @keyword +(as) @keyword +(port) @keyword +(exposing) @keyword +(alias) @keyword +(infix) @keyword + +(arrow) @keyword.operator +(dot) @keyword.operator + +(type_annotation(lower_case_identifier) @function) +(port_annotation(lower_case_identifier) @function) +(file (value_declaration (function_declaration_left(lower_case_identifier) @function))) + +(field name: (lower_case_identifier) @attribute) +(field_access_expr(lower_case_identifier) @attribute) + +(operator_identifier) @keyword.operator +(eq) @keyword.operator.assignment + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +"|" @keyword +"," @punctuation.delimiter + +[ + "|>" +] @keyword + + +(import) @keyword.control.import +(module) @keyword.other + +(number_constant_expr) @constant.numeric + +(type) @type + +(type_declaration(upper_case_identifier) @type) +(type_ref) @type +(type_alias_declaration name: (upper_case_identifier) @type) + +(union_pattern constructor: (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member)) +(union_pattern constructor: (upper_case_qid (upper_case_identifier) @variable.other.member)) + +(union_variant(upper_case_identifier) @variable.other.member) +(value_expr name: (value_qid (upper_case_identifier) @label)) +(value_expr (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member)) +(value_expr(upper_case_qid(upper_case_identifier)) @variable.other.member) + +; comments +(line_comment) @comment +(block_comment) @comment + +; strings +(string_escape) @constant.character.escape + +(open_quote) @string +(close_quote) @string +(regular_string_part) @string + +(open_char) @constant.character +(close_char) @constant.character diff --git a/runtime/queries/gren/locals.scm b/runtime/queries/gren/locals.scm new file mode 100644 index 00000000..ab103115 --- /dev/null +++ b/runtime/queries/gren/locals.scm @@ -0,0 +1,14 @@ +(value_declaration) @local.scope +(type_alias_declaration) @local.scope +(type_declaration) @local.scope +(type_annotation) @local.scope +(port_annotation) @local.scope +(infix_declaration) @local.scope +(let_in_expr) @local.scope + +(function_declaration_left (lower_pattern (lower_case_identifier)) @local.definition) +(function_declaration_left (lower_case_identifier) @local.definition) + +(value_expr(value_qid(upper_case_identifier)) @local.reference) +(value_expr(value_qid(lower_case_identifier)) @local.reference) +(type_ref (upper_case_qid) @local.reference) diff --git a/runtime/queries/gren/tags.scm b/runtime/queries/gren/tags.scm new file mode 100644 index 00000000..d6ac5cd9 --- /dev/null +++ b/runtime/queries/gren/tags.scm @@ -0,0 +1,19 @@ +(value_declaration (function_declaration_left (lower_case_identifier) @name)) @definition.function + +(function_call_expr (value_expr (value_qid) @name)) @reference.function +(exposed_value (lower_case_identifier) @name) @reference.function +(type_annotation ((lower_case_identifier) @name) (colon)) @reference.function + +(type_declaration ((upper_case_identifier) @name) ) @definition.type + +(type_ref (upper_case_qid (upper_case_identifier) @name)) @reference.type +(exposed_type (upper_case_identifier) @name) @reference.type + +(type_declaration (union_variant (upper_case_identifier) @name)) @definition.union + +(value_expr (upper_case_qid (upper_case_identifier) @name)) @reference.union + + +(module_declaration + (upper_case_qid (upper_case_identifier)) @name +) @definition.module \ No newline at end of file diff --git a/runtime/queries/gren/textobjects.scm b/runtime/queries/gren/textobjects.scm new file mode 100644 index 00000000..38565784 --- /dev/null +++ b/runtime/queries/gren/textobjects.scm @@ -0,0 +1,56 @@ + +(line_comment) @comment.inside +(line_comment)+ @comment.around +(block_comment) @comment.inside +(block_comment)+ @comment.around + +((type_annotation)? + (value_declaration + (function_declaration_left (lower_case_identifier)) + (eq) + (_) @function.inside + ) +) @function.around + +(parenthesized_expr + (anonymous_function_expr + ( + (arrow) + (_) @function.inside + ) + ) +) @function.around + +(value_declaration + (function_declaration_left + (lower_pattern + (lower_case_identifier) @parameter.inside @parameter.around + ) + ) +) + +(value_declaration + (function_declaration_left + (pattern) @parameter.inside @parameter.around + ) +) + +(value_declaration + (function_declaration_left + (record_pattern + (lower_pattern + (lower_case_identifier) @parameter.inside + ) + ) @parameter.around + ) +) + +(parenthesized_expr + (anonymous_function_expr + ( + (backslash) + (pattern) @parameter.inside + (arrow) + ) + ) +) From ca19496eed655db1d2ac49eb6e15c28b8c616372 Mon Sep 17 00:00:00 2001 From: David Else <12832280+David-Else@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:55:01 +0000 Subject: [PATCH 23/23] Improve `dark_plus` theme: Change `special`, `ui.text.directory` and `ui.virtual.wrap` (#12530) --- runtime/themes/dark_plus.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index 61878664..28ec163d 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -24,7 +24,7 @@ "operator" = "text" "punctuation" = "text" "punctuation.delimiter" = "text" -"special" = "text" +"special" = "light_blue" "string" = "orange" "string.regexp" = "gold" "tag" = "blue2" @@ -72,9 +72,10 @@ "ui.bufferline.background" = { bg = "background" } "ui.text" = { fg = "text" } "ui.text.focus" = { fg = "white" } -"ui.text.directory" = { fg = "blue3" } +"ui.text.directory" = { fg = "blue2" } "ui.text.inactive" = { fg = "dark_gray" } "ui.virtual.whitespace" = { fg = "#3e3e3d" } +"ui.virtual.wrap" = { fg = "#3e3e3d" } "ui.virtual.ruler" = { bg = "borders" } "ui.virtual.indent-guide" = { fg = "dark_gray4" } "ui.virtual.inlay-hint" = { fg = "dark_gray5"}