Fix bugs with multicursor completions (#28586)

Release Notes:

- Fixed completions with multiple cursors leaving duplicated prefixes.
- Fixed crash when accepting a completion in a multibuffer with multiple
cursors.
- Vim: improved `single-repeat` after accepting a completion, now
pressing `.` to replay the completion will re-insert the completion text
at the cursor position.
This commit is contained in:
João Marcos 2025-04-14 15:09:28 -03:00 committed by GitHub
parent 47b663a8df
commit ff41be30dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 341 additions and 76 deletions

View file

@ -9677,9 +9677,9 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
buffer_marked_text: "before <edi|tor> after".into(),
completion_text: "editor",
expected_with_insert_mode: "before editorˇtor after".into(),
expected_with_replace_mode: "before ediˇtor after".into(),
expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
expected_with_replace_suffix_mode: "before ediˇ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: "End of word matches completion text -- cursor at end",
@ -9727,9 +9727,9 @@ async fn test_completion_mode(cx: &mut TestAppContext) {
buffer_marked_text: "[<el|element>]".into(),
completion_text: "element",
expected_with_insert_mode: "[elementˇelement]".into(),
expected_with_replace_mode: "[elˇement]".into(),
expected_with_replace_mode: "[elementˇ]".into(),
expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
expected_with_replace_suffix_mode: "[elˇement]".into(),
expected_with_replace_suffix_mode: "[elementˇ]".into(),
},
Run {
run_description: "Ends with matching suffix",
@ -9923,6 +9923,270 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext)
apply_additional_edits.await.unwrap();
}
#[gpui::test]
async fn test_completion_replacing_suffix_in_multicursors(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let initial_state = indoc! {"
1. buf.to_offˇsuffix
2. buf.to_offˇsuf
3. buf.to_offˇfix
4. buf.to_offˇ
5. into_offˇensive
6. ˇsuffix
7. let ˇ //
8. aaˇzz
9. buf.to_off«zzzzzˇ»suffix
10. buf.«ˇzzzzz»suffix
11. to_off«ˇzzzzz»
buf.to_offˇsuffix // newest cursor
"};
let completion_marked_buffer = indoc! {"
1. buf.to_offsuffix
2. buf.to_offsuf
3. buf.to_offfix
4. buf.to_off
5. into_offensive
6. suffix
7. let //
8. aazz
9. buf.to_offzzzzzsuffix
10. buf.zzzzzsuffix
11. to_offzzzzz
buf.<to_off|suffix> // newest cursor
"};
let completion_text = "to_offset";
let expected = indoc! {"
1. buf.to_offsetˇ
2. buf.to_offsetˇsuf
3. buf.to_offsetˇfix
4. buf.to_offsetˇ
5. into_offsetˇensive
6. to_offsetˇsuffix
7. let to_offsetˇ //
8. aato_offsetˇzz
9. buf.to_offsetˇ
10. buf.to_offsetˇsuffix
11. to_offsetˇ
buf.to_offsetˇ // newest cursor
"};
cx.set_state(initial_state);
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
let counter = Arc::new(AtomicUsize::new(0));
handle_completion_request_with_insert_and_replace(
&mut cx,
completion_marked_buffer,
vec![completion_text],
counter.clone(),
)
.await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
let apply_additional_edits = cx.update_editor(|editor, window, cx| {
editor
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
.unwrap()
});
cx.assert_editor_state(expected);
handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
}
// This used to crash
#[gpui::test]
async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let buffer_text = indoc! {"
fn main() {
10.satu;
//
// separate cursors so they open in different excerpts (manually reproducible)
//
10.satu20;
}
"};
let multibuffer_text_with_selections = indoc! {"
fn main() {
10.satuˇ;
//
//
10.satuˇ20;
}
"};
let expected_multibuffer = indoc! {"
fn main() {
10.saturating_sub()ˇ;
//
//
10.saturating_sub()ˇ;
}
"};
let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/a"),
json!({
"main.rs": buffer_text,
}),
)
.await;
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
resolve_provider: None,
..lsp::CompletionOptions::default()
}),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/a/main.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new(|cx| {
let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
multi_buffer.push_excerpts(
buffer.clone(),
[ExcerptRange::new(0..first_excerpt_end)],
cx,
);
multi_buffer.push_excerpts(
buffer.clone(),
[ExcerptRange::new(second_excerpt_end..buffer_text.len())],
cx,
);
multi_buffer
});
let editor = workspace
.update(cx, |_, window, cx| {
cx.new(|cx| {
Editor::new(
EditorMode::Full {
scale_ui_elements_with_buffer_font_size: false,
show_active_line_background: false,
},
multi_buffer.clone(),
Some(project.clone()),
window,
cx,
)
})
})
.unwrap();
let pane = workspace
.update(cx, |workspace, _, _| workspace.active_pane().clone())
.unwrap();
pane.update_in(cx, |pane, window, cx| {
pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
});
let fake_server = fake_servers.next().await.unwrap();
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
s.select_ranges([
Point::new(1, 11)..Point::new(1, 11),
Point::new(7, 11)..Point::new(7, 11),
])
});
assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
});
editor.update_in(cx, |editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
fake_server
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
let completion_item = lsp::CompletionItem {
label: "saturating_sub()".into(),
text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
lsp::InsertReplaceEdit {
new_text: "saturating_sub()".to_owned(),
insert: lsp::Range::new(
lsp::Position::new(7, 7),
lsp::Position::new(7, 11),
),
replace: lsp::Range::new(
lsp::Position::new(7, 7),
lsp::Position::new(7, 13),
),
},
)),
..lsp::CompletionItem::default()
};
Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
})
.next()
.await
.unwrap();
cx.condition(&editor, |editor, _| editor.context_menu_visible())
.await;
editor
.update_in(cx, |editor, window, cx| {
editor
.confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
.unwrap()
})
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_text_with_selections(editor, expected_multibuffer, cx);
})
}
#[gpui::test]
async fn test_completion(cx: &mut TestAppContext) {
init_test(cx, |_| {});