Skip to content

Commit 06a199d

Browse files
authored
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()`.
1 parent ab6125d commit 06a199d

File tree

2 files changed

+222
-103
lines changed

2 files changed

+222
-103
lines changed

crates/editor/src/editor.rs

Lines changed: 147 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5368,42 +5368,30 @@ impl Editor {
53685368
mat.candidate_id
53695369
};
53705370

5371-
let buffer_handle = completions_menu.buffer;
53725371
let completion = completions_menu
53735372
.completions
53745373
.borrow()
53755374
.get(candidate_id)?
53765375
.clone();
53775376
cx.stop_propagation();
53785377

5379-
let snapshot = self.buffer.read(cx).snapshot(cx);
5380-
let newest_anchor = self.selections.newest_anchor();
5378+
let buffer_handle = completions_menu.buffer;
53815379

5382-
let snippet;
5383-
let new_text;
5384-
if completion.is_snippet() {
5385-
let mut snippet_source = completion.new_text.clone();
5386-
if let Some(scope) = snapshot.language_scope_at(newest_anchor.head()) {
5387-
if scope.prefers_label_for_snippet_in_completion() {
5388-
if let Some(label) = completion.label() {
5389-
if matches!(
5390-
completion.kind(),
5391-
Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD)
5392-
) {
5393-
snippet_source = label;
5394-
}
5395-
}
5396-
}
5397-
}
5398-
snippet = Some(Snippet::parse(&snippet_source).log_err()?);
5399-
new_text = snippet.as_ref().unwrap().text.clone();
5400-
} else {
5401-
snippet = None;
5402-
new_text = completion.new_text.clone();
5403-
};
5380+
let CompletionEdit {
5381+
new_text,
5382+
snippet,
5383+
replace_range,
5384+
} = process_completion_for_edit(
5385+
&completion,
5386+
intent,
5387+
&buffer_handle,
5388+
&completions_menu.initial_position.text_anchor,
5389+
cx,
5390+
);
54045391

5405-
let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx);
54065392
let buffer = buffer_handle.read(cx);
5393+
let snapshot = self.buffer.read(cx).snapshot(cx);
5394+
let newest_anchor = self.selections.newest_anchor();
54075395
let replace_range_multibuffer = {
54085396
let excerpt = snapshot.excerpt_containing(newest_anchor.range()).unwrap();
54095397
let multibuffer_anchor = snapshot
@@ -19514,79 +19502,152 @@ fn vim_enabled(cx: &App) -> bool {
1951419502
== Some(&serde_json::Value::Bool(true))
1951519503
}
1951619504

19517-
// Consider user intent and default settings
19518-
fn choose_completion_range(
19505+
fn process_completion_for_edit(
1951919506
completion: &Completion,
1952019507
intent: CompletionIntent,
1952119508
buffer: &Entity<Buffer>,
19509+
cursor_position: &text::Anchor,
1952219510
cx: &mut Context<Editor>,
19523-
) -> Range<usize> {
19524-
fn should_replace(
19525-
completion: &Completion,
19526-
insert_range: &Range<text::Anchor>,
19527-
intent: CompletionIntent,
19528-
completion_mode_setting: LspInsertMode,
19529-
buffer: &Buffer,
19530-
) -> bool {
19531-
// specific actions take precedence over settings
19532-
match intent {
19533-
CompletionIntent::CompleteWithInsert => return false,
19534-
CompletionIntent::CompleteWithReplace => return true,
19535-
CompletionIntent::Complete | CompletionIntent::Compose => {}
19536-
}
19537-
19538-
match completion_mode_setting {
19539-
LspInsertMode::Insert => false,
19540-
LspInsertMode::Replace => true,
19541-
LspInsertMode::ReplaceSubsequence => {
19542-
let mut text_to_replace = buffer.chars_for_range(
19543-
buffer.anchor_before(completion.replace_range.start)
19544-
..buffer.anchor_after(completion.replace_range.end),
19545-
);
19546-
let mut completion_text = completion.new_text.chars();
19547-
19548-
// is `text_to_replace` a subsequence of `completion_text`
19549-
text_to_replace
19550-
.all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch))
19511+
) -> CompletionEdit {
19512+
let buffer = buffer.read(cx);
19513+
let buffer_snapshot = buffer.snapshot();
19514+
let (snippet, new_text) = if completion.is_snippet() {
19515+
let mut snippet_source = completion.new_text.clone();
19516+
if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {
19517+
if scope.prefers_label_for_snippet_in_completion() {
19518+
if let Some(label) = completion.label() {
19519+
if matches!(
19520+
completion.kind(),
19521+
Some(CompletionItemKind::FUNCTION) | Some(CompletionItemKind::METHOD)
19522+
) {
19523+
snippet_source = label;
19524+
}
19525+
}
1955119526
}
19552-
LspInsertMode::ReplaceSuffix => {
19553-
let range_after_cursor = insert_range.end..completion.replace_range.end;
19527+
}
19528+
match Snippet::parse(&snippet_source).log_err() {
19529+
Some(parsed_snippet) => (Some(parsed_snippet.clone()), parsed_snippet.text),
19530+
None => (None, completion.new_text.clone()),
19531+
}
19532+
} else {
19533+
(None, completion.new_text.clone())
19534+
};
1955419535

19555-
let text_after_cursor = buffer
19556-
.text_for_range(
19557-
buffer.anchor_before(range_after_cursor.start)
19558-
..buffer.anchor_after(range_after_cursor.end),
19559-
)
19560-
.collect::<String>();
19561-
completion.new_text.ends_with(&text_after_cursor)
19536+
let mut range_to_replace = {
19537+
let replace_range = &completion.replace_range;
19538+
if let CompletionSource::Lsp {
19539+
insert_range: Some(insert_range),
19540+
..
19541+
} = &completion.source
19542+
{
19543+
debug_assert_eq!(
19544+
insert_range.start, replace_range.start,
19545+
"insert_range and replace_range should start at the same position"
19546+
);
19547+
debug_assert!(
19548+
insert_range
19549+
.start
19550+
.cmp(&cursor_position, &buffer_snapshot)
19551+
.is_le(),
19552+
"insert_range should start before or at cursor position"
19553+
);
19554+
debug_assert!(
19555+
replace_range
19556+
.start
19557+
.cmp(&cursor_position, &buffer_snapshot)
19558+
.is_le(),
19559+
"replace_range should start before or at cursor position"
19560+
);
19561+
debug_assert!(
19562+
insert_range
19563+
.end
19564+
.cmp(&cursor_position, &buffer_snapshot)
19565+
.is_le(),
19566+
"insert_range should end before or at cursor position"
19567+
);
19568+
19569+
let should_replace = match intent {
19570+
CompletionIntent::CompleteWithInsert => false,
19571+
CompletionIntent::CompleteWithReplace => true,
19572+
CompletionIntent::Complete | CompletionIntent::Compose => {
19573+
let insert_mode =
19574+
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
19575+
.completions
19576+
.lsp_insert_mode;
19577+
match insert_mode {
19578+
LspInsertMode::Insert => false,
19579+
LspInsertMode::Replace => true,
19580+
LspInsertMode::ReplaceSubsequence => {
19581+
let mut text_to_replace = buffer.chars_for_range(
19582+
buffer.anchor_before(replace_range.start)
19583+
..buffer.anchor_after(replace_range.end),
19584+
);
19585+
let mut current_needle = text_to_replace.next();
19586+
for haystack_ch in completion.label.text.chars() {
19587+
if let Some(needle_ch) = current_needle {
19588+
if haystack_ch.eq_ignore_ascii_case(&needle_ch) {
19589+
current_needle = text_to_replace.next();
19590+
}
19591+
}
19592+
}
19593+
current_needle.is_none()
19594+
}
19595+
LspInsertMode::ReplaceSuffix => {
19596+
if replace_range
19597+
.end
19598+
.cmp(&cursor_position, &buffer_snapshot)
19599+
.is_gt()
19600+
{
19601+
let range_after_cursor = *cursor_position..replace_range.end;
19602+
let text_after_cursor = buffer
19603+
.text_for_range(
19604+
buffer.anchor_before(range_after_cursor.start)
19605+
..buffer.anchor_after(range_after_cursor.end),
19606+
)
19607+
.collect::<String>()
19608+
.to_ascii_lowercase();
19609+
completion
19610+
.label
19611+
.text
19612+
.to_ascii_lowercase()
19613+
.ends_with(&text_after_cursor)
19614+
} else {
19615+
true
19616+
}
19617+
}
19618+
}
19619+
}
19620+
};
19621+
19622+
if should_replace {
19623+
replace_range.clone()
19624+
} else {
19625+
insert_range.clone()
1956219626
}
19627+
} else {
19628+
replace_range.clone()
1956319629
}
19564-
}
19565-
19566-
let buffer = buffer.read(cx);
19630+
};
1956719631

19568-
if let CompletionSource::Lsp {
19569-
insert_range: Some(insert_range),
19570-
..
19571-
} = &completion.source
19632+
if range_to_replace
19633+
.end
19634+
.cmp(&cursor_position, &buffer_snapshot)
19635+
.is_lt()
1957219636
{
19573-
let completion_mode_setting =
19574-
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
19575-
.completions
19576-
.lsp_insert_mode;
19637+
range_to_replace.end = *cursor_position;
19638+
}
1957719639

19578-
if !should_replace(
19579-
completion,
19580-
&insert_range,
19581-
intent,
19582-
completion_mode_setting,
19583-
buffer,
19584-
) {
19585-
return insert_range.to_offset(buffer);
19586-
}
19640+
CompletionEdit {
19641+
new_text,
19642+
replace_range: range_to_replace.to_offset(&buffer),
19643+
snippet,
1958719644
}
19645+
}
1958819646

19589-
completion.replace_range.to_offset(buffer)
19647+
struct CompletionEdit {
19648+
new_text: String,
19649+
replace_range: Range<usize>,
19650+
snippet: Option<Snippet>,
1959019651
}
1959119652

1959219653
fn insert_extra_newline_brackets(

0 commit comments

Comments
 (0)