editor: Fix completion accept for optional chaining in Typescript (#31878)
Closes #31662 Currently, we assume `insert_range` will always end at the cursor and `replace_range` will also always end after the cursor for calculating range to replace. This is a particular case for the rust-analyzer, but not widely true for other language servers. This PR fixes this assumption, and now `insert_range` and `replace_range` both can end before cursor. In this particular case: ```ts let x: string | undefined; x.tostˇ // here insert as well as replace range is just "." while new_text is "?.toString()" ``` This change makes it such that if final range to replace ends before cursor, we extend it till the cursor. Bonus: - Improves suffix and subsequence matching to use `label` over `new_text` as `new_text` can contain end characters like `()` or `$` which is not visible while accepting the completion. - Make suffix and subsequence check case insensitive. - Fixes broken subsequence matching which was not considering the order of characters while matching subsequence. Release Notes: - Fixed an issue where autocompleting optional chaining methods in TypeScript, such as `x.tostr`, would result in `x?.toString()tostr` instead of `x?.toString()`.
This commit is contained in:
parent
ab6125ddde
commit
06a199da4d
2 changed files with 228 additions and 109 deletions
|
@ -5368,7 +5368,6 @@ impl Editor {
|
||||||
mat.candidate_id
|
mat.candidate_id
|
||||||
};
|
};
|
||||||
|
|
||||||
let buffer_handle = completions_menu.buffer;
|
|
||||||
let completion = completions_menu
|
let completion = completions_menu
|
||||||
.completions
|
.completions
|
||||||
.borrow()
|
.borrow()
|
||||||
|
@ -5376,34 +5375,23 @@ impl Editor {
|
||||||
.clone();
|
.clone();
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
|
|
||||||
|
let buffer_handle = completions_menu.buffer;
|
||||||
|
|
||||||
|
let CompletionEdit {
|
||||||
|
new_text,
|
||||||
|
snippet,
|
||||||
|
replace_range,
|
||||||
|
} = process_completion_for_edit(
|
||||||
|
&completion,
|
||||||
|
intent,
|
||||||
|
&buffer_handle,
|
||||||
|
&completions_menu.initial_position.text_anchor,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let buffer = buffer_handle.read(cx);
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
let newest_anchor = self.selections.newest_anchor();
|
let newest_anchor = self.selections.newest_anchor();
|
||||||
|
|
||||||
let snippet;
|
|
||||||
let new_text;
|
|
||||||
if completion.is_snippet() {
|
|
||||||
let mut snippet_source = completion.new_text.clone();
|
|
||||||
if let Some(scope) = snapshot.language_scope_at(newest_anchor.head()) {
|
|
||||||
if scope.prefers_label_for_snippet_in_completion() {
|
|
||||||
if let Some(label) = completion.label() {
|
|
||||||
if matches!(
|
|
||||||
completion.kind(),
|
|
||||||
Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD)
|
|
||||||
) {
|
|
||||||
snippet_source = label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
snippet = Some(Snippet::parse(&snippet_source).log_err()?);
|
|
||||||
new_text = snippet.as_ref().unwrap().text.clone();
|
|
||||||
} else {
|
|
||||||
snippet = None;
|
|
||||||
new_text = completion.new_text.clone();
|
|
||||||
};
|
|
||||||
|
|
||||||
let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx);
|
|
||||||
let buffer = buffer_handle.read(cx);
|
|
||||||
let replace_range_multibuffer = {
|
let replace_range_multibuffer = {
|
||||||
let excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap();
|
let excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap();
|
||||||
let multibuffer_anchor = snapshot
|
let multibuffer_anchor = snapshot
|
||||||
|
@ -19514,79 +19502,152 @@ fn vim_enabled(cx: &App) -> bool {
|
||||||
== Some(&serde_json::Value::Bool(true))
|
== Some(&serde_json::Value::Bool(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consider user intent and default settings
|
fn process_completion_for_edit(
|
||||||
fn choose_completion_range(
|
|
||||||
completion: &Completion,
|
completion: &Completion,
|
||||||
intent: CompletionIntent,
|
intent: CompletionIntent,
|
||||||
buffer: &Entity<Buffer>,
|
buffer: &Entity<Buffer>,
|
||||||
|
cursor_position: &text::Anchor,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Range<usize> {
|
) -> CompletionEdit {
|
||||||
fn should_replace(
|
|
||||||
completion: &Completion,
|
|
||||||
insert_range: &Range<text::Anchor>,
|
|
||||||
intent: CompletionIntent,
|
|
||||||
completion_mode_setting: LspInsertMode,
|
|
||||||
buffer: &Buffer,
|
|
||||||
) -> bool {
|
|
||||||
// specific actions take precedence over settings
|
|
||||||
match intent {
|
|
||||||
CompletionIntent::CompleteWithInsert => return false,
|
|
||||||
CompletionIntent::CompleteWithReplace => return true,
|
|
||||||
CompletionIntent::Complete | CompletionIntent::Compose => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
match completion_mode_setting {
|
|
||||||
LspInsertMode::Insert => false,
|
|
||||||
LspInsertMode::Replace => true,
|
|
||||||
LspInsertMode::ReplaceSubsequence => {
|
|
||||||
let mut text_to_replace = buffer.chars_for_range(
|
|
||||||
buffer.anchor_before(completion.replace_range.start)
|
|
||||||
..buffer.anchor_after(completion.replace_range.end),
|
|
||||||
);
|
|
||||||
let mut completion_text = completion.new_text.chars();
|
|
||||||
|
|
||||||
// is `text_to_replace` a subsequence of `completion_text`
|
|
||||||
text_to_replace
|
|
||||||
.all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch))
|
|
||||||
}
|
|
||||||
LspInsertMode::ReplaceSuffix => {
|
|
||||||
let range_after_cursor = insert_range.end..completion.replace_range.end;
|
|
||||||
|
|
||||||
let text_after_cursor = buffer
|
|
||||||
.text_for_range(
|
|
||||||
buffer.anchor_before(range_after_cursor.start)
|
|
||||||
..buffer.anchor_after(range_after_cursor.end),
|
|
||||||
)
|
|
||||||
.collect::<String>();
|
|
||||||
completion.new_text.ends_with(&text_after_cursor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
|
let buffer_snapshot = buffer.snapshot();
|
||||||
if let CompletionSource::Lsp {
|
let (snippet, new_text) = if completion.is_snippet() {
|
||||||
insert_range: Some(insert_range),
|
let mut snippet_source = completion.new_text.clone();
|
||||||
..
|
if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
|
||||||
} = &completion.source
|
if scope.prefers_label_for_snippet_in_completion() {
|
||||||
{
|
if let Some(label) = completion.label() {
|
||||||
let completion_mode_setting =
|
if matches!(
|
||||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
|
completion.kind(),
|
||||||
.completions
|
Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD)
|
||||||
.lsp_insert_mode;
|
) {
|
||||||
|
snippet_source = label;
|
||||||
if !should_replace(
|
}
|
||||||
completion,
|
}
|
||||||
&insert_range,
|
}
|
||||||
intent,
|
|
||||||
completion_mode_setting,
|
|
||||||
buffer,
|
|
||||||
) {
|
|
||||||
return insert_range.to_offset(buffer);
|
|
||||||
}
|
}
|
||||||
|
match Snippet::parse(&snippet_source).log_err() {
|
||||||
|
Some(parsed_snippet) => (Some(parsed_snippet.clone()), parsed_snippet.text),
|
||||||
|
None => (None, completion.new_text.clone()),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(None, completion.new_text.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut range_to_replace = {
|
||||||
|
let replace_range = &completion.replace_range;
|
||||||
|
if let CompletionSource::Lsp {
|
||||||
|
insert_range: Some(insert_range),
|
||||||
|
..
|
||||||
|
} = &completion.source
|
||||||
|
{
|
||||||
|
debug_assert_eq!(
|
||||||
|
insert_range.start, replace_range.start,
|
||||||
|
"insert_range and replace_range should start at the same position"
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
insert_range
|
||||||
|
.start
|
||||||
|
.cmp(&cursor_position, &buffer_snapshot)
|
||||||
|
.is_le(),
|
||||||
|
"insert_range should start before or at cursor position"
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
replace_range
|
||||||
|
.start
|
||||||
|
.cmp(&cursor_position, &buffer_snapshot)
|
||||||
|
.is_le(),
|
||||||
|
"replace_range should start before or at cursor position"
|
||||||
|
);
|
||||||
|
debug_assert!(
|
||||||
|
insert_range
|
||||||
|
.end
|
||||||
|
.cmp(&cursor_position, &buffer_snapshot)
|
||||||
|
.is_le(),
|
||||||
|
"insert_range should end before or at cursor position"
|
||||||
|
);
|
||||||
|
|
||||||
|
let should_replace = match intent {
|
||||||
|
CompletionIntent::CompleteWithInsert => false,
|
||||||
|
CompletionIntent::CompleteWithReplace => true,
|
||||||
|
CompletionIntent::Complete | CompletionIntent::Compose => {
|
||||||
|
let insert_mode =
|
||||||
|
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
|
||||||
|
.completions
|
||||||
|
.lsp_insert_mode;
|
||||||
|
match insert_mode {
|
||||||
|
LspInsertMode::Insert => false,
|
||||||
|
LspInsertMode::Replace => true,
|
||||||
|
LspInsertMode::ReplaceSubsequence => {
|
||||||
|
let mut text_to_replace = buffer.chars_for_range(
|
||||||
|
buffer.anchor_before(replace_range.start)
|
||||||
|
..buffer.anchor_after(replace_range.end),
|
||||||
|
);
|
||||||
|
let mut current_needle = text_to_replace.next();
|
||||||
|
for haystack_ch in completion.label.text.chars() {
|
||||||
|
if let Some(needle_ch) = current_needle {
|
||||||
|
if haystack_ch.eq_ignore_ascii_case(&needle_ch) {
|
||||||
|
current_needle = text_to_replace.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current_needle.is_none()
|
||||||
|
}
|
||||||
|
LspInsertMode::ReplaceSuffix => {
|
||||||
|
if replace_range
|
||||||
|
.end
|
||||||
|
.cmp(&cursor_position, &buffer_snapshot)
|
||||||
|
.is_gt()
|
||||||
|
{
|
||||||
|
let range_after_cursor = *cursor_position..replace_range.end;
|
||||||
|
let text_after_cursor = buffer
|
||||||
|
.text_for_range(
|
||||||
|
buffer.anchor_before(range_after_cursor.start)
|
||||||
|
..buffer.anchor_after(range_after_cursor.end),
|
||||||
|
)
|
||||||
|
.collect::<String>()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
completion
|
||||||
|
.label
|
||||||
|
.text
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.ends_with(&text_after_cursor)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_replace {
|
||||||
|
replace_range.clone()
|
||||||
|
} else {
|
||||||
|
insert_range.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
replace_range.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if range_to_replace
|
||||||
|
.end
|
||||||
|
.cmp(&cursor_position, &buffer_snapshot)
|
||||||
|
.is_lt()
|
||||||
|
{
|
||||||
|
range_to_replace.end = *cursor_position;
|
||||||
}
|
}
|
||||||
|
|
||||||
completion.replace_range.to_offset(buffer)
|
CompletionEdit {
|
||||||
|
new_text,
|
||||||
|
replace_range: range_to_replace.to_offset(&buffer),
|
||||||
|
snippet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CompletionEdit {
|
||||||
|
new_text: String,
|
||||||
|
replace_range: Range<usize>,
|
||||||
|
snippet: Option<Snippet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert_extra_newline_brackets(
|
fn insert_extra_newline_brackets(
|
||||||
|
|
|
@ -10479,6 +10479,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: &'static str,
|
run_description: &'static str,
|
||||||
initial_state: String,
|
initial_state: String,
|
||||||
buffer_marked_text: String,
|
buffer_marked_text: String,
|
||||||
|
completion_label: &'static str,
|
||||||
completion_text: &'static str,
|
completion_text: &'static str,
|
||||||
expected_with_insert_mode: String,
|
expected_with_insert_mode: String,
|
||||||
expected_with_replace_mode: String,
|
expected_with_replace_mode: String,
|
||||||
|
@ -10491,6 +10492,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Start of word matches completion text",
|
run_description: "Start of word matches completion text",
|
||||||
initial_state: "before ediˇ after".into(),
|
initial_state: "before ediˇ after".into(),
|
||||||
buffer_marked_text: "before <edi|> after".into(),
|
buffer_marked_text: "before <edi|> after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
completion_text: "editor",
|
completion_text: "editor",
|
||||||
expected_with_insert_mode: "before editorˇ after".into(),
|
expected_with_insert_mode: "before editorˇ after".into(),
|
||||||
expected_with_replace_mode: "before editorˇ after".into(),
|
expected_with_replace_mode: "before editorˇ after".into(),
|
||||||
|
@ -10501,6 +10503,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Accept same text at the middle of the word",
|
run_description: "Accept same text at the middle of the word",
|
||||||
initial_state: "before ediˇtor after".into(),
|
initial_state: "before ediˇtor after".into(),
|
||||||
buffer_marked_text: "before <edi|tor> after".into(),
|
buffer_marked_text: "before <edi|tor> after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
completion_text: "editor",
|
completion_text: "editor",
|
||||||
expected_with_insert_mode: "before editorˇtor after".into(),
|
expected_with_insert_mode: "before editorˇtor after".into(),
|
||||||
expected_with_replace_mode: "before editorˇ after".into(),
|
expected_with_replace_mode: "before editorˇ after".into(),
|
||||||
|
@ -10511,6 +10514,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "End of word matches completion text -- cursor at end",
|
run_description: "End of word matches completion text -- cursor at end",
|
||||||
initial_state: "before torˇ after".into(),
|
initial_state: "before torˇ after".into(),
|
||||||
buffer_marked_text: "before <tor|> after".into(),
|
buffer_marked_text: "before <tor|> after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
completion_text: "editor",
|
completion_text: "editor",
|
||||||
expected_with_insert_mode: "before editorˇ after".into(),
|
expected_with_insert_mode: "before editorˇ after".into(),
|
||||||
expected_with_replace_mode: "before editorˇ after".into(),
|
expected_with_replace_mode: "before editorˇ after".into(),
|
||||||
|
@ -10521,6 +10525,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "End of word matches completion text -- cursor at start",
|
run_description: "End of word matches completion text -- cursor at start",
|
||||||
initial_state: "before ˇtor after".into(),
|
initial_state: "before ˇtor after".into(),
|
||||||
buffer_marked_text: "before <|tor> after".into(),
|
buffer_marked_text: "before <|tor> after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
completion_text: "editor",
|
completion_text: "editor",
|
||||||
expected_with_insert_mode: "before editorˇtor after".into(),
|
expected_with_insert_mode: "before editorˇtor after".into(),
|
||||||
expected_with_replace_mode: "before editorˇ after".into(),
|
expected_with_replace_mode: "before editorˇ after".into(),
|
||||||
|
@ -10531,6 +10536,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Prepend text containing whitespace",
|
run_description: "Prepend text containing whitespace",
|
||||||
initial_state: "pˇfield: bool".into(),
|
initial_state: "pˇfield: bool".into(),
|
||||||
buffer_marked_text: "<p|field>: bool".into(),
|
buffer_marked_text: "<p|field>: bool".into(),
|
||||||
|
completion_label: "pub ",
|
||||||
completion_text: "pub ",
|
completion_text: "pub ",
|
||||||
expected_with_insert_mode: "pub ˇfield: bool".into(),
|
expected_with_insert_mode: "pub ˇfield: bool".into(),
|
||||||
expected_with_replace_mode: "pub ˇ: bool".into(),
|
expected_with_replace_mode: "pub ˇ: bool".into(),
|
||||||
|
@ -10541,6 +10547,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Add element to start of list",
|
run_description: "Add element to start of list",
|
||||||
initial_state: "[element_ˇelement_2]".into(),
|
initial_state: "[element_ˇelement_2]".into(),
|
||||||
buffer_marked_text: "[<element_|element_2>]".into(),
|
buffer_marked_text: "[<element_|element_2>]".into(),
|
||||||
|
completion_label: "element_1",
|
||||||
completion_text: "element_1",
|
completion_text: "element_1",
|
||||||
expected_with_insert_mode: "[element_1ˇelement_2]".into(),
|
expected_with_insert_mode: "[element_1ˇelement_2]".into(),
|
||||||
expected_with_replace_mode: "[element_1ˇ]".into(),
|
expected_with_replace_mode: "[element_1ˇ]".into(),
|
||||||
|
@ -10551,6 +10558,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Add element to start of list -- first and second elements are equal",
|
run_description: "Add element to start of list -- first and second elements are equal",
|
||||||
initial_state: "[elˇelement]".into(),
|
initial_state: "[elˇelement]".into(),
|
||||||
buffer_marked_text: "[<el|element>]".into(),
|
buffer_marked_text: "[<el|element>]".into(),
|
||||||
|
completion_label: "element",
|
||||||
completion_text: "element",
|
completion_text: "element",
|
||||||
expected_with_insert_mode: "[elementˇelement]".into(),
|
expected_with_insert_mode: "[elementˇelement]".into(),
|
||||||
expected_with_replace_mode: "[elementˇ]".into(),
|
expected_with_replace_mode: "[elementˇ]".into(),
|
||||||
|
@ -10561,6 +10569,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Ends with matching suffix",
|
run_description: "Ends with matching suffix",
|
||||||
initial_state: "SubˇError".into(),
|
initial_state: "SubˇError".into(),
|
||||||
buffer_marked_text: "<Sub|Error>".into(),
|
buffer_marked_text: "<Sub|Error>".into(),
|
||||||
|
completion_label: "SubscriptionError",
|
||||||
completion_text: "SubscriptionError",
|
completion_text: "SubscriptionError",
|
||||||
expected_with_insert_mode: "SubscriptionErrorˇError".into(),
|
expected_with_insert_mode: "SubscriptionErrorˇError".into(),
|
||||||
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
||||||
|
@ -10571,6 +10580,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Suffix is a subsequence -- contiguous",
|
run_description: "Suffix is a subsequence -- contiguous",
|
||||||
initial_state: "SubˇErr".into(),
|
initial_state: "SubˇErr".into(),
|
||||||
buffer_marked_text: "<Sub|Err>".into(),
|
buffer_marked_text: "<Sub|Err>".into(),
|
||||||
|
completion_label: "SubscriptionError",
|
||||||
completion_text: "SubscriptionError",
|
completion_text: "SubscriptionError",
|
||||||
expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
|
expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
|
||||||
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
||||||
|
@ -10581,6 +10591,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
|
run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
|
||||||
initial_state: "Suˇscrirr".into(),
|
initial_state: "Suˇscrirr".into(),
|
||||||
buffer_marked_text: "<Su|scrirr>".into(),
|
buffer_marked_text: "<Su|scrirr>".into(),
|
||||||
|
completion_label: "SubscriptionError",
|
||||||
completion_text: "SubscriptionError",
|
completion_text: "SubscriptionError",
|
||||||
expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
|
expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
|
||||||
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
expected_with_replace_mode: "SubscriptionErrorˇ".into(),
|
||||||
|
@ -10591,12 +10602,46 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
|
run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
|
||||||
initial_state: "foo(indˇix)".into(),
|
initial_state: "foo(indˇix)".into(),
|
||||||
buffer_marked_text: "foo(<ind|ix>)".into(),
|
buffer_marked_text: "foo(<ind|ix>)".into(),
|
||||||
|
completion_label: "node_index",
|
||||||
completion_text: "node_index",
|
completion_text: "node_index",
|
||||||
expected_with_insert_mode: "foo(node_indexˇix)".into(),
|
expected_with_insert_mode: "foo(node_indexˇix)".into(),
|
||||||
expected_with_replace_mode: "foo(node_indexˇ)".into(),
|
expected_with_replace_mode: "foo(node_indexˇ)".into(),
|
||||||
expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
|
expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
|
||||||
expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
|
expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
|
||||||
},
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Replace range ends before cursor - should extend to cursor",
|
||||||
|
initial_state: "before editˇo after".into(),
|
||||||
|
buffer_marked_text: "before <{ed}>it|o after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
|
completion_text: "editor",
|
||||||
|
expected_with_insert_mode: "before editorˇo after".into(),
|
||||||
|
expected_with_replace_mode: "before editorˇo after".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "before editorˇo after".into(),
|
||||||
|
expected_with_replace_suffix_mode: "before editorˇo after".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Uses label for suffix matching",
|
||||||
|
initial_state: "before ediˇtor after".into(),
|
||||||
|
buffer_marked_text: "before <edi|tor> after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
|
completion_text: "editor()",
|
||||||
|
expected_with_insert_mode: "before editor()ˇtor after".into(),
|
||||||
|
expected_with_replace_mode: "before editor()ˇ after".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
|
||||||
|
expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
|
||||||
|
},
|
||||||
|
Run {
|
||||||
|
run_description: "Case insensitive subsequence and suffix matching",
|
||||||
|
initial_state: "before EDiˇtoR after".into(),
|
||||||
|
buffer_marked_text: "before <EDi|toR> after".into(),
|
||||||
|
completion_label: "editor",
|
||||||
|
completion_text: "editor",
|
||||||
|
expected_with_insert_mode: "before editorˇtoR after".into(),
|
||||||
|
expected_with_replace_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_subsequence_mode: "before editorˇ after".into(),
|
||||||
|
expected_with_replace_suffix_mode: "before editorˇ after".into(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for run in runs {
|
for run in runs {
|
||||||
|
@ -10637,7 +10682,7 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
&run.buffer_marked_text,
|
&run.buffer_marked_text,
|
||||||
vec![run.completion_text],
|
vec![(run.completion_label, run.completion_text)],
|
||||||
counter.clone(),
|
counter.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -10697,7 +10742,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
&buffer_marked_text,
|
&buffer_marked_text,
|
||||||
vec![completion_text],
|
vec![(completion_text, completion_text)],
|
||||||
counter.clone(),
|
counter.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -10731,7 +10776,7 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
&buffer_marked_text,
|
&buffer_marked_text,
|
||||||
vec![completion_text],
|
vec![(completion_text, completion_text)],
|
||||||
counter.clone(),
|
counter.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -10818,7 +10863,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
completion_marked_buffer,
|
completion_marked_buffer,
|
||||||
vec![completion_text],
|
vec![(completion_text, completion_text)],
|
||||||
Arc::new(AtomicUsize::new(0)),
|
Arc::new(AtomicUsize::new(0)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -10872,7 +10917,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
completion_marked_buffer,
|
completion_marked_buffer,
|
||||||
vec![completion_text],
|
vec![(completion_text, completion_text)],
|
||||||
Arc::new(AtomicUsize::new(0)),
|
Arc::new(AtomicUsize::new(0)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -10921,7 +10966,7 @@ async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut T
|
||||||
handle_completion_request_with_insert_and_replace(
|
handle_completion_request_with_insert_and_replace(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
completion_marked_buffer,
|
completion_marked_buffer,
|
||||||
vec![completion_text],
|
vec![(completion_text, completion_text)],
|
||||||
Arc::new(AtomicUsize::new(0)),
|
Arc::new(AtomicUsize::new(0)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
@ -21064,19 +21109,27 @@ pub fn handle_completion_request(
|
||||||
/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
|
/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
|
||||||
/// given instead, which also contains an `insert` range.
|
/// given instead, which also contains an `insert` range.
|
||||||
///
|
///
|
||||||
/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
|
/// This function uses markers to define ranges:
|
||||||
/// that is, `replace_range.start..cursor_pos`.
|
/// - `|` marks the cursor position
|
||||||
|
/// - `<>` marks the replace range
|
||||||
|
/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
|
||||||
pub fn handle_completion_request_with_insert_and_replace(
|
pub fn handle_completion_request_with_insert_and_replace(
|
||||||
cx: &mut EditorLspTestContext,
|
cx: &mut EditorLspTestContext,
|
||||||
marked_string: &str,
|
marked_string: &str,
|
||||||
completions: Vec<&'static str>,
|
completions: Vec<(&'static str, &'static str)>, // (label, new_text)
|
||||||
counter: Arc<AtomicUsize>,
|
counter: Arc<AtomicUsize>,
|
||||||
) -> impl Future<Output = ()> {
|
) -> impl Future<Output = ()> {
|
||||||
let complete_from_marker: TextRangeMarker = '|'.into();
|
let complete_from_marker: TextRangeMarker = '|'.into();
|
||||||
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
let replace_range_marker: TextRangeMarker = ('<', '>').into();
|
||||||
|
let insert_range_marker: TextRangeMarker = ('{', '}').into();
|
||||||
|
|
||||||
let (_, mut marked_ranges) = marked_text_ranges_by(
|
let (_, mut marked_ranges) = marked_text_ranges_by(
|
||||||
marked_string,
|
marked_string,
|
||||||
vec![complete_from_marker.clone(), replace_range_marker.clone()],
|
vec![
|
||||||
|
complete_from_marker.clone(),
|
||||||
|
replace_range_marker.clone(),
|
||||||
|
insert_range_marker.clone(),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let complete_from_position =
|
let complete_from_position =
|
||||||
|
@ -21084,6 +21137,14 @@ pub fn handle_completion_request_with_insert_and_replace(
|
||||||
let replace_range =
|
let replace_range =
|
||||||
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
|
||||||
|
|
||||||
|
let insert_range = match marked_ranges.remove(&insert_range_marker) {
|
||||||
|
Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
|
||||||
|
_ => lsp::Range {
|
||||||
|
start: replace_range.start,
|
||||||
|
end: complete_from_position,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let mut request =
|
let mut request =
|
||||||
cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
|
cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
|
||||||
let completions = completions.clone();
|
let completions = completions.clone();
|
||||||
|
@ -21097,16 +21158,13 @@ pub fn handle_completion_request_with_insert_and_replace(
|
||||||
Ok(Some(lsp::CompletionResponse::Array(
|
Ok(Some(lsp::CompletionResponse::Array(
|
||||||
completions
|
completions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|completion_text| lsp::CompletionItem {
|
.map(|(label, new_text)| lsp::CompletionItem {
|
||||||
label: completion_text.to_string(),
|
label: label.to_string(),
|
||||||
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
|
||||||
lsp::InsertReplaceEdit {
|
lsp::InsertReplaceEdit {
|
||||||
insert: lsp::Range {
|
insert: insert_range,
|
||||||
start: replace_range.start,
|
|
||||||
end: complete_from_position,
|
|
||||||
},
|
|
||||||
replace: replace_range,
|
replace: replace_range,
|
||||||
new_text: completion_text.to_string(),
|
new_text: new_text.to_string(),
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue