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:
parent
5b9916e34b
commit
889aac9c03
4 changed files with 321 additions and 39 deletions
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue