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:
parent
47b663a8df
commit
ff41be30dc
4 changed files with 341 additions and 76 deletions
|
@ -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, |_| {});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue