From 0ef19dedd24753995e0b9887926fe195f9c5fc33 Mon Sep 17 00:00:00 2001 From: Luke Naylor Date: Sun, 21 Jul 2024 23:57:34 +0100 Subject: [PATCH] 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> --- crates/snippet/src/snippet.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs index 7d627f6833..41529939a1 100644 --- a/crates/snippet/src/snippet.rs +++ b/crates/snippet/src/snippet.rs @@ -47,10 +47,20 @@ fn parse_snippet<'a>( source = parse_tabstop(&source[1..], text, tabstops)?; } 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..]; if let Some(c) = source.chars().next() { - text.push(c); - source = &source[c.len_utf8()..]; + if c == '$' || c == '\\' || c == '}' { + text.push(c); + // All escapable characters are 1 byte long: + source = &source[1..]; + } else { + text.push('\\'); + } + } else { + text.push('\\'); } } Some('}') => { @@ -197,6 +207,17 @@ mod tests { let snippet = Snippet::parse("{a\\}").unwrap(); assert_eq!(snippet.text, "{a}"); 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>> {