From 0d924255e4ea3d5d5c4be9b11a337f4316550e32 Mon Sep 17 00:00:00 2001 From: Andrii Grynenko Date: Mon, 20 Feb 2023 22:04:24 -0800 Subject: [PATCH] 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 63054cdbb..b27077e70 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!(