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
|
@ -5,6 +5,7 @@ use gpui::{Task, ViewContext};
|
||||||
|
|
||||||
use crate::Editor;
|
use crate::Editor;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct DebouncedDelay {
|
pub struct DebouncedDelay {
|
||||||
task: Option<Task<()>>,
|
task: Option<Task<()>>,
|
||||||
cancel_channel: Option<oneshot::Sender<()>>,
|
cancel_channel: Option<oneshot::Sender<()>>,
|
||||||
|
|
|
@ -883,6 +883,7 @@ struct AutocloseRegion {
|
||||||
struct SnippetState {
|
struct SnippetState {
|
||||||
ranges: Vec<Vec<Range<Anchor>>>,
|
ranges: Vec<Vec<Range<Anchor>>>,
|
||||||
active_index: usize,
|
active_index: usize,
|
||||||
|
choices: Vec<Option<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
@ -1000,7 +1001,7 @@ enum ContextMenuOrigin {
|
||||||
GutterIndicator(DisplayRow),
|
GutterIndicator(DisplayRow),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
struct CompletionsMenu {
|
struct CompletionsMenu {
|
||||||
id: CompletionId,
|
id: CompletionId,
|
||||||
sort_completions: bool,
|
sort_completions: bool,
|
||||||
|
@ -1011,10 +1012,100 @@ struct CompletionsMenu {
|
||||||
matches: Arc<[StringMatch]>,
|
matches: Arc<[StringMatch]>,
|
||||||
selected_item: usize,
|
selected_item: usize,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
selected_completion_documentation_resolve_debounce: Arc<Mutex<DebouncedDelay>>,
|
selected_completion_documentation_resolve_debounce: Option<Arc<Mutex<DebouncedDelay>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompletionsMenu {
|
impl CompletionsMenu {
|
||||||
|
fn new(
|
||||||
|
id: CompletionId,
|
||||||
|
sort_completions: bool,
|
||||||
|
initial_position: Anchor,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
completions: Box<[Completion]>,
|
||||||
|
) -> Self {
|
||||||
|
let match_candidates = completions
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, completion)| StringMatchCandidate::new(id, completion.label.text.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
sort_completions,
|
||||||
|
initial_position,
|
||||||
|
buffer,
|
||||||
|
completions: Arc::new(RwLock::new(completions)),
|
||||||
|
match_candidates,
|
||||||
|
matches: Vec::new().into(),
|
||||||
|
selected_item: 0,
|
||||||
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
|
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||||
|
DebouncedDelay::new(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_snippet_choices(
|
||||||
|
id: CompletionId,
|
||||||
|
sort_completions: bool,
|
||||||
|
choices: &Vec<String>,
|
||||||
|
selection: Range<Anchor>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
) -> Self {
|
||||||
|
let completions = choices
|
||||||
|
.iter()
|
||||||
|
.map(|choice| Completion {
|
||||||
|
old_range: selection.start.text_anchor..selection.end.text_anchor,
|
||||||
|
new_text: choice.to_string(),
|
||||||
|
label: CodeLabel {
|
||||||
|
text: choice.to_string(),
|
||||||
|
runs: Default::default(),
|
||||||
|
filter_range: Default::default(),
|
||||||
|
},
|
||||||
|
server_id: LanguageServerId(usize::MAX),
|
||||||
|
documentation: None,
|
||||||
|
lsp_completion: Default::default(),
|
||||||
|
confirm: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let match_candidates = choices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
|
||||||
|
.collect();
|
||||||
|
let matches = choices
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, completion)| StringMatch {
|
||||||
|
candidate_id: id,
|
||||||
|
score: 1.,
|
||||||
|
positions: vec![],
|
||||||
|
string: completion.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
sort_completions,
|
||||||
|
initial_position: selection.start,
|
||||||
|
buffer,
|
||||||
|
completions: Arc::new(RwLock::new(completions)),
|
||||||
|
match_candidates,
|
||||||
|
matches,
|
||||||
|
selected_item: 0,
|
||||||
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
|
selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new(
|
||||||
|
DebouncedDelay::new(),
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suppress_documentation_resolution(mut self) -> Self {
|
||||||
|
self.selected_completion_documentation_resolve_debounce
|
||||||
|
.take();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn select_first(
|
fn select_first(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
@ -1115,6 +1206,12 @@ impl CompletionsMenu {
|
||||||
let Some(provider) = provider else {
|
let Some(provider) = provider else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(documentation_resolve) = self
|
||||||
|
.selected_completion_documentation_resolve_debounce
|
||||||
|
.as_ref()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let resolve_task = provider.resolve_completions(
|
let resolve_task = provider.resolve_completions(
|
||||||
self.buffer.clone(),
|
self.buffer.clone(),
|
||||||
|
@ -1127,15 +1224,13 @@ impl CompletionsMenu {
|
||||||
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
|
EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce;
|
||||||
let delay = Duration::from_millis(delay_ms);
|
let delay = Duration::from_millis(delay_ms);
|
||||||
|
|
||||||
self.selected_completion_documentation_resolve_debounce
|
documentation_resolve.lock().fire_new(delay, cx, |_, cx| {
|
||||||
.lock()
|
cx.spawn(move |this, mut cx| async move {
|
||||||
.fire_new(delay, cx, |_, cx| {
|
if let Some(true) = resolve_task.await.log_err() {
|
||||||
cx.spawn(move |this, mut cx| async move {
|
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||||
if let Some(true) = resolve_task.await.log_err() {
|
}
|
||||||
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
})
|
||||||
}
|
});
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible(&self) -> bool {
|
fn visible(&self) -> bool {
|
||||||
|
@ -1418,6 +1513,7 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct AvailableCodeAction {
|
struct AvailableCodeAction {
|
||||||
excerpt_id: ExcerptId,
|
excerpt_id: ExcerptId,
|
||||||
action: CodeAction,
|
action: CodeAction,
|
||||||
|
@ -4386,6 +4482,10 @@ impl Editor {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let position = self.selections.newest_anchor().head();
|
let position = self.selections.newest_anchor().head();
|
||||||
let (buffer, buffer_position) =
|
let (buffer, buffer_position) =
|
||||||
if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) {
|
if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) {
|
||||||
|
@ -4431,30 +4531,13 @@ impl Editor {
|
||||||
})?;
|
})?;
|
||||||
let completions = completions.await.log_err();
|
let completions = completions.await.log_err();
|
||||||
let menu = if let Some(completions) = completions {
|
let menu = if let Some(completions) = completions {
|
||||||
let mut menu = CompletionsMenu {
|
let mut menu = CompletionsMenu::new(
|
||||||
id,
|
id,
|
||||||
sort_completions,
|
sort_completions,
|
||||||
initial_position: position,
|
position,
|
||||||
match_candidates: completions
|
buffer.clone(),
|
||||||
.iter()
|
completions.into(),
|
||||||
.enumerate()
|
);
|
||||||
.map(|(id, completion)| {
|
|
||||||
StringMatchCandidate::new(
|
|
||||||
id,
|
|
||||||
completion.label.text[completion.label.filter_range.clone()]
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
buffer: buffer.clone(),
|
|
||||||
completions: Arc::new(RwLock::new(completions.into())),
|
|
||||||
matches: Vec::new().into(),
|
|
||||||
selected_item: 0,
|
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
|
||||||
selected_completion_documentation_resolve_debounce: Arc::new(Mutex::new(
|
|
||||||
DebouncedDelay::new(),
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -4657,7 +4740,11 @@ impl Editor {
|
||||||
self.transact(cx, |this, cx| {
|
self.transact(cx, |this, cx| {
|
||||||
if let Some(mut snippet) = snippet {
|
if let Some(mut snippet) = snippet {
|
||||||
snippet.text = text.to_string();
|
snippet.text = text.to_string();
|
||||||
for tabstop in snippet.tabstops.iter_mut().flatten() {
|
for tabstop in snippet
|
||||||
|
.tabstops
|
||||||
|
.iter_mut()
|
||||||
|
.flat_map(|tabstop| tabstop.ranges.iter_mut())
|
||||||
|
{
|
||||||
tabstop.start -= common_prefix_len as isize;
|
tabstop.start -= common_prefix_len as isize;
|
||||||
tabstop.end -= common_prefix_len as isize;
|
tabstop.end -= common_prefix_len as isize;
|
||||||
}
|
}
|
||||||
|
@ -5693,6 +5780,27 @@ impl Editor {
|
||||||
context_menu
|
context_menu
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn show_snippet_choices(
|
||||||
|
&mut self,
|
||||||
|
choices: &Vec<String>,
|
||||||
|
selection: Range<Anchor>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
if selection.start.buffer_id.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let buffer_id = selection.start.buffer_id.unwrap();
|
||||||
|
let buffer = self.buffer().read(cx).buffer(buffer_id);
|
||||||
|
let id = post_inc(&mut self.next_completion_id);
|
||||||
|
|
||||||
|
if let Some(buffer) = buffer {
|
||||||
|
*self.context_menu.write() = Some(ContextMenu::Completions(
|
||||||
|
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer)
|
||||||
|
.suppress_documentation_resolution(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn insert_snippet(
|
pub fn insert_snippet(
|
||||||
&mut self,
|
&mut self,
|
||||||
insertion_ranges: &[Range<usize>],
|
insertion_ranges: &[Range<usize>],
|
||||||
|
@ -5702,6 +5810,7 @@ impl Editor {
|
||||||
struct Tabstop<T> {
|
struct Tabstop<T> {
|
||||||
is_end_tabstop: bool,
|
is_end_tabstop: bool,
|
||||||
ranges: Vec<Range<T>>,
|
ranges: Vec<Range<T>>,
|
||||||
|
choices: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let tabstops = self.buffer.update(cx, |buffer, cx| {
|
let tabstops = self.buffer.update(cx, |buffer, cx| {
|
||||||
|
@ -5721,10 +5830,11 @@ impl Editor {
|
||||||
.tabstops
|
.tabstops
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tabstop| {
|
.map(|tabstop| {
|
||||||
let is_end_tabstop = tabstop.first().map_or(false, |tabstop| {
|
let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| {
|
||||||
tabstop.is_empty() && tabstop.start == snippet.text.len() as isize
|
tabstop.is_empty() && tabstop.start == snippet.text.len() as isize
|
||||||
});
|
});
|
||||||
let mut tabstop_ranges = tabstop
|
let mut tabstop_ranges = tabstop
|
||||||
|
.ranges
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|tabstop_range| {
|
.flat_map(|tabstop_range| {
|
||||||
let mut delta = 0_isize;
|
let mut delta = 0_isize;
|
||||||
|
@ -5746,6 +5856,7 @@ impl Editor {
|
||||||
Tabstop {
|
Tabstop {
|
||||||
is_end_tabstop,
|
is_end_tabstop,
|
||||||
ranges: tabstop_ranges,
|
ranges: tabstop_ranges,
|
||||||
|
choices: tabstop.choices.clone(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -5755,16 +5866,29 @@ impl Editor {
|
||||||
s.select_ranges(tabstop.ranges.iter().cloned());
|
s.select_ranges(tabstop.ranges.iter().cloned());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(choices) = &tabstop.choices {
|
||||||
|
if let Some(selection) = tabstop.ranges.first() {
|
||||||
|
self.show_snippet_choices(choices, selection.clone(), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If we're already at the last tabstop and it's at the end of the snippet,
|
// If we're already at the last tabstop and it's at the end of the snippet,
|
||||||
// we're done, we don't need to keep the state around.
|
// we're done, we don't need to keep the state around.
|
||||||
if !tabstop.is_end_tabstop {
|
if !tabstop.is_end_tabstop {
|
||||||
|
let choices = tabstops
|
||||||
|
.iter()
|
||||||
|
.map(|tabstop| tabstop.choices.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
let ranges = tabstops
|
let ranges = tabstops
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tabstop| tabstop.ranges)
|
.map(|tabstop| tabstop.ranges)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
self.snippet_stack.push(SnippetState {
|
self.snippet_stack.push(SnippetState {
|
||||||
active_index: 0,
|
active_index: 0,
|
||||||
ranges,
|
ranges,
|
||||||
|
choices,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5839,6 +5963,13 @@ impl Editor {
|
||||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
s.select_anchor_ranges(current_ranges.iter().cloned())
|
s.select_anchor_ranges(current_ranges.iter().cloned())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(choices) = &snippet.choices[snippet.active_index] {
|
||||||
|
if let Some(selection) = current_ranges.first() {
|
||||||
|
self.show_snippet_choices(&choices, selection.clone(), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If snippet state is not at the last tabstop, push it back on the stack
|
// If snippet state is not at the last tabstop, push it back on the stack
|
||||||
if snippet.active_index + 1 < snippet.ranges.len() {
|
if snippet.active_index + 1 < snippet.ranges.len() {
|
||||||
self.snippet_stack.push(snippet);
|
self.snippet_stack.push(snippet);
|
||||||
|
|
|
@ -6551,6 +6551,45 @@ async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_snippet_placeholder_choices(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let (text, insertion_ranges) = marked_text_ranges(
|
||||||
|
indoc! {"
|
||||||
|
ˇ
|
||||||
|
"},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
|
||||||
|
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
|
||||||
|
|
||||||
|
_ = editor.update(cx, |editor, cx| {
|
||||||
|
let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
|
||||||
|
|
||||||
|
editor
|
||||||
|
.insert_snippet(&insertion_ranges, snippet, cx)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
|
||||||
|
let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
|
||||||
|
assert_eq!(editor.text(cx), expected_text);
|
||||||
|
assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(
|
||||||
|
editor,
|
||||||
|
cx,
|
||||||
|
indoc! {"
|
||||||
|
type «» =•
|
||||||
|
"},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(editor.context_menu_visible(), "There should be a matches");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
async fn test_snippets(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
@ -8,7 +8,11 @@ pub struct Snippet {
|
||||||
pub tabstops: Vec<TabStop>,
|
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 {
|
impl Snippet {
|
||||||
pub fn parse(source: &str) -> Result<Self> {
|
pub fn parse(source: &str) -> Result<Self> {
|
||||||
|
@ -24,7 +28,11 @@ impl Snippet {
|
||||||
if let Some(final_tabstop) = final_tabstop {
|
if let Some(final_tabstop) = final_tabstop {
|
||||||
tabstops.push(final_tabstop);
|
tabstops.push(final_tabstop);
|
||||||
} else {
|
} 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) {
|
if !tabstops.last().map_or(false, |t| *t == end_tabstop) {
|
||||||
tabstops.push(end_tabstop);
|
tabstops.push(end_tabstop);
|
||||||
}
|
}
|
||||||
|
@ -88,11 +96,17 @@ fn parse_tabstop<'a>(
|
||||||
) -> Result<&'a str> {
|
) -> Result<&'a str> {
|
||||||
let tabstop_start = text.len();
|
let tabstop_start = text.len();
|
||||||
let tabstop_index;
|
let tabstop_index;
|
||||||
|
let mut choices = None;
|
||||||
|
|
||||||
if source.starts_with('{') {
|
if source.starts_with('{') {
|
||||||
let (index, rest) = parse_int(&source[1..])?;
|
let (index, rest) = parse_int(&source[1..])?;
|
||||||
tabstop_index = index;
|
tabstop_index = index;
|
||||||
source = rest;
|
source = rest;
|
||||||
|
|
||||||
|
if source.starts_with("|") {
|
||||||
|
(source, choices) = parse_choices(&source[1..], text)?;
|
||||||
|
}
|
||||||
|
|
||||||
if source.starts_with(':') {
|
if source.starts_with(':') {
|
||||||
source = parse_snippet(&source[1..], true, text, tabstops)?;
|
source = parse_snippet(&source[1..], true, text, tabstops)?;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +124,11 @@ fn parse_tabstop<'a>(
|
||||||
|
|
||||||
tabstops
|
tabstops
|
||||||
.entry(tabstop_index)
|
.entry(tabstop_index)
|
||||||
.or_default()
|
.or_insert_with(|| TabStop {
|
||||||
|
ranges: Default::default(),
|
||||||
|
choices,
|
||||||
|
})
|
||||||
|
.ranges
|
||||||
.push(tabstop_start as isize..text.len() as isize);
|
.push(tabstop_start as isize..text.len() as isize);
|
||||||
Ok(source)
|
Ok(source)
|
||||||
}
|
}
|
||||||
|
@ -126,6 +144,61 @@ fn parse_int(source: &str) -> Result<(usize, &str)> {
|
||||||
Ok((prefix.parse()?, suffix))
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -142,11 +215,13 @@ mod tests {
|
||||||
let snippet = Snippet::parse("one$1two").unwrap();
|
let snippet = Snippet::parse("one$1two").unwrap();
|
||||||
assert_eq!(snippet.text, "onetwo");
|
assert_eq!(snippet.text, "onetwo");
|
||||||
assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]);
|
assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]);
|
||||||
|
assert_eq!(tabstop_choices(&snippet), &[&None, &None]);
|
||||||
|
|
||||||
// Multi-digit numbers
|
// Multi-digit numbers
|
||||||
let snippet = Snippet::parse("one$123-$99-two").unwrap();
|
let snippet = Snippet::parse("one$123-$99-two").unwrap();
|
||||||
assert_eq!(snippet.text, "one--two");
|
assert_eq!(snippet.text, "one--two");
|
||||||
assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]);
|
assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]);
|
||||||
|
assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -157,6 +232,7 @@ mod tests {
|
||||||
// an additional tabstop at the end.
|
// an additional tabstop at the end.
|
||||||
assert_eq!(snippet.text, r#"foo."#);
|
assert_eq!(snippet.text, r#"foo."#);
|
||||||
assert_eq!(tabstops(&snippet), &[vec![4..4]]);
|
assert_eq!(tabstops(&snippet), &[vec![4..4]]);
|
||||||
|
assert_eq!(tabstop_choices(&snippet), &[&None]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -167,6 +243,7 @@ mod tests {
|
||||||
// don't insert an additional tabstop at the end.
|
// don't insert an additional tabstop at the end.
|
||||||
assert_eq!(snippet.text, r#"<div class=""></div>"#);
|
assert_eq!(snippet.text, r#"<div class=""></div>"#);
|
||||||
assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]);
|
assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]);
|
||||||
|
assert_eq!(tabstop_choices(&snippet), &[&None, &None]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -177,6 +254,30 @@ mod tests {
|
||||||
tabstops(&snippet),
|
tabstops(&snippet),
|
||||||
&[vec![3..6], vec![11..15], vec![15..15]]
|
&[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]
|
#[test]
|
||||||
|
@ -196,6 +297,10 @@ mod tests {
|
||||||
vec![40..40],
|
vec![40..40],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
tabstop_choices(&snippet),
|
||||||
|
&[&None, &None, &None, &None, &None]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -203,10 +308,12 @@ mod tests {
|
||||||
let snippet = Snippet::parse("\"\\$schema\": $1").unwrap();
|
let snippet = Snippet::parse("\"\\$schema\": $1").unwrap();
|
||||||
assert_eq!(snippet.text, "\"$schema\": ");
|
assert_eq!(snippet.text, "\"$schema\": ");
|
||||||
assert_eq!(tabstops(&snippet), &[vec![11..11]]);
|
assert_eq!(tabstops(&snippet), &[vec![11..11]]);
|
||||||
|
assert_eq!(tabstop_choices(&snippet), &[&None]);
|
||||||
|
|
||||||
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]]);
|
||||||
|
assert_eq!(tabstop_choices(&snippet), &[&None]);
|
||||||
|
|
||||||
// backslash not functioning as an escape
|
// backslash not functioning as an escape
|
||||||
let snippet = Snippet::parse("a\\b").unwrap();
|
let snippet = Snippet::parse("a\\b").unwrap();
|
||||||
|
@ -221,6 +328,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tabstops(snippet: &Snippet) -> Vec<Vec<Range<isize>>> {
|
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