Add parser for LSP snippet

pull/5465/merge
Michael Davis 2 years ago committed by Blaž Hrastnik
parent c8e6857aff
commit 9c12e0fb76

1
Cargo.lock generated

@ -1141,6 +1141,7 @@ dependencies = [
"helix-loader",
"log",
"lsp-types",
"once_cell",
"serde",
"serde_json",
"thiserror",

@ -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.26", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.12"
which = "4.4"
once_cell = "1.15"

@ -1,5 +1,6 @@
mod client;
pub mod jsonrpc;
pub mod snippet;
mod transport;
pub use client::Client;

@ -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<FormatItem<'a>>,
options: Option<&'a str>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum SnippetElement<'a> {
Tabstop {
tabstop: usize,
},
Placeholder {
tabstop: usize,
value: Box<SnippetElement<'a>>,
},
Choice {
tabstop: usize,
choices: Vec<&'a str>,
},
Variable {
name: &'a str,
default: Option<&'a str>,
regex: Option<Regex<'a>>,
},
Text(&'a str),
}
#[derive(Debug, PartialEq, Eq)]
pub struct Snippet<'a> {
elements: Vec<SnippetElement<'a>>,
}
pub fn parse<'a>(s: &'a str) -> Result<Snippet<'a>> {
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<regex::Regex> = Lazy::new(|| regex::Regex::new(r"^[0-9]+").unwrap());
static VARIABLE: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"^[_a-zA-Z][_a-zA-Z0-9]*").unwrap());
static TEXT: Lazy<regex::Regex> = 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, &str> {
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/}")
);
}
}
}
Loading…
Cancel
Save