Correct escaping in snippets (#14912)

## Release Notes:

- Fixed issue with backslashes not appearing in snippets
([#14721](https://github.com/zed-industries/zed/issues/14721)),
motivated by a snippet provided by the latex LSP
([texlab](https://github.com/latex-lsp/texlab)) not working as intended
in Zed ([extension
issue](https://github.com/rzukic/zed-latex/issues/5)).

[Screencast from 2024-07-21
14-57-19.webm](https://github.com/user-attachments/assets/3c95a987-16e5-4132-8c96-15553966d4ac)

## Fix details:

Only $, }, \ can be escaped by a backslash as per [LSP spec (under
grammar
section)](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/\#snippet_syntax).
Technically, commas and pipes can also be escaped only in "choice"
tabstops but it does not look like they are implemented in Zed yet.

## Additional tests added for cases currently not covered:
- backslash not being used to escape anything (so just a normal
backslash)
- backslash escaping a backslash (so that the second does not escape
what follows it)

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Luke Naylor 2024-07-21 23:57:34 +01:00 committed by GitHub
parent 83f6a7f228
commit 0ef19dedd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -47,10 +47,20 @@ fn parse_snippet<'a>(
source = parse_tabstop(&source[1..], text, tabstops)?; source = parse_tabstop(&source[1..], text, tabstops)?;
} }
Some('\\') => { Some('\\') => {
// As specified in the LSP spec (`Grammar` section),
// backslashes can escape some characters:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax
source = &source[1..]; source = &source[1..];
if let Some(c) = source.chars().next() { if let Some(c) = source.chars().next() {
if c == '$' || c == '\\' || c == '}' {
text.push(c); text.push(c);
source = &source[c.len_utf8()..]; // All escapable characters are 1 byte long:
source = &source[1..];
} else {
text.push('\\');
}
} else {
text.push('\\');
} }
} }
Some('}') => { Some('}') => {
@ -197,6 +207,17 @@ mod tests {
let snippet = Snippet::parse("{a\\}").unwrap(); let snippet = Snippet::parse("{a\\}").unwrap();
assert_eq!(snippet.text, "{a}"); assert_eq!(snippet.text, "{a}");
assert_eq!(tabstops(&snippet), &[vec![3..3]]); assert_eq!(tabstops(&snippet), &[vec![3..3]]);
// backslash not functioning as an escape
let snippet = Snippet::parse("a\\b").unwrap();
assert_eq!(snippet.text, "a\\b");
assert_eq!(tabstops(&snippet), &[vec![3..3]]);
// first backslash cancelling escaping that would
// have happened with second backslash
let snippet = Snippet::parse("one\\\\$1two").unwrap();
assert_eq!(snippet.text, "one\\two");
assert_eq!(tabstops(&snippet), &[vec![4..4], vec![7..7]]);
} }
fn tabstops(snippet: &Snippet) -> Vec<Vec<Range<isize>>> { fn tabstops(snippet: &Snippet) -> Vec<Vec<Range<isize>>> {