Snippet choices (#13958)

Closes: #12739

Release Notes:

Solves #12739 by
- Enable snippet parsing to successfully parse snippets with choices
- Show completion menu when tabbing to a snippet variable with multiple
choices

Todo:
 - [x] Parse snippet choices
- [x] Open completion menu when tabbing to a snippet variable with
several choices (Thank you Piotr)
- [x] Get snippet choices to reappear when tabbing back to a previous
tabstop in a snippet
 - [x] add snippet unit tests
- [x] Add fuzzy search to snippet choice completion menu & update
completion menu based on choices
 - [x] add completion menu unit tests

Current State:

Using these custom snippets

```json
  "my snippet": {
      "prefix": "log",
      "body": ["type ${1|i32, u32|} = $2"],
      "description": "Expand `log` to `console.log()`"
  },
  "my snippet2": {
      "prefix": "snip",
      "body": [
        "type ${1|i,i8,i16,i64,i32|} ${2|test,test_again,test_final|} = $3"
      ],
      "description": "snippet choice tester"
    }
```

Using snippet choices:



https://github.com/user-attachments/assets/d29fb1a2-7632-4071-944f-daeaa243e3ac

---------

Co-authored-by: Piotr Osiewicz <piotr@zed.dev>
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Anthony Eid 2024-11-18 16:56:34 -05:00 committed by GitHub
parent 5b9916e34b
commit 889aac9c03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 321 additions and 39 deletions

View file

@ -8,7 +8,11 @@ pub struct Snippet {
pub tabstops: Vec<TabStop>,
}
type TabStop = SmallVec<[Range<isize>; 2]>;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct TabStop {
pub ranges: SmallVec<[Range<isize>; 2]>,
pub choices: Option<Vec<String>>,
}
impl Snippet {
pub fn parse(source: &str) -> Result<Self> {
@ -24,7 +28,11 @@ impl Snippet {
if let Some(final_tabstop) = final_tabstop {
tabstops.push(final_tabstop);
} else {
let end_tabstop = [len..len].into_iter().collect();
let end_tabstop = TabStop {
ranges: [len..len].into_iter().collect(),
choices: None,
};
if !tabstops.last().map_or(false, |t| *t == end_tabstop) {
tabstops.push(end_tabstop);
}
@ -88,11 +96,17 @@ fn parse_tabstop<'a>(
) -> Result<&'a str> {
let tabstop_start = text.len();
let tabstop_index;
let mut choices = None;
if source.starts_with('{') {
let (index, rest) = parse_int(&source[1..])?;
tabstop_index = index;
source = rest;
if source.starts_with("|") {
(source, choices) = parse_choices(&source[1..], text)?;
}
if source.starts_with(':') {
source = parse_snippet(&source[1..], true, text, tabstops)?;
}
@ -110,7 +124,11 @@ fn parse_tabstop<'a>(
tabstops
.entry(tabstop_index)
.or_default()
.or_insert_with(|| TabStop {
ranges: Default::default(),
choices,
})
.ranges
.push(tabstop_start as isize..text.len() as isize);
Ok(source)
}
@ -126,6 +144,61 @@ fn parse_int(source: &str) -> Result<(usize, &str)> {
Ok((prefix.parse()?, suffix))
}
fn parse_choices<'a>(
mut source: &'a str,
text: &mut String,
) -> Result<(&'a str, Option<Vec<String>>)> {
let mut found_default_choice = false;
let mut current_choice = String::new();
let mut choices = Vec::new();
loop {
match source.chars().next() {
None => return Ok(("", Some(choices))),
Some('\\') => {
source = &source[1..];
if let Some(c) = source.chars().next() {
if !found_default_choice {
current_choice.push(c);
text.push(c);
}
source = &source[c.len_utf8()..];
}
}
Some(',') => {
found_default_choice = true;
source = &source[1..];
choices.push(current_choice);
current_choice = String::new();
}
Some('|') => {
source = &source[1..];
choices.push(current_choice);
return Ok((source, Some(choices)));
}
Some(_) => {
let chunk_end = source.find([',', '|', '\\']);
if chunk_end.is_none() {
return Err(anyhow!(
"Placeholder choice doesn't contain closing pipe-character '|'"
));
}
let (chunk, rest) = source.split_at(chunk_end.unwrap());
if !found_default_choice {
text.push_str(chunk);
}
current_choice.push_str(chunk);
source = rest;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -142,11 +215,13 @@ mod tests {
let snippet = Snippet::parse("one$1two").unwrap();
assert_eq!(snippet.text, "onetwo");
assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]);
assert_eq!(tabstop_choices(&snippet), &[&None, &None]);
// Multi-digit numbers
let snippet = Snippet::parse("one$123-$99-two").unwrap();
assert_eq!(snippet.text, "one--two");
assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]);
assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]);
}
#[test]
@ -157,6 +232,7 @@ mod tests {
// an additional tabstop at the end.
assert_eq!(snippet.text, r#"foo."#);
assert_eq!(tabstops(&snippet), &[vec![4..4]]);
assert_eq!(tabstop_choices(&snippet), &[&None]);
}
#[test]
@ -167,6 +243,7 @@ mod tests {
// don't insert an additional tabstop at the end.
assert_eq!(snippet.text, r#"<div class=""></div>"#);
assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]);
assert_eq!(tabstop_choices(&snippet), &[&None, &None]);
}
#[test]
@ -177,6 +254,30 @@ mod tests {
tabstops(&snippet),
&[vec![3..6], vec![11..15], vec![15..15]]
);
assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]);
}
#[test]
fn test_snippet_with_choice_placeholders() {
let snippet = Snippet::parse("type ${1|i32, u32|} = $2")
.expect("Should be able to unpack choice placeholders");
assert_eq!(snippet.text, "type i32 = ");
assert_eq!(tabstops(&snippet), &[vec![5..8], vec![11..11],]);
assert_eq!(
tabstop_choices(&snippet),
&[&Some(vec!["i32".to_string(), " u32".to_string()]), &None]
);
let snippet = Snippet::parse(r"${1|\$\{1\|one\,two\,tree\|\}|}")
.expect("Should be able to parse choice with escape characters");
assert_eq!(snippet.text, "${1|one,two,tree|}");
assert_eq!(tabstops(&snippet), &[vec![0..18], vec![18..18]]);
assert_eq!(
tabstop_choices(&snippet),
&[&Some(vec!["${1|one,two,tree|}".to_string(),]), &None]
);
}
#[test]
@ -196,6 +297,10 @@ mod tests {
vec![40..40],
]
);
assert_eq!(
tabstop_choices(&snippet),
&[&None, &None, &None, &None, &None]
);
}
#[test]
@ -203,10 +308,12 @@ mod tests {
let snippet = Snippet::parse("\"\\$schema\": $1").unwrap();
assert_eq!(snippet.text, "\"$schema\": ");
assert_eq!(tabstops(&snippet), &[vec![11..11]]);
assert_eq!(tabstop_choices(&snippet), &[&None]);
let snippet = Snippet::parse("{a\\}").unwrap();
assert_eq!(snippet.text, "{a}");
assert_eq!(tabstops(&snippet), &[vec![3..3]]);
assert_eq!(tabstop_choices(&snippet), &[&None]);
// backslash not functioning as an escape
let snippet = Snippet::parse("a\\b").unwrap();
@ -221,6 +328,10 @@ mod tests {
}
fn tabstops(snippet: &Snippet) -> Vec<Vec<Range<isize>>> {
snippet.tabstops.iter().map(|t| t.to_vec()).collect()
snippet.tabstops.iter().map(|t| t.ranges.to_vec()).collect()
}
fn tabstop_choices(snippet: &Snippet) -> Vec<&Option<Vec<String>>> {
snippet.tabstops.iter().map(|t| &t.choices).collect()
}
}