From dcb0b0f248c350d55c29ad152f4d3eb02b5e8a4e Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Tue, 7 Feb 2023 09:18:44 +0100 Subject: [PATCH 01/12] Add a parser-combinator crate Parser-combinators are one of the simpler tools for building ad-hoc parsers. They're a good fit because they are... * Small: each parser / parser-combinator is around 10 LOC. * Functional: helix_core strives to be a functional set of utilities usable throughout the rest of the editor. * Flexible: use them to build any sort of ad-hoc parser. In the child commit, we'll parse LSP Snippet syntax using these new parser combinators. Why not use an existing parser-combinator crate? Existing popular parser-combinator crates have histories of making breaking changes (for example nom and combine). > Implementation note: I tried to not introduce a new trait since the > types can be expressed in terms of `impl Fn`s. The trait is necessary > to build `seq` implementations without a proc macro though, and also > allows us to use `&'static str`s very conveniently: see the trait > implementation for `&'static str`. --- Cargo.lock | 7 + Cargo.toml | 1 + helix-parsec/Cargo.toml | 14 + helix-parsec/src/lib.rs | 560 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 582 insertions(+) create mode 100644 helix-parsec/Cargo.toml create mode 100644 helix-parsec/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b70f34c412b1..4f6e6115a76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1192,6 +1192,13 @@ dependencies = [ "which", ] +[[package]] +name = "helix-parsec" +version = "0.6.0" +dependencies = [ + "regex", +] + [[package]] name = "helix-term" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index c7e254728c31..c6351889748d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "helix-dap", "helix-loader", "helix-vcs", + "helix-parsec", "xtask", ] diff --git a/helix-parsec/Cargo.toml b/helix-parsec/Cargo.toml new file mode 100644 index 000000000000..562df8ddd147 --- /dev/null +++ b/helix-parsec/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "helix-parsec" +version = "0.6.0" +authors = ["Blaž Hrastnik "] +edition = "2021" +license = "MPL-2.0" +description = "Parser combinators for Helix" +categories = ["editor"] +repository = "https://github.com/helix-editor/helix" +homepage = "https://helix-editor.com" +include = ["src/**/*", "README.md"] + +[dependencies] +regex = "1" diff --git a/helix-parsec/src/lib.rs b/helix-parsec/src/lib.rs new file mode 100644 index 000000000000..c86a1a056a6d --- /dev/null +++ b/helix-parsec/src/lib.rs @@ -0,0 +1,560 @@ +//! Parser-combinator functions +//! +//! This module provides parsers and parser combinators which can be used +//! together to build parsers by functional composition. + +use regex::Regex; + +// This module implements parser combinators following https://bodil.lol/parser-combinators/. +// `sym` (trait implementation for `&'static str`), `map`, `pred` (filter), `one_or_more`, +// `zero_or_more`, as well as the `Parser` trait originate mostly from that post. +// The remaining parsers and parser combinators are either based on +// https://github.com/archseer/snippets.nvim/blob/a583da6ef130d2a4888510afd8c4e5ffd62d0dce/lua/snippet/parser.lua#L5-L138 +// or are novel. + +// When a parser matches the input successfully, it returns `Ok((next_input, some_value))` +// where the type of the returned value depends on the parser. If the parser fails to match, +// it returns `Err(input)`. +type ParseResult<'a, Output> = Result<(&'a str, Output), &'a str>; + +/// A parser or parser-combinator. +/// +/// Parser-combinators compose multiple parsers together to parse input. +/// For example, two basic parsers (`&'static str`s) may be combined with +/// a parser-combinator like [or] to produce a new parser. +/// +/// ``` +/// use helix_parsec::{or, Parser}; +/// let foo = "foo"; // matches "foo" literally +/// let bar = "bar"; // matches "bar" literally +/// let foo_or_bar = or(foo, bar); // matches either "foo" or "bar" +/// assert_eq!(Ok(("", "foo")), foo_or_bar.parse("foo")); +/// assert_eq!(Ok(("", "bar")), foo_or_bar.parse("bar")); +/// assert_eq!(Err("baz"), foo_or_bar.parse("baz")); +/// ``` +pub trait Parser<'a> { + type Output; + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output>; +} + +// Most parser-combinators are written as higher-order functions which take some +// parser(s) as input and return a new parser: a function that takes input and returns +// a parse result. The underlying implementation of [Parser::parse] for these functions +// is simply application. +#[doc(hidden)] +impl<'a, F, T> Parser<'a> for F +where + F: Fn(&'a str) -> ParseResult, +{ + type Output = T; + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output> { + self(input) + } +} + +/// A parser which matches the string literal exactly. +/// +/// This parser succeeds if the next characters in the input are equal to the given +/// string literal. +/// +/// Note that [str::parse] interferes with calling [Parser::parse] on string literals +/// directly; this trait implementation works when used within any parser combinator +/// but does not work on its own. To call [Parser::parse] on a parser for a string +/// literal, use the [token] parser. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{or, Parser}; +/// let parser = or("foo", "bar"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Ok(("", "bar")), parser.parse("bar")); +/// assert_eq!(Err("baz"), parser.parse("baz")); +/// ``` +impl<'a> Parser<'a> for &'static str { + type Output = &'a str; + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output> { + match input.get(0..self.len()) { + Some(actual) if actual == *self => Ok((&input[self.len()..], &input[0..self.len()])), + _ => Err(input), + } + } +} + +// Parsers + +/// A parser which matches the given string literally. +/// +/// This function is a convenience for interpreting string literals as parsers +/// and is only necessary to avoid conflict with [str::parse]. See the documentation +/// for the `&'static str` implementation of [Parser]. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{token, Parser}; +/// let parser = token("foo"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Err("bar"), parser.parse("bar")); +/// ``` +pub fn token<'a>(literal: &'static str) -> impl Parser<'a, Output = &'a str> { + literal +} + +/// A parser which matches the pattern described by the given regular expression. +/// +/// The pattern must match from the beginning of the input as if the regular expression +/// included the `^` anchor. Using a `^` anchor in the regular expression is +/// recommended in order to reduce any work done by the regex on non-matching input. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{pattern, Parser}; +/// use regex::Regex; +/// let regex = Regex::new(r"Hello, \w+!").unwrap(); +/// let parser = pattern(®ex); +/// assert_eq!(Ok(("", "Hello, world!")), parser.parse("Hello, world!")); +/// assert_eq!(Err("Hey, you!"), parser.parse("Hey, you!")); +/// assert_eq!(Err("Oh Hello, world!"), parser.parse("Oh Hello, world!")); +/// ``` +pub fn pattern<'a>(regex: &'a Regex) -> impl Parser<'a, Output = &'a str> { + move |input: &'a str| match regex.find(input) { + Some(match_) if match_.start() == 0 => { + Ok((&input[match_.end()..], &input[0..match_.end()])) + } + _ => Err(input), + } +} + +/// A parser which matches all values until the specified pattern is found. +/// +/// If the pattern is not found, this parser does not match. The input up to the +/// character which returns `true` is returned but not that character itself. +/// +/// If the pattern function returns true on the first input character, this +/// parser fails. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{take_until, Parser}; +/// let parser = take_until(|c| c == '.'); +/// assert_eq!(Ok((".bar", "foo")), parser.parse("foo.bar")); +/// assert_eq!(Err(".foo"), parser.parse(".foo")); +/// assert_eq!(Err("foo"), parser.parse("foo")); +/// ``` +pub fn take_until<'a, F>(pattern: F) -> impl Parser<'a, Output = &'a str> +where + F: Fn(char) -> bool, +{ + move |input: &'a str| match input.find(&pattern) { + Some(index) if index != 0 => Ok((&input[index..], &input[0..index])), + _ => Err(input), + } +} + +// Variadic parser combinators + +/// A parser combinator which matches a sequence of parsers in an all-or-nothing fashion. +/// +/// The returned value is a tuple containing the outputs of all parsers in order. Each +/// parser in the sequence may be typed differently. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{seq, Parser}; +/// let parser = seq!("<", "a", ">"); +/// assert_eq!(Ok(("", ("<", "a", ">"))), parser.parse("")); +/// assert_eq!(Err(""), parser.parse("")); +/// ``` +#[macro_export] +macro_rules! seq { + ($($parsers: expr),+ $(,)?) => { + ($($parsers),+) + } +} + +// Seq is implemented using trait-implementations of Parser for various size tuples. +// This allows sequences to be typed heterogeneously. +macro_rules! seq_impl { + ($($parser:ident),+) => { + #[allow(non_snake_case)] + impl<'a, $($parser),+> Parser<'a> for ($($parser),+) + where + $($parser: Parser<'a>),+ + { + type Output = ($($parser::Output),+); + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output> { + let ($($parser),+) = self; + seq_body_impl!(input, input, $($parser),+ ; ) + } + } + } +} + +macro_rules! seq_body_impl { + ($input:expr, $next_input:expr, $head:ident, $($tail:ident),+ ; $(,)? $($acc:ident),*) => { + match $head.parse($next_input) { + Ok((next_input, $head)) => seq_body_impl!($input, next_input, $($tail),+ ; $($acc),*, $head), + Err(_) => Err($input), + } + }; + ($input:expr, $next_input:expr, $last:ident ; $(,)? $($acc:ident),*) => { + match $last.parse($next_input) { + Ok((next_input, last)) => Ok((next_input, ($($acc),+, last))), + Err(_) => Err($input), + } + } +} + +seq_impl!(A, B); +seq_impl!(A, B, C); +seq_impl!(A, B, C, D); +seq_impl!(A, B, C, D, E); +seq_impl!(A, B, C, D, E, F); +seq_impl!(A, B, C, D, E, F, G); +seq_impl!(A, B, C, D, E, F, G, H); +seq_impl!(A, B, C, D, E, F, G, H, I); +seq_impl!(A, B, C, D, E, F, G, H, I, J); + +/// A parser combinator which chooses the first of the input parsers which matches +/// successfully. +/// +/// All input parsers must have the same output type. This is a variadic form for [or]. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{choice, or, Parser}; +/// let parser = choice!("foo", "bar", "baz"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Ok(("", "bar")), parser.parse("bar")); +/// assert_eq!(Err("quiz"), parser.parse("quiz")); +/// ``` +#[macro_export] +macro_rules! choice { + ($parser: expr $(,)?) => { + $parser + }; + ($parser: expr, $($rest: expr),+ $(,)?) => { + or($parser, choice!($($rest),+)) + } +} + +// Ordinary parser combinators + +/// A parser combinator which takes a parser as input and maps the output using the +/// given transformation function. +/// +/// This corresponds to [Result::map]. The value is only mapped if the input parser +/// matches against input. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{map, Parser}; +/// let parser = map("123", |s| s.parse::().unwrap()); +/// assert_eq!(Ok(("", 123)), parser.parse("123")); +/// assert_eq!(Err("abc"), parser.parse("abc")); +/// ``` +pub fn map<'a, P, F, T>(parser: P, map_fn: F) -> impl Parser<'a, Output = T> +where + P: Parser<'a>, + F: Fn(P::Output) -> T, +{ + move |input| { + parser + .parse(input) + .map(|(next_input, result)| (next_input, map_fn(result))) + } +} + +/// A parser combinator which succeeds if the given parser matches the input and +/// the given `filter_map_fn` returns `Some`. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{filter_map, take_until, Parser}; +/// let parser = filter_map(take_until(|c| c == '.'), |s| s.parse::().ok()); +/// assert_eq!(Ok((".456", 123)), parser.parse("123.456")); +/// assert_eq!(Err("abc.def"), parser.parse("abc.def")); +/// ``` +pub fn filter_map<'a, P, F, T>(parser: P, filter_map_fn: F) -> impl Parser<'a, Output = T> +where + P: Parser<'a>, + F: Fn(P::Output) -> Option, +{ + move |input| match parser.parse(input) { + Ok((next_input, value)) => match filter_map_fn(value) { + Some(value) => Ok((next_input, value)), + None => Err(input), + }, + Err(_) => Err(input), + } +} + +/// A parser combinator which succeeds if the first given parser matches the input and +/// the second given parse also matches. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{reparse_as, take_until, one_or_more, Parser}; +/// let parser = reparse_as(take_until(|c| c == '/'), one_or_more("a")); +/// assert_eq!(Ok(("/bb", vec!["a", "a"])), parser.parse("aa/bb")); +/// ``` +pub fn reparse_as<'a, P1, P2, T>(parser1: P1, parser2: P2) -> impl Parser<'a, Output = T> +where + P1: Parser<'a, Output = &'a str>, + P2: Parser<'a, Output = T>, +{ + filter_map(parser1, move |str| { + parser2.parse(str).map(|(_, value)| value).ok() + }) +} + +/// A parser combinator which only matches the input when the predicate function +/// returns true. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{filter, take_until, Parser}; +/// let parser = filter(take_until(|c| c == '.'), |s| s == &"123"); +/// assert_eq!(Ok((".456", "123")), parser.parse("123.456")); +/// assert_eq!(Err("456.123"), parser.parse("456.123")); +/// ``` +pub fn filter<'a, P, F, T>(parser: P, pred_fn: F) -> impl Parser<'a, Output = T> +where + P: Parser<'a, Output = T>, + F: Fn(&P::Output) -> bool, +{ + move |input| { + if let Ok((next_input, value)) = parser.parse(input) { + if pred_fn(&value) { + return Ok((next_input, value)); + } + } + Err(input) + } +} + +/// A parser combinator which matches either of the input parsers. +/// +/// Both parsers must have the same output type. For a variadic form which +/// can take any number of parsers, use `choice!`. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{or, Parser}; +/// let parser = or("foo", "bar"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Ok(("", "bar")), parser.parse("bar")); +/// assert_eq!(Err("baz"), parser.parse("baz")); +/// ``` +pub fn or<'a, P1, P2, T>(parser1: P1, parser2: P2) -> impl Parser<'a, Output = T> +where + P1: Parser<'a, Output = T>, + P2: Parser<'a, Output = T>, +{ + move |input| match parser1.parse(input) { + ok @ Ok(_) => ok, + Err(_) => parser2.parse(input), + } +} + +/// A parser combinator which attempts to match the given parser, returning a +/// `None` output value if the parser does not match. +/// +/// The parser produced with this combinator always succeeds. If the given parser +/// succeeds, `Some(value)` is returned where `value` is the output of the given +/// parser. Otherwise, `None`. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{optional, Parser}; +/// let parser = optional("foo"); +/// assert_eq!(Ok(("bar", Some("foo"))), parser.parse("foobar")); +/// assert_eq!(Ok(("bar", None)), parser.parse("bar")); +/// ``` +pub fn optional<'a, P, T>(parser: P) -> impl Parser<'a, Output = Option> +where + P: Parser<'a, Output = T>, +{ + move |input| match parser.parse(input) { + Ok((next_input, value)) => Ok((next_input, Some(value))), + Err(_) => Ok((input, None)), + } +} + +/// A parser combinator which runs the given parsers in sequence and returns the +/// value of `left` if both are matched. +/// +/// This is useful for two-element sequences in which you only want the output +/// value of the `left` parser. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{left, Parser}; +/// let parser = left("foo", "bar"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foobar")); +/// ``` +pub fn left<'a, L, R, T>(left: L, right: R) -> impl Parser<'a, Output = T> +where + L: Parser<'a, Output = T>, + R: Parser<'a>, +{ + map(seq!(left, right), |(left_value, _)| left_value) +} + +/// A parser combinator which runs the given parsers in sequence and returns the +/// value of `right` if both are matched. +/// +/// This is useful for two-element sequences in which you only want the output +/// value of the `right` parser. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{right, Parser}; +/// let parser = right("foo", "bar"); +/// assert_eq!(Ok(("", "bar")), parser.parse("foobar")); +/// ``` +pub fn right<'a, L, R, T>(left: L, right: R) -> impl Parser<'a, Output = T> +where + L: Parser<'a>, + R: Parser<'a, Output = T>, +{ + map(seq!(left, right), |(_, right_value)| right_value) +} + +/// A parser combinator which matches the given parser against the input zero or +/// more times. +/// +/// This parser always succeeds and returns the empty Vec when it matched zero +/// times. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{zero_or_more, Parser}; +/// let parser = zero_or_more("a"); +/// assert_eq!(Ok(("", vec![])), parser.parse("")); +/// assert_eq!(Ok(("", vec!["a"])), parser.parse("a")); +/// assert_eq!(Ok(("", vec!["a", "a"])), parser.parse("aa")); +/// assert_eq!(Ok(("bb", vec![])), parser.parse("bb")); +/// ``` +pub fn zero_or_more<'a, P, T>(parser: P) -> impl Parser<'a, Output = Vec> +where + P: Parser<'a, Output = T>, +{ + move |mut input| { + let mut values = Vec::new(); + + while let Ok((next_input, value)) = parser.parse(input) { + input = next_input; + values.push(value); + } + + Ok((input, values)) + } +} + +/// A parser combinator which matches the given parser against the input one or +/// more times. +/// +/// This parser combinator acts the same as [zero_or_more] but must match at +/// least once. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{one_or_more, Parser}; +/// let parser = one_or_more("a"); +/// assert_eq!(Err(""), parser.parse("")); +/// assert_eq!(Ok(("", vec!["a"])), parser.parse("a")); +/// assert_eq!(Ok(("", vec!["a", "a"])), parser.parse("aa")); +/// assert_eq!(Err("bb"), parser.parse("bb")); +/// ``` +pub fn one_or_more<'a, P, T>(parser: P) -> impl Parser<'a, Output = Vec> +where + P: Parser<'a, Output = T>, +{ + move |mut input| { + let mut values = Vec::new(); + + match parser.parse(input) { + Ok((next_input, value)) => { + input = next_input; + values.push(value); + } + Err(err) => return Err(err), + } + + while let Ok((next_input, value)) = parser.parse(input) { + input = next_input; + values.push(value); + } + + Ok((input, values)) + } +} + +/// A parser combinator which matches one or more instances of the given parser +/// interspersed with the separator parser. +/// +/// Output values of the separator parser are discarded. +/// +/// This is typically used to parse function arguments or list items. +/// +/// # Examples +/// +/// ```rust +/// use helix_parsec::{sep, Parser}; +/// let parser = sep("a", ","); +/// assert_eq!(Ok(("", vec!["a", "a", "a"])), parser.parse("a,a,a")); +/// ``` +pub fn sep<'a, P, S, T>(parser: P, separator: S) -> impl Parser<'a, Output = Vec> +where + P: Parser<'a, Output = T>, + S: Parser<'a>, +{ + move |mut input| { + let mut values = Vec::new(); + + match parser.parse(input) { + Ok((next_input, value)) => { + input = next_input; + values.push(value); + } + Err(err) => return Err(err), + } + + loop { + match separator.parse(input) { + Ok((next_input, _)) => input = next_input, + Err(_) => break, + } + + match parser.parse(input) { + Ok((next_input, value)) => { + input = next_input; + values.push(value); + } + Err(_) => break, + } + } + + Ok((input, values)) + } +} From b2fb5241e60f991e7e764863084dc53e2b2262f3 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Mon, 3 Oct 2022 20:41:31 -0500 Subject: [PATCH 02/12] Add parser for LSP snippet --- Cargo.lock | 1 + helix-lsp/Cargo.toml | 2 + helix-lsp/src/lib.rs | 1 + helix-lsp/src/snippet.rs | 367 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 371 insertions(+) create mode 100644 helix-lsp/src/snippet.rs diff --git a/Cargo.lock b/Cargo.lock index 4f6e6115a76e..e50786e8e71d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,6 +1184,7 @@ dependencies = [ "helix-loader", "log", "lsp-types", + "once_cell", "serde", "serde_json", "thiserror", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 06811902479e..4a2e2652b04d 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -14,6 +14,7 @@ homepage = "https://helix-editor.com" [dependencies] helix-core = { version = "0.6", path = "../helix-core" } helix-loader = { version = "0.6", path = "../helix-loader" } +helix-parsec = { version = "0.6", path = "../helix-parsec" } anyhow = "1.0" futures-executor = "0.3" @@ -26,3 +27,4 @@ thiserror = "1.0" tokio = { version = "1.25", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.11" which = "4.4" +once_cell = "1.15" diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 341d4a547b35..52573b7be263 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -1,5 +1,6 @@ mod client; pub mod jsonrpc; +pub mod snippet; mod transport; pub use client::Client; diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs new file mode 100644 index 000000000000..529c3b97587c --- /dev/null +++ b/helix-lsp/src/snippet.rs @@ -0,0 +1,367 @@ +use anyhow::{anyhow, Result}; + +use crate::{util::lsp_pos_to_pos, OffsetEncoding}; + +#[derive(Debug, PartialEq, Eq)] +pub enum CaseChange { + Upcase, + Downcase, + Capitalize, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum FormatItem<'a> { + Text(&'a str), + Capture(usize), + CaseChange(usize, CaseChange), + Conditional(usize, Option<&'a str>, Option<&'a str>), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Regex<'a> { + value: &'a str, + replacement: Vec>, + options: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SnippetElement<'a> { + Tabstop { + tabstop: usize, + }, + Placeholder { + tabstop: usize, + value: Box>, + }, + Choice { + tabstop: usize, + choices: Vec<&'a str>, + }, + Variable { + name: &'a str, + default: Option<&'a str>, + regex: Option>, + }, + Text(&'a str), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Snippet<'a> { + elements: Vec>, +} + +pub fn parse<'a>(s: &'a str) -> Result> { + parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) +} + +mod parser { + use helix_core::regex; + use once_cell::sync::Lazy; + + use helix_parsec::*; + + use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement}; + + /* + https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax + + any ::= tabstop | placeholder | choice | variable | text + tabstop ::= '$' int | '${' int '}' + placeholder ::= '${' int ':' any '}' + choice ::= '${' int '|' text (',' text)* '|}' + variable ::= '$' var | '${' var }' + | '${' var ':' any '}' + | '${' var '/' regex '/' (format | text)+ '/' options '}' + format ::= '$' int | '${' int '}' + | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' + | '${' int ':+' if '}' + | '${' int ':?' if ':' else '}' + | '${' int ':-' else '}' | '${' int ':' else '}' + regex ::= Regular Expression value (ctor-string) + options ::= Regular Expression option (ctor-options) + var ::= [_a-zA-Z] [_a-zA-Z0-9]* + int ::= [0-9]+ + text ::= .* + if ::= text + else ::= text + */ + + static DIGIT: Lazy = Lazy::new(|| regex::Regex::new(r"^[0-9]+").unwrap()); + static VARIABLE: Lazy = + Lazy::new(|| regex::Regex::new(r"^[_a-zA-Z][_a-zA-Z0-9]*").unwrap()); + static TEXT: Lazy = Lazy::new(|| regex::Regex::new(r"^[^\$]+").unwrap()); + + fn var<'a>() -> impl Parser<'a, Output = &'a str> { + pattern(&VARIABLE) + } + + fn digit<'a>() -> impl Parser<'a, Output = usize> { + filter_map(pattern(&DIGIT), |s| s.parse().ok()) + } + + fn case_change<'a>() -> impl Parser<'a, Output = CaseChange> { + use CaseChange::*; + + choice!( + map("upcase", |_| Upcase), + map("downcase", |_| Downcase), + map("capitalize", |_| Capitalize), + ) + } + + fn format<'a>() -> impl Parser<'a, Output = FormatItem<'a>> { + use FormatItem::*; + + choice!( + // '$' int + map(right("$", digit()), Capture), + // '${' int '}' + map(seq!("${", digit(), "}"), |seq| Capture(seq.1)), + // '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' + map(seq!("${", digit(), ":/", case_change(), "}"), |seq| { + CaseChange(seq.1, seq.3) + }), + // '${' int ':+' if '}' + map( + seq!("${", digit(), ":+", take_until(|c| c == '}'), "}"), + |seq| { Conditional(seq.1, Some(seq.3), None) } + ), + // '${' int ':?' if ':' else '}' + map( + seq!( + "${", + digit(), + ":?", + take_until(|c| c == ':'), + ":", + take_until(|c| c == '}'), + "}" + ), + |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) } + ), + // '${' int ':-' else '}' | '${' int ':' else '}' + map( + seq!( + "${", + digit(), + ":", + optional("-"), + take_until(|c| c == '}'), + "}" + ), + |seq| { Conditional(seq.1, None, Some(seq.4)) } + ), + // Any text + map(pattern(&TEXT), Text), + ) + } + + fn regex<'a>() -> impl Parser<'a, Output = Regex<'a>> { + let replacement = reparse_as(take_until(|c| c == '/'), one_or_more(format())); + + map( + seq!( + "/", + take_until(|c| c == '/'), + "/", + replacement, + "/", + optional(take_until(|c| c == '}')), + ), + |(_, value, _, replacement, _, options)| Regex { + value, + replacement, + options, + }, + ) + } + + fn tabstop<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + map( + or( + right("$", digit()), + map(seq!("${", digit(), "}"), |values| values.1), + ), + |digit| SnippetElement::Tabstop { tabstop: digit }, + ) + } + + fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + // TODO: why doesn't parse_as work? + // let value = reparse_as(take_until(|c| c == '}'), anything()); + let value = filter_map(take_until(|c| c == '}'), |s| { + anything().parse(s).map(|parse_result| parse_result.1).ok() + }); + + map(seq!("${", digit(), ":", value, "}"), |seq| { + SnippetElement::Placeholder { + tabstop: seq.1, + value: Box::new(seq.3), + } + }) + } + + fn choice<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + map( + seq!( + "${", + digit(), + "|", + sep(take_until(|c| c == ',' || c == '|'), ","), + "|}", + ), + |seq| SnippetElement::Choice { + tabstop: seq.1, + choices: seq.3, + }, + ) + } + + fn variable<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + choice!( + // $var + map(right("$", var()), |name| SnippetElement::Variable { + name, + default: None, + regex: None, + }), + // ${var:default} + map( + seq!("${", var(), ":", take_until(|c| c == '}'), "}",), + |values| SnippetElement::Variable { + name: values.1, + default: Some(values.3), + regex: None, + } + ), + // ${var/value/format/options} + map(seq!("${", var(), regex(), "}"), |values| { + SnippetElement::Variable { + name: values.1, + default: None, + regex: Some(values.2), + } + }), + ) + } + + fn text<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + map(pattern(&TEXT), SnippetElement::Text) + } + + fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + choice!(tabstop(), placeholder(), choice(), variable(), text()) + } + + fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> { + map(one_or_more(anything()), |parts| Snippet { elements: parts }) + } + + pub fn parse(s: &str) -> Result { + snippet().parse(s).map(|(_input, elements)| elements) + } + + #[cfg(test)] + mod test { + use super::SnippetElement::*; + use super::*; + + #[test] + fn empty_string_is_error() { + assert_eq!(Err(""), parse("")); + } + + #[test] + fn parse_placeholders_in_function_call() { + assert_eq!( + Ok(Snippet { + elements: vec![ + Text("match("), + Placeholder { + tabstop: 1, + value: Box::new(Text("Arg1")), + }, + Text(")") + ] + }), + parse("match(${1:Arg1})") + ) + } + + #[test] + fn parse_placeholders_in_statement() { + assert_eq!( + Ok(Snippet { + elements: vec![ + Text("local "), + Placeholder { + tabstop: 1, + value: Box::new(Text("var")), + }, + Text(" = "), + Placeholder { + tabstop: 1, + value: Box::new(Text("value")), + }, + ] + }), + parse("local ${1:var} = ${1:value}") + ) + } + + #[test] + fn parse_all() { + assert_eq!( + Ok(Snippet { + elements: vec![ + Text("hello "), + Tabstop { tabstop: 1 }, + Tabstop { tabstop: 2 }, + Text(" "), + Choice { + tabstop: 1, + choices: vec!["one", "two", "three"] + }, + Text(" "), + Variable { + name: "name", + default: Some("foo"), + regex: None + }, + Text(" "), + Variable { + name: "var", + default: None, + regex: None + }, + Text(" "), + Variable { + name: "TM", + default: None, + regex: None + }, + ] + }), + parse("hello $1${2} ${1|one,two,three|} ${name:foo} $var $TM") + ); + } + + #[test] + fn regex_capture_replace() { + assert_eq!( + Ok(Snippet { + elements: vec![Variable { + name: "TM_FILENAME", + default: None, + regex: Some(Regex { + value: "(.*).+$", + replacement: vec![FormatItem::Capture(1)], + options: None, + }), + }] + }), + parse("${TM_FILENAME/(.*).+$/$1/}") + ); + } + } +} From 3c0a5b4f807b2adfb7dbcfe0b0021cf95ce72b73 Mon Sep 17 00:00:00 2001 From: Urgau Date: Tue, 7 Feb 2023 20:15:39 +0100 Subject: [PATCH 03/12] Optimize LSP snippet parsing --- Cargo.lock | 1 - helix-lsp/Cargo.toml | 1 - helix-lsp/src/snippet.rs | 45 +++++++++++++++++++++++++--------------- helix-parsec/src/lib.rs | 29 ++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e50786e8e71d..4f6e6115a76e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1184,7 +1184,6 @@ dependencies = [ "helix-loader", "log", "lsp-types", - "once_cell", "serde", "serde_json", "thiserror", diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 4a2e2652b04d..875a13165053 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -27,4 +27,3 @@ thiserror = "1.0" tokio = { version = "1.25", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.11" which = "4.4" -once_cell = "1.15" diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index 529c3b97587c..27b103d5d630 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -50,14 +50,11 @@ pub struct Snippet<'a> { elements: Vec>, } -pub fn parse<'a>(s: &'a str) -> Result> { +pub fn parse(s: &str) -> Result> { parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) } mod parser { - use helix_core::regex; - use once_cell::sync::Lazy; - use helix_parsec::*; use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement}; @@ -86,17 +83,34 @@ mod parser { else ::= text */ - static DIGIT: Lazy = Lazy::new(|| regex::Regex::new(r"^[0-9]+").unwrap()); - static VARIABLE: Lazy = - Lazy::new(|| regex::Regex::new(r"^[_a-zA-Z][_a-zA-Z0-9]*").unwrap()); - static TEXT: Lazy = Lazy::new(|| regex::Regex::new(r"^[^\$]+").unwrap()); - fn var<'a>() -> impl Parser<'a, Output = &'a str> { - pattern(&VARIABLE) + // var = [_a-zA-Z][_a-zA-Z0-9]* + move |input: &'a str| match input + .char_indices() + .take_while(|(p, c)| { + *c == '_' + || if *p == 0 { + c.is_ascii_alphabetic() + } else { + c.is_ascii_alphanumeric() + } + }) + .last() + { + Some((index, c)) if index >= 1 => { + let index = index + c.len_utf8(); + Ok((&input[index..], &input[0..index])) + } + _ => Err(input), + } + } + + fn text<'a>() -> impl Parser<'a, Output = &'a str> { + take_while(|c| c != '$') } fn digit<'a>() -> impl Parser<'a, Output = usize> { - filter_map(pattern(&DIGIT), |s| s.parse().ok()) + filter_map(take_while(|c| c.is_ascii_digit()), |s| s.parse().ok()) } fn case_change<'a>() -> impl Parser<'a, Output = CaseChange> { @@ -152,7 +166,7 @@ mod parser { |seq| { Conditional(seq.1, None, Some(seq.4)) } ), // Any text - map(pattern(&TEXT), Text), + map(text(), Text), ) } @@ -245,12 +259,9 @@ mod parser { ) } - fn text<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - map(pattern(&TEXT), SnippetElement::Text) - } - fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - choice!(tabstop(), placeholder(), choice(), variable(), text()) + let text = map(text(), SnippetElement::Text); + choice!(tabstop(), placeholder(), choice(), variable(), text) } fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> { diff --git a/helix-parsec/src/lib.rs b/helix-parsec/src/lib.rs index c86a1a056a6d..bfa981e58d30 100644 --- a/helix-parsec/src/lib.rs +++ b/helix-parsec/src/lib.rs @@ -157,6 +157,35 @@ where } } +/// A parser which matches all values until the specified pattern no longer match. +/// +/// This parser only ever fails if the input has a length of zero. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{take_while, Parser}; +/// let parser = take_while(|c| c == '1'); +/// assert_eq!(Ok(("2", "11")), parser.parse("112")); +/// assert_eq!(Err("22"), parser.parse("22")); +/// ``` +pub fn take_while<'a, F>(pattern: F) -> impl Parser<'a, Output = &'a str> +where + F: Fn(char) -> bool, +{ + move |input: &'a str| match input + .char_indices() + .take_while(|(_p, c)| pattern(*c)) + .last() + { + Some((index, c)) => { + let index = index + c.len_utf8(); + Ok((&input[index..], &input[0..index])) + } + _ => Err(input), + } +} + // Variadic parser combinators /// A parser combinator which matches a sequence of parsers in an all-or-nothing fashion. From 3c786489241f9cbebd138471daa8d57b04611726 Mon Sep 17 00:00:00 2001 From: Urgau Date: Thu, 16 Feb 2023 11:21:53 +0100 Subject: [PATCH 04/12] Remove now unused the pattern combinator --- Cargo.lock | 3 --- helix-parsec/Cargo.toml | 1 - helix-parsec/src/lib.rs | 28 ---------------------------- 3 files changed, 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f6e6115a76e..bf9605d14620 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,9 +1195,6 @@ dependencies = [ [[package]] name = "helix-parsec" version = "0.6.0" -dependencies = [ - "regex", -] [[package]] name = "helix-term" diff --git a/helix-parsec/Cargo.toml b/helix-parsec/Cargo.toml index 562df8ddd147..505a4247e3ef 100644 --- a/helix-parsec/Cargo.toml +++ b/helix-parsec/Cargo.toml @@ -11,4 +11,3 @@ homepage = "https://helix-editor.com" include = ["src/**/*", "README.md"] [dependencies] -regex = "1" diff --git a/helix-parsec/src/lib.rs b/helix-parsec/src/lib.rs index bfa981e58d30..e09814b81f5b 100644 --- a/helix-parsec/src/lib.rs +++ b/helix-parsec/src/lib.rs @@ -3,8 +3,6 @@ //! This module provides parsers and parser combinators which can be used //! together to build parsers by functional composition. -use regex::Regex; - // This module implements parser combinators following https://bodil.lol/parser-combinators/. // `sym` (trait implementation for `&'static str`), `map`, `pred` (filter), `one_or_more`, // `zero_or_more`, as well as the `Parser` trait originate mostly from that post. @@ -104,32 +102,6 @@ pub fn token<'a>(literal: &'static str) -> impl Parser<'a, Output = &'a str> { literal } -/// A parser which matches the pattern described by the given regular expression. -/// -/// The pattern must match from the beginning of the input as if the regular expression -/// included the `^` anchor. Using a `^` anchor in the regular expression is -/// recommended in order to reduce any work done by the regex on non-matching input. -/// -/// # Examples -/// -/// ``` -/// use helix_parsec::{pattern, Parser}; -/// use regex::Regex; -/// let regex = Regex::new(r"Hello, \w+!").unwrap(); -/// let parser = pattern(®ex); -/// assert_eq!(Ok(("", "Hello, world!")), parser.parse("Hello, world!")); -/// assert_eq!(Err("Hey, you!"), parser.parse("Hey, you!")); -/// assert_eq!(Err("Oh Hello, world!"), parser.parse("Oh Hello, world!")); -/// ``` -pub fn pattern<'a>(regex: &'a Regex) -> impl Parser<'a, Output = &'a str> { - move |input: &'a str| match regex.find(input) { - Some(match_) if match_.start() == 0 => { - Ok((&input[match_.end()..], &input[0..match_.end()])) - } - _ => Err(input), - } -} - /// A parser which matches all values until the specified pattern is found. /// /// If the pattern is not found, this parser does not match. The input up to the From 6dcc92b97fd660bea82b39c8afeee84fa9d36622 Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sat, 22 Oct 2022 09:52:25 -0500 Subject: [PATCH 05/12] Apply snippets as transactions --- Cargo.lock | 1 + helix-lsp/src/snippet.rs | 108 ++++++++++++++++++++++++++++++++ helix-term/src/ui/completion.rs | 46 +++++++++++--- 3 files changed, 145 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf9605d14620..183395cb2d2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1182,6 +1182,7 @@ dependencies = [ "futures-util", "helix-core", "helix-loader", + "helix-parsec", "log", "lsp-types", "serde", diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index 27b103d5d630..f74237496ecd 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use anyhow::{anyhow, Result}; use crate::{util::lsp_pos_to_pos, OffsetEncoding}; @@ -54,6 +56,112 @@ pub fn parse(s: &str) -> Result> { parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) } +pub fn into_transaction<'a>( + snippet: Snippet<'a>, + doc: &helix_core::Rope, + selection: &helix_core::Selection, + edit: &lsp_types::TextEdit, + line_ending: &str, + offset_encoding: OffsetEncoding, +) -> helix_core::Transaction { + use helix_core::{smallvec, Range, Selection, Transaction}; + use SnippetElement::*; + + let text = doc.slice(..); + let primary_cursor = selection.primary().cursor(text); + + let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) { + Some(start) => start as i128 - primary_cursor as i128, + None => return Transaction::new(doc), + }; + let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) { + Some(end) => end as i128 - primary_cursor as i128, + None => return Transaction::new(doc), + }; + + let newline_with_offset = format!( + "{line_ending}{blank:width$}", + width = edit.range.start.character as usize, + blank = "" + ); + + let mut insert = String::new(); + let mut offset = (primary_cursor as i128 + start_offset) as usize; + let mut tabstops: Vec = Vec::new(); + + for element in snippet.elements { + match element { + Text(text) => { + // small optimization to avoid calling replace when it's unnecessary + let text = if text.contains('\n') { + Cow::Owned(text.replace('\n', &newline_with_offset)) + } else { + Cow::Borrowed(text) + }; + offset += text.chars().count(); + insert.push_str(&text); + } + Variable { + name: _name, + regex: None, + r#default, + } => { + // TODO: variables. For now, fall back to the default, which defaults to "". + let text = r#default.unwrap_or_default(); + offset += text.chars().count(); + insert.push_str(text); + } + Tabstop { .. } => { + // TODO: tabstop indexing: 0 is final cursor position. 1,2,.. are positions. + // TODO: merge tabstops with the same index + tabstops.push(Range::point(offset)); + } + Placeholder { + tabstop: _tabstop, + value, + } => match value.as_ref() { + // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html + // would make this a bit nicer + Text(text) => { + let len_chars = text.chars().count(); + tabstops.push(Range::new(offset, offset + len_chars + 1)); + offset += len_chars; + insert.push_str(text); + } + other => { + log::error!( + "Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.", + other + ); + return Transaction::new(doc); + } + }, + other => { + log::error!( + "Discarding snippet: generating a transaction for {:?} is unimplemented.", + other + ); + return Transaction::new(doc); + } + } + } + + let transaction = Transaction::change_by_selection(doc, selection, |range| { + let cursor = range.cursor(text); + ( + (cursor as i128 + start_offset) as usize, + (cursor as i128 + end_offset) as usize, + Some(insert.clone().into()), + ) + }); + + if let Some(first) = tabstops.first() { + transaction.with_selection(Selection::new(smallvec![*first], 0)) + } else { + transaction + } +} + mod parser { use helix_parsec::*; diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index a24da20a9fac..6897305de762 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -119,7 +119,9 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Transaction { - let transaction = if let Some(edit) = &item.text_edit { + use helix_lsp::snippet; + + if let Some(edit) = &item.text_edit { let edit = match edit { lsp::CompletionTextEdit::Edit(edit) => edit.clone(), lsp::CompletionTextEdit::InsertAndReplace(item) => { @@ -128,12 +130,38 @@ impl Completion { } }; - util::generate_transaction_from_completion_edit( - doc.text(), - doc.selection(view_id), - edit, - offset_encoding, // TODO: should probably transcode in Client - ) + if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) + || matches!( + item.insert_text_format, + Some(lsp::InsertTextFormat::SNIPPET) + ) + { + match snippet::parse(&edit.new_text) { + Ok(snippet) => snippet::into_transaction( + snippet, + doc.text(), + doc.selection(view_id), + &edit, + doc.line_ending.as_str(), + offset_encoding, + ), + Err(err) => { + log::error!( + "Failed to parse snippet: {:?}, remaining output: {}", + &edit.new_text, + err + ); + Transaction::new(doc.text()) + } + } + } else { + util::generate_transaction_from_completion_edit( + doc.text(), + doc.selection(view_id), + edit, + offset_encoding, // TODO: should probably transcode in Client + ) + } } else { let text = item.insert_text.as_ref().unwrap_or(&item.label); // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯ @@ -157,9 +185,7 @@ impl Completion { (cursor, cursor, Some(text.into())) }) - }; - - transaction + } } fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec { From 9f2b586d54cc17a5781f61dfff9f54f5a39085fc Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Sun, 30 Oct 2022 16:39:27 -0500 Subject: [PATCH 06/12] LSP: Advertise snippet support --- helix-lsp/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 3f88b3523f80..95f3ea348d5a 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -320,7 +320,7 @@ impl Client { text_document: Some(lsp::TextDocumentClientCapabilities { completion: Some(lsp::CompletionClientCapabilities { completion_item: Some(lsp::CompletionItemCapability { - snippet_support: Some(false), + snippet_support: Some(true), resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport { properties: vec![ String::from("documentation"), From c78cc9852df6dacd3590cbe0eb2e39bbab6ca9f1 Mon Sep 17 00:00:00 2001 From: Urgau Date: Wed, 8 Feb 2023 09:36:15 +0100 Subject: [PATCH 07/12] Implement LSP snippet tabstops sorting and merging --- helix-lsp/src/snippet.rs | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index f74237496ecd..87c839f94ca2 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use anyhow::{anyhow, Result}; +use helix_core::SmallVec; use crate::{util::lsp_pos_to_pos, OffsetEncoding}; @@ -87,7 +88,7 @@ pub fn into_transaction<'a>( let mut insert = String::new(); let mut offset = (primary_cursor as i128 + start_offset) as usize; - let mut tabstops: Vec = Vec::new(); + let mut tabstops: Vec<(usize, Range)> = Vec::new(); for element in snippet.elements { match element { @@ -111,20 +112,15 @@ pub fn into_transaction<'a>( offset += text.chars().count(); insert.push_str(text); } - Tabstop { .. } => { - // TODO: tabstop indexing: 0 is final cursor position. 1,2,.. are positions. - // TODO: merge tabstops with the same index - tabstops.push(Range::point(offset)); + Tabstop { tabstop } => { + tabstops.push((tabstop, Range::point(offset))); } - Placeholder { - tabstop: _tabstop, - value, - } => match value.as_ref() { + Placeholder { tabstop, value } => match value.as_ref() { // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html // would make this a bit nicer Text(text) => { let len_chars = text.chars().count(); - tabstops.push(Range::new(offset, offset + len_chars + 1)); + tabstops.push((tabstop, Range::new(offset, offset + len_chars + 1))); offset += len_chars; insert.push_str(text); } @@ -155,8 +151,25 @@ pub fn into_transaction<'a>( ) }); - if let Some(first) = tabstops.first() { - transaction.with_selection(Selection::new(smallvec![*first], 0)) + // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) + tabstops.sort_unstable_by_key(|(n, _range)| if *n == 0 { usize::MAX } else { *n }); + + // merge tabstops with the same index (we take advantage of the fact that we just sorted them + // above to simply look backwards) + let mut ntabstops = Vec::>::new(); + let mut prev = None; + for (tabstop, range) in tabstops { + if prev == Some(tabstop) { + let len_1 = ntabstops.len() - 1; + ntabstops[len_1].push(range); + } else { + prev = Some(tabstop); + ntabstops.push(smallvec![range]); + } + } + + if let Some(first) = ntabstops.first() { + transaction.with_selection(Selection::new(first.clone(), 0)) } else { transaction } From 0af6b192efecb7161d5a210c3ffbec025a2eac9e Mon Sep 17 00:00:00 2001 From: Pascal Kuthe Date: Sat, 11 Feb 2023 14:20:49 +0100 Subject: [PATCH 08/12] Delete snippet placeholders when accepting completion When accepting a snippet completion we automatically delete the placeholders for now as doing so manual is quite cumbersome. In the future we should keep these as a mark + virtual text that is automatically removed once the cursor moves there. --- helix-lsp/src/snippet.rs | 13 +++++++++---- helix-term/src/ui/completion.rs | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index 87c839f94ca2..441c419f872e 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -64,6 +64,7 @@ pub fn into_transaction<'a>( edit: &lsp_types::TextEdit, line_ending: &str, offset_encoding: OffsetEncoding, + include_placeholer: bool, ) -> helix_core::Transaction { use helix_core::{smallvec, Range, Selection, Transaction}; use SnippetElement::*; @@ -119,10 +120,14 @@ pub fn into_transaction<'a>( // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html // would make this a bit nicer Text(text) => { - let len_chars = text.chars().count(); - tabstops.push((tabstop, Range::new(offset, offset + len_chars + 1))); - offset += len_chars; - insert.push_str(text); + if include_placeholer { + let len_chars = text.chars().count(); + tabstops.push((tabstop, Range::new(offset, offset + len_chars + 1))); + offset += len_chars; + insert.push_str(text); + } else { + tabstops.push((tabstop, Range::point(offset))); + } } other => { log::error!( diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6897305de762..c7955a3dd17c 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -118,6 +118,7 @@ impl Completion { offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, + include_placeholder: bool, ) -> Transaction { use helix_lsp::snippet; @@ -144,6 +145,7 @@ impl Completion { &edit, doc.line_ending.as_str(), offset_encoding, + include_placeholder, ), Err(err) => { log::error!( @@ -216,6 +218,7 @@ impl Completion { offset_encoding, start_offset, trigger_offset, + true, ); // initialize a savepoint @@ -238,6 +241,7 @@ impl Completion { offset_encoding, start_offset, trigger_offset, + false, ); doc.apply(&transaction, view.id); From 3d0812627c89afd0ceef3f419b80da3ac665593f Mon Sep 17 00:00:00 2001 From: Urgau Date: Sat, 11 Feb 2023 18:45:30 +0100 Subject: [PATCH 09/12] Correctly handle multiple cursors with LSP snippets --- helix-core/src/selection.rs | 10 ++++++++ helix-lsp/src/snippet.rs | 48 ++++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 7817618fb488..0b517c44284a 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -578,6 +578,16 @@ impl Selection { self.normalize() } + /// Takes a closure and maps each `Range` over the closure to multiple `Range`s. + pub fn transform_iter(mut self, f: F) -> Self + where + F: FnMut(Range) -> I, + I: Iterator, + { + self.ranges = self.ranges.into_iter().flat_map(f).collect(); + self.normalize() + } + // Ensures the selection adheres to the following invariants: // 1. All ranges are grapheme aligned. // 2. All ranges are at least 1 character wide, unless at the diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index 441c419f872e..ab0f406d0458 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -66,7 +66,7 @@ pub fn into_transaction<'a>( offset_encoding: OffsetEncoding, include_placeholer: bool, ) -> helix_core::Transaction { - use helix_core::{smallvec, Range, Selection, Transaction}; + use helix_core::{smallvec, Range, Transaction}; use SnippetElement::*; let text = doc.slice(..); @@ -87,9 +87,9 @@ pub fn into_transaction<'a>( blank = "" ); + let mut offset = 0; let mut insert = String::new(); - let mut offset = (primary_cursor as i128 + start_offset) as usize; - let mut tabstops: Vec<(usize, Range)> = Vec::new(); + let mut tabstops: Vec<(usize, usize, usize)> = Vec::new(); for element in snippet.elements { match element { @@ -114,7 +114,7 @@ pub fn into_transaction<'a>( insert.push_str(text); } Tabstop { tabstop } => { - tabstops.push((tabstop, Range::point(offset))); + tabstops.push((tabstop, offset, offset)); } Placeholder { tabstop, value } => match value.as_ref() { // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html @@ -122,11 +122,11 @@ pub fn into_transaction<'a>( Text(text) => { if include_placeholer { let len_chars = text.chars().count(); - tabstops.push((tabstop, Range::new(offset, offset + len_chars + 1))); + tabstops.push((tabstop, offset, offset + len_chars + 1)); offset += len_chars; insert.push_str(text); } else { - tabstops.push((tabstop, Range::point(offset))); + tabstops.push((tabstop, offset, offset)); } } other => { @@ -157,24 +157,38 @@ pub fn into_transaction<'a>( }); // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) - tabstops.sort_unstable_by_key(|(n, _range)| if *n == 0 { usize::MAX } else { *n }); + tabstops.sort_unstable_by_key(|(n, _o1, _o2)| if *n == 0 { usize::MAX } else { *n }); // merge tabstops with the same index (we take advantage of the fact that we just sorted them // above to simply look backwards) - let mut ntabstops = Vec::>::new(); - let mut prev = None; - for (tabstop, range) in tabstops { - if prev == Some(tabstop) { - let len_1 = ntabstops.len() - 1; - ntabstops[len_1].push(range); - } else { - prev = Some(tabstop); - ntabstops.push(smallvec![range]); + let mut ntabstops = Vec::>::new(); + { + let mut prev = None; + for (tabstop, o1, o2) in tabstops { + if prev == Some(tabstop) { + let len_1 = ntabstops.len() - 1; + ntabstops[len_1].push((o1, o2)); + } else { + prev = Some(tabstop); + ntabstops.push(smallvec![(o1, o2)]); + } } } if let Some(first) = ntabstops.first() { - transaction.with_selection(Selection::new(first.clone(), 0)) + let cursor_offset = insert.chars().count() as i128 - (end_offset - start_offset); + let mut extra_offset = start_offset; + transaction.with_selection(selection.clone().transform_iter(|range| { + let cursor = range.cursor(text); + let iter = first.iter().map(move |first| { + Range::new( + (cursor as i128 + first.0 as i128 + extra_offset) as usize, + (cursor as i128 + first.1 as i128 + extra_offset) as usize, + ) + }); + extra_offset += cursor_offset; + iter + })) } else { transaction } From 0399c9f1e31c91ba8fd8a0c98cd8bbd32c772c7f Mon Sep 17 00:00:00 2001 From: Andrii Grynenko Date: Fri, 17 Feb 2023 07:51:00 -0800 Subject: [PATCH 10/12] Render every LSP snippets for every cursor This refactors the snippet logic to be largely unaware of the rest of the document. The completion application logic is moved into generate_transaction_from_snippet which is extended to support dynamically computing replacement text. --- helix-lsp/src/lib.rs | 79 ++++++++++++++ helix-lsp/src/snippet.rs | 187 ++++++++++++++------------------ helix-term/src/ui/completion.rs | 8 +- 3 files changed, 166 insertions(+), 108 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 52573b7be263..3dbddbbe3b87 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -60,6 +60,7 @@ pub mod util { use super::*; use helix_core::line_ending::{line_end_byte_index, line_end_char_index}; use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction}; + use helix_core::{smallvec, SmallVec}; /// Converts a diagnostic in the document to [`lsp::Diagnostic`]. /// @@ -282,6 +283,84 @@ pub mod util { }) } + /// Creates a [Transaction] from the [snippet::Snippet] in a completion response. + /// The transaction applies the edit to all cursors. + pub fn generate_transaction_from_snippet( + doc: &Rope, + selection: &Selection, + edit_range: &lsp::Range, + snippet: snippet::Snippet, + line_ending: &str, + include_placeholder: bool, + offset_encoding: OffsetEncoding, + ) -> Transaction { + let text = doc.slice(..); + let primary_cursor = selection.primary().cursor(text); + + let start_offset = match lsp_pos_to_pos(doc, edit_range.start, offset_encoding) { + Some(start) => start as i128 - primary_cursor as i128, + None => return Transaction::new(doc), + }; + let end_offset = match lsp_pos_to_pos(doc, edit_range.end, offset_encoding) { + Some(end) => end as i128 - primary_cursor as i128, + None => return Transaction::new(doc), + }; + + // For each cursor store offsets for the first tabstop + let mut cursor_tabstop_offsets = Vec::>::new(); + let transaction = Transaction::change_by_selection(doc, selection, |range| { + let cursor = range.cursor(text); + let replacement_start = (cursor as i128 + start_offset) as usize; + let replacement_end = (cursor as i128 + end_offset) as usize; + let newline_with_offset = format!( + "{line_ending}{blank:width$}", + line_ending = line_ending, + width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)), + blank = "" + ); + + let (replacement, tabstops) = + snippet::render(&snippet, newline_with_offset, include_placeholder); + + let replacement_len = replacement.chars().count(); + cursor_tabstop_offsets.push( + tabstops + .first() + .unwrap_or(&smallvec![(replacement_len, replacement_len)]) + .iter() + .map(|(from, to)| -> (i128, i128) { + ( + *from as i128 - replacement_len as i128, + *to as i128 - replacement_len as i128, + ) + }) + .collect(), + ); + + (replacement_start, replacement_end, Some(replacement.into())) + }); + + // Create new selection based on the cursor tabstop from above + let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter(); + let selection = selection + .clone() + .map(transaction.changes()) + .transform_iter(|range| { + cursor_tabstop_offsets_iter + .next() + .unwrap() + .iter() + .map(move |(from, to)| { + Range::new( + (range.anchor as i128 + *from) as usize, + (range.anchor as i128 + *to) as usize, + ) + }) + }); + + transaction.with_selection(selection) + } + pub fn generate_transaction_from_edits( doc: &Rope, mut edits: Vec, diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index ab0f406d0458..63054cdbb1fa 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -1,9 +1,7 @@ use std::borrow::Cow; use anyhow::{anyhow, Result}; -use helix_core::SmallVec; - -use crate::{util::lsp_pos_to_pos, OffsetEncoding}; +use helix_core::{SmallVec, smallvec}; #[derive(Debug, PartialEq, Eq)] pub enum CaseChange { @@ -34,7 +32,7 @@ pub enum SnippetElement<'a> { }, Placeholder { tabstop: usize, - value: Box>, + value: Vec>, }, Choice { tabstop: usize, @@ -57,141 +55,108 @@ pub fn parse(s: &str) -> Result> { parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) } -pub fn into_transaction<'a>( - snippet: Snippet<'a>, - doc: &helix_core::Rope, - selection: &helix_core::Selection, - edit: &lsp_types::TextEdit, - line_ending: &str, - offset_encoding: OffsetEncoding, +fn render_elements( + snippet_elements: &[SnippetElement<'_>], + insert: &mut String, + offset: &mut usize, + tabstops: &mut Vec<(usize, (usize, usize))>, + newline_with_offset: &String, include_placeholer: bool, -) -> helix_core::Transaction { - use helix_core::{smallvec, Range, Transaction}; +) { use SnippetElement::*; - let text = doc.slice(..); - let primary_cursor = selection.primary().cursor(text); - - let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) { - Some(start) => start as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; - let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) { - Some(end) => end as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; - - let newline_with_offset = format!( - "{line_ending}{blank:width$}", - width = edit.range.start.character as usize, - blank = "" - ); - - let mut offset = 0; - let mut insert = String::new(); - let mut tabstops: Vec<(usize, usize, usize)> = Vec::new(); - - for element in snippet.elements { + for element in snippet_elements { match element { - Text(text) => { + &Text(text) => { // small optimization to avoid calling replace when it's unnecessary let text = if text.contains('\n') { - Cow::Owned(text.replace('\n', &newline_with_offset)) + Cow::Owned(text.replace('\n', newline_with_offset)) } else { Cow::Borrowed(text) }; - offset += text.chars().count(); + *offset += text.chars().count(); insert.push_str(&text); } - Variable { - name: _name, - regex: None, + &Variable { + name: _, + regex: _, r#default, } => { // TODO: variables. For now, fall back to the default, which defaults to "". let text = r#default.unwrap_or_default(); - offset += text.chars().count(); + *offset += text.chars().count(); insert.push_str(text); } - Tabstop { tabstop } => { - tabstops.push((tabstop, offset, offset)); + &Tabstop { tabstop } => { + tabstops.push((tabstop, (*offset, *offset))); } - Placeholder { tabstop, value } => match value.as_ref() { - // https://doc.rust-lang.org/beta/unstable-book/language-features/box-patterns.html - // would make this a bit nicer - Text(text) => { - if include_placeholer { - let len_chars = text.chars().count(); - tabstops.push((tabstop, offset, offset + len_chars + 1)); - offset += len_chars; - insert.push_str(text); - } else { - tabstops.push((tabstop, offset, offset)); - } - } - other => { - log::error!( - "Discarding snippet: generating a transaction for placeholder contents {:?} is unimplemented.", - other + Placeholder { + tabstop, + value: inner_snippet_elements, + } => { + let start_offset = *offset; + if include_placeholer { + render_elements( + inner_snippet_elements, + insert, + offset, + tabstops, + newline_with_offset, + include_placeholer, ); - return Transaction::new(doc); } - }, - other => { - log::error!( - "Discarding snippet: generating a transaction for {:?} is unimplemented.", - other - ); - return Transaction::new(doc); + tabstops.push((*tabstop, (start_offset, *offset))); + } + &Choice { + tabstop, + choices: _, + } => { + // TODO: choices + tabstops.push((tabstop, (*offset, *offset))); } } } +} - let transaction = Transaction::change_by_selection(doc, selection, |range| { - let cursor = range.cursor(text); - ( - (cursor as i128 + start_offset) as usize, - (cursor as i128 + end_offset) as usize, - Some(insert.clone().into()), - ) - }); +#[allow(clippy::type_complexity)] // only used one time +pub fn render( + snippet: &Snippet<'_>, + newline_with_offset: String, + include_placeholer: bool, +) -> (String, Vec>) { + let mut insert = String::new(); + let mut tabstops = Vec::new(); + let mut offset = 0; + + render_elements( + &snippet.elements, + &mut insert, + &mut offset, + &mut tabstops, + &newline_with_offset, + include_placeholer, + ); // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) - tabstops.sort_unstable_by_key(|(n, _o1, _o2)| if *n == 0 { usize::MAX } else { *n }); + tabstops.sort_unstable_by_key(|(n, _)| if *n == 0 { usize::MAX } else { *n }); // merge tabstops with the same index (we take advantage of the fact that we just sorted them // above to simply look backwards) let mut ntabstops = Vec::>::new(); { let mut prev = None; - for (tabstop, o1, o2) in tabstops { + for (tabstop, r) in tabstops { if prev == Some(tabstop) { let len_1 = ntabstops.len() - 1; - ntabstops[len_1].push((o1, o2)); + ntabstops[len_1].push(r); } else { prev = Some(tabstop); - ntabstops.push(smallvec![(o1, o2)]); + ntabstops.push(smallvec![r]); } } } - if let Some(first) = ntabstops.first() { - let cursor_offset = insert.chars().count() as i128 - (end_offset - start_offset); - let mut extra_offset = start_offset; - transaction.with_selection(selection.clone().transform_iter(|range| { - let cursor = range.cursor(text); - let iter = first.iter().map(move |first| { - Range::new( - (cursor as i128 + first.0 as i128 + extra_offset) as usize, - (cursor as i128 + first.1 as i128 + extra_offset) as usize, - ) - }); - extra_offset += cursor_offset; - iter - })) - } else { - transaction - } + (insert, ntabstops) } mod parser { @@ -343,14 +308,15 @@ mod parser { fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { // TODO: why doesn't parse_as work? // let value = reparse_as(take_until(|c| c == '}'), anything()); + // TODO: fix this to parse nested placeholders (take until terminates too early) let value = filter_map(take_until(|c| c == '}'), |s| { - anything().parse(s).map(|parse_result| parse_result.1).ok() + snippet().parse(s).map(|parse_result| parse_result.1).ok() }); map(seq!("${", digit(), ":", value, "}"), |seq| { SnippetElement::Placeholder { tabstop: seq.1, - value: Box::new(seq.3), + value: seq.3.elements, } }) } @@ -430,7 +396,7 @@ mod parser { Text("match("), Placeholder { tabstop: 1, - value: Box::new(Text("Arg1")), + value: vec!(Text("Arg1")), }, Text(")") ] @@ -447,12 +413,12 @@ mod parser { Text("local "), Placeholder { tabstop: 1, - value: Box::new(Text("var")), + value: vec!(Text("var")), }, Text(" = "), Placeholder { tabstop: 1, - value: Box::new(Text("value")), + value: vec!(Text("value")), }, ] }), @@ -460,6 +426,19 @@ mod parser { ) } + #[test] + fn parse_tabstop_nested_in_placeholder() { + assert_eq!( + Ok(Snippet { + elements: vec![Placeholder { + tabstop: 1, + value: vec!(Text("var, "), Tabstop { tabstop: 2 },), + },] + }), + parse("${1:var, $2}") + ) + } + #[test] fn parse_all() { assert_eq!( diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index c7955a3dd17c..e7815e12df74 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -138,14 +138,14 @@ impl Completion { ) { match snippet::parse(&edit.new_text) { - Ok(snippet) => snippet::into_transaction( - snippet, + Ok(snippet) => util::generate_transaction_from_snippet( doc.text(), doc.selection(view_id), - &edit, + &edit.range, + snippet, doc.line_ending.as_str(), - offset_encoding, include_placeholder, + offset_encoding, ), Err(err) => { log::error!( From 48f3b0b10df49a74b825ee43faaf9e832ec80a95 Mon Sep 17 00:00:00 2001 From: Andrii Grynenko Date: Mon, 20 Feb 2023 22:04:24 -0800 Subject: [PATCH 11/12] Add nested placeholder parsing for LSP snippets And fix `text` over-parsing, inspired by https://github.com/neovim/neovim/blob/d18f8d5c2d82b209093b2feaa8921a4792b71d59/runtime/lua/vim/lsp/_snippet.lua --- helix-lsp/src/snippet.rs | 70 ++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index 63054cdbb1fa..b27077e7068c 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use anyhow::{anyhow, Result}; -use helix_core::{SmallVec, smallvec}; +use helix_core::{smallvec, SmallVec}; #[derive(Debug, PartialEq, Eq)] pub enum CaseChange { @@ -210,8 +210,8 @@ mod parser { } } - fn text<'a>() -> impl Parser<'a, Output = &'a str> { - take_while(|c| c != '$') + fn text<'a, const SIZE: usize>(cs: [char; SIZE]) -> impl Parser<'a, Output = &'a str> { + take_while(move |c| cs.into_iter().all(|c1| c != c1)) } fn digit<'a>() -> impl Parser<'a, Output = usize> { @@ -270,13 +270,15 @@ mod parser { ), |seq| { Conditional(seq.1, None, Some(seq.4)) } ), - // Any text - map(text(), Text), ) } fn regex<'a>() -> impl Parser<'a, Output = Regex<'a>> { - let replacement = reparse_as(take_until(|c| c == '/'), one_or_more(format())); + let text = map(text(['$', '/']), FormatItem::Text); + let replacement = reparse_as( + take_until(|c| c == '/'), + one_or_more(choice!(format(), text)), + ); map( seq!( @@ -306,19 +308,20 @@ mod parser { } fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - // TODO: why doesn't parse_as work? - // let value = reparse_as(take_until(|c| c == '}'), anything()); - // TODO: fix this to parse nested placeholders (take until terminates too early) - let value = filter_map(take_until(|c| c == '}'), |s| { - snippet().parse(s).map(|parse_result| parse_result.1).ok() - }); - - map(seq!("${", digit(), ":", value, "}"), |seq| { - SnippetElement::Placeholder { + let text = map(text(['$', '}']), SnippetElement::Text); + map( + seq!( + "${", + digit(), + ":", + one_or_more(choice!(anything(), text)), + "}" + ), + |seq| SnippetElement::Placeholder { tabstop: seq.1, - value: seq.3.elements, - } - }) + value: seq.3, + }, + ) } fn choice<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { @@ -366,12 +369,18 @@ mod parser { } fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - let text = map(text(), SnippetElement::Text); - choice!(tabstop(), placeholder(), choice(), variable(), text) + // The parser has to be constructed lazily to avoid infinite opaque type recursion + |input: &'a str| { + let parser = choice!(tabstop(), placeholder(), choice(), variable()); + parser.parse(input) + } } fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> { - map(one_or_more(anything()), |parts| Snippet { elements: parts }) + let text = map(text(['$']), SnippetElement::Text); + map(one_or_more(choice!(anything(), text)), |parts| Snippet { + elements: parts, + }) } pub fn parse(s: &str) -> Result { @@ -439,6 +448,25 @@ mod parser { ) } + #[test] + fn parse_placeholder_nested_in_placeholder() { + assert_eq!( + Ok(Snippet { + elements: vec![Placeholder { + tabstop: 1, + value: vec!( + Text("foo "), + Placeholder { + tabstop: 2, + value: vec!(Text("bar")), + }, + ), + },] + }), + parse("${1:foo ${2:bar}}") + ) + } + #[test] fn parse_all() { assert_eq!( From c2fab5e3147d424fe5935f23d8dfc5eeea2a1518 Mon Sep 17 00:00:00 2001 From: Andrii Grynenko Date: Thu, 2 Mar 2023 21:41:06 -0800 Subject: [PATCH 12/12] Handle snippets for LSPs not providing offsets for completion --- helix-lsp/src/lib.rs | 33 +++--------- helix-term/src/ui/completion.rs | 95 +++++++++++++++++++-------------- 2 files changed, 61 insertions(+), 67 deletions(-) diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 3dbddbbe3b87..e3c1c09d12ee 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -252,26 +252,17 @@ pub mod util { pub fn generate_transaction_from_completion_edit( doc: &Rope, selection: &Selection, - edit: lsp::TextEdit, - offset_encoding: OffsetEncoding, + start_offset: i128, + end_offset: i128, + new_text: String, ) -> Transaction { - let replacement: Option = if edit.new_text.is_empty() { + let replacement: Option = if new_text.is_empty() { None } else { - Some(edit.new_text.into()) + Some(new_text.into()) }; let text = doc.slice(..); - let primary_cursor = selection.primary().cursor(text); - - let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) { - Some(start) => start as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; - let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) { - Some(end) => end as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; Transaction::change_by_selection(doc, selection, |range| { let cursor = range.cursor(text); @@ -288,23 +279,13 @@ pub mod util { pub fn generate_transaction_from_snippet( doc: &Rope, selection: &Selection, - edit_range: &lsp::Range, + start_offset: i128, + end_offset: i128, snippet: snippet::Snippet, line_ending: &str, include_placeholder: bool, - offset_encoding: OffsetEncoding, ) -> Transaction { let text = doc.slice(..); - let primary_cursor = selection.primary().cursor(text); - - let start_offset = match lsp_pos_to_pos(doc, edit_range.start, offset_encoding) { - Some(start) => start as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; - let end_offset = match lsp_pos_to_pos(doc, edit_range.end, offset_encoding) { - Some(end) => end as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; // For each cursor store offsets for the first tabstop let mut cursor_tabstop_offsets = Vec::>::new(); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index e7815e12df74..85931fe321cf 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -121,8 +121,9 @@ impl Completion { include_placeholder: bool, ) -> Transaction { use helix_lsp::snippet; + let selection = doc.selection(view_id); - if let Some(edit) = &item.text_edit { + let (start_offset, end_offset, new_text) = if let Some(edit) = &item.text_edit { let edit = match edit { lsp::CompletionTextEdit::Edit(edit) => edit.clone(), lsp::CompletionTextEdit::InsertAndReplace(item) => { @@ -130,46 +131,27 @@ impl Completion { lsp::TextEdit::new(item.replace, item.new_text.clone()) } }; - - if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) - || matches!( - item.insert_text_format, - Some(lsp::InsertTextFormat::SNIPPET) - ) - { - match snippet::parse(&edit.new_text) { - Ok(snippet) => util::generate_transaction_from_snippet( - doc.text(), - doc.selection(view_id), - &edit.range, - snippet, - doc.line_ending.as_str(), - include_placeholder, - offset_encoding, - ), - Err(err) => { - log::error!( - "Failed to parse snippet: {:?}, remaining output: {}", - &edit.new_text, - err - ); - Transaction::new(doc.text()) - } - } - } else { - util::generate_transaction_from_completion_edit( - doc.text(), - doc.selection(view_id), - edit, - offset_encoding, // TODO: should probably transcode in Client - ) - } + let text = doc.text().slice(..); + let primary_cursor = selection.primary().cursor(text); + + let start_offset = + match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) { + Some(start) => start as i128 - primary_cursor as i128, + None => return Transaction::new(doc.text()), + }; + let end_offset = + match util::lsp_pos_to_pos(doc.text(), edit.range.end, offset_encoding) { + Some(end) => end as i128 - primary_cursor as i128, + None => return Transaction::new(doc.text()), + }; + + (start_offset, end_offset, edit.new_text) } else { - let text = item.insert_text.as_ref().unwrap_or(&item.label); + let new_text = item.insert_text.as_ref().unwrap_or(&item.label); // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯ // in these cases we need to check for a common prefix and remove it let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset)); - let text = text.trim_start_matches::<&str>(&prefix); + let new_text = new_text.trim_start_matches::<&str>(&prefix); // TODO: this needs to be true for the numbers to work out correctly // in the closure below. It's passed in to a callback as this same @@ -182,11 +164,42 @@ impl Completion { == trigger_offset ); - Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| { - let cursor = range.cursor(doc.text().slice(..)); + (0, 0, new_text.into()) + }; - (cursor, cursor, Some(text.into())) - }) + if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) + || matches!( + item.insert_text_format, + Some(lsp::InsertTextFormat::SNIPPET) + ) + { + match snippet::parse(&new_text) { + Ok(snippet) => util::generate_transaction_from_snippet( + doc.text(), + selection, + start_offset, + end_offset, + snippet, + doc.line_ending.as_str(), + include_placeholder, + ), + Err(err) => { + log::error!( + "Failed to parse snippet: {:?}, remaining output: {}", + &new_text, + err + ); + Transaction::new(doc.text()) + } + } + } else { + util::generate_transaction_from_completion_edit( + doc.text(), + selection, + start_offset, + end_offset, + new_text, + ) } }