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
|
||||
};
|
||||
|
||||
let buffer_handle = completions_menu.buffer;
|
||||
let completion = completions_menu
|
||||
.completions
|
||||
.borrow()
|
||||
|
@ -5376,34 +5375,23 @@ impl Editor {
|
|||
.clone();
|
||||
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 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 excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap();
|
||||
let multibuffer_anchor = snapshot
|
||||
|
@ -19514,79 +19502,152 @@ fn vim_enabled(cx: &App) -> bool {
|
|||
== Some(&serde_json::Value::Bool(true))
|
||||
}
|
||||
|
||||
// Consider user intent and default settings
|
||||
fn choose_completion_range(
|
||||
fn process_completion_for_edit(
|
||||
completion: &Completion,
|
||||
intent: CompletionIntent,
|
||||
buffer: &Entity<Buffer>,
|
||||
cursor_position: &text::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Range<usize> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
) -> CompletionEdit {
|
||||
let buffer = buffer.read(cx);
|
||||
|
||||
if let CompletionSource::Lsp {
|
||||
insert_range: Some(insert_range),
|
||||
..
|
||||
} = &completion.source
|
||||
{
|
||||
let completion_mode_setting =
|
||||
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
|
||||
.completions
|
||||
.lsp_insert_mode;
|
||||
|
||||
if !should_replace(
|
||||
completion,
|
||||
&insert_range,
|
||||
intent,
|
||||
completion_mode_setting,
|
||||
buffer,
|
||||
) {
|
||||
return insert_range.to_offset(buffer);
|
||||
let buffer_snapshot = buffer.snapshot();
|
||||
let (snippet, new_text) = if completion.is_snippet() {
|
||||
let mut snippet_source = completion.new_text.clone();
|
||||
if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue