@@ -1937,6 +1937,10 @@ impl Editor {
}
fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+ if !cx.global::<Settings>().show_completions_on_input {
+ return;
+ }
+
let selection = self.selections.newest_anchor();
if self
.buffer
@@ -6225,7 +6229,8 @@ pub fn styled_runs_for_code_label<'a>(
#[cfg(test)]
mod tests {
use crate::test::{
- assert_text_with_selections, build_editor, select_ranges, EditorTestContext,
+ assert_text_with_selections, build_editor, select_ranges, EditorLspTestContext,
+ EditorTestContext,
};
use super::*;
@@ -6236,7 +6241,6 @@ mod tests {
};
use indoc::indoc;
use language::{FakeLspAdapter, LanguageConfig};
- use lsp::FakeLanguageServer;
use project::FakeFs;
use settings::EditorSettings;
use std::{cell::RefCell, rc::Rc, time::Instant};
@@ -6244,7 +6248,9 @@ mod tests {
use unindent::Unindent;
use util::{
assert_set_eq,
- test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
+ test::{
+ marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker,
+ },
};
use workspace::{FollowableItem, ItemHandle, NavigationEntry, Pane};
@@ -9524,199 +9530,182 @@ mod tests {
#[gpui::test]
async fn test_completion(cx: &mut gpui::TestAppContext) {
- let mut language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- Some(tree_sitter_rust::language()),
- );
- let mut fake_servers = language
- .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
- capabilities: lsp::ServerCapabilities {
- completion_provider: Some(lsp::CompletionOptions {
- trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
- ..Default::default()
- }),
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
..Default::default()
- },
+ }),
..Default::default()
- }))
- .await;
+ },
+ cx,
+ )
+ .await;
- let text = "
- one
+ cx.set_state(indoc! {"
+ one|
two
- three
- "
- .unindent();
-
- let fs = FakeFs::new(cx.background().clone());
- fs.insert_file("/file.rs", text).await;
-
- let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
- project.update(cx, |project, _| project.languages().add(Arc::new(language)));
- let buffer = project
- .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
- .await
- .unwrap();
- let mut fake_server = fake_servers.next().await.unwrap();
-
- let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
- let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
-
- editor.update(cx, |editor, cx| {
- editor.project = Some(project);
- editor.change_selections(None, cx, |s| {
- s.select_ranges([Point::new(0, 3)..Point::new(0, 3)])
- });
- editor.handle_input(&Input(".".to_string()), cx);
- });
-
+ three"});
+ cx.simulate_keystroke(".");
handle_completion_request(
- &mut fake_server,
- "/file.rs",
- Point::new(0, 4),
- vec![
- (Point::new(0, 4)..Point::new(0, 4), "first_completion"),
- (Point::new(0, 4)..Point::new(0, 4), "second_completion"),
- ],
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three"},
+ vec!["first_completion", "second_completion"],
)
.await;
- editor
- .condition(&cx, |editor, _| editor.context_menu_visible())
+ cx.condition(|editor, _| editor.context_menu_visible())
.await;
-
- let apply_additional_edits = editor.update(cx, |editor, cx| {
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
editor.move_down(&MoveDown, cx);
- let apply_additional_edits = editor
+ editor
.confirm_completion(&ConfirmCompletion::default(), cx)
- .unwrap();
- assert_eq!(
- editor.text(cx),
- "
- one.second_completion
- two
- three
- "
- .unindent()
- );
- apply_additional_edits
+ .unwrap()
});
+ cx.assert_editor_state(indoc! {"
+ one.second_completion|
+ two
+ three"});
handle_resolve_completion_request(
- &mut fake_server,
- Some((Point::new(2, 5)..Point::new(2, 5), "\nadditional edit")),
+ &mut cx,
+ Some((
+ indoc! {"
+ one.second_completion
+ two
+ three<>"},
+ "\nadditional edit",
+ )),
)
.await;
apply_additional_edits.await.unwrap();
- assert_eq!(
- editor.read_with(cx, |editor, cx| editor.text(cx)),
- "
- one.second_completion
- two
- three
- additional edit
- "
- .unindent()
- );
-
- editor.update(cx, |editor, cx| {
- editor.change_selections(None, cx, |s| {
- s.select_ranges([
- Point::new(1, 3)..Point::new(1, 3),
- Point::new(2, 5)..Point::new(2, 5),
- ])
- });
+ cx.assert_editor_state(indoc! {"
+ one.second_completion|
+ two
+ three
+ additional edit"});
- editor.handle_input(&Input(" ".to_string()), cx);
- assert!(editor.context_menu.is_none());
- editor.handle_input(&Input("s".to_string()), cx);
- assert!(editor.context_menu.is_none());
- });
+ cx.set_state(indoc! {"
+ one.second_completion
+ two|
+ three|
+ additional edit"});
+ cx.simulate_keystroke(" ");
+ assert!(cx.editor(|e, _| e.context_menu.is_none()));
+ cx.simulate_keystroke("s");
+ assert!(cx.editor(|e, _| e.context_menu.is_none()));
+ cx.assert_editor_state(indoc! {"
+ one.second_completion
+ two s|
+ three s|
+ additional edit"});
handle_completion_request(
- &mut fake_server,
- "/file.rs",
- Point::new(2, 7),
- vec![
- (Point::new(2, 6)..Point::new(2, 7), "fourth_completion"),
- (Point::new(2, 6)..Point::new(2, 7), "fifth_completion"),
- (Point::new(2, 6)..Point::new(2, 7), "sixth_completion"),
- ],
+ &mut cx,
+ indoc! {"
+ one.second_completion
+ two s
+ three <s|>
+ additional edit"},
+ vec!["fourth_completion", "fifth_completion", "sixth_completion"],
)
.await;
- editor
- .condition(&cx, |editor, _| editor.context_menu_visible())
+ cx.condition(|editor, _| editor.context_menu_visible())
.await;
- editor.update(cx, |editor, cx| {
- editor.handle_input(&Input("i".to_string()), cx);
- });
+ cx.simulate_keystroke("i");
handle_completion_request(
- &mut fake_server,
- "/file.rs",
- Point::new(2, 8),
- vec![
- (Point::new(2, 6)..Point::new(2, 8), "fourth_completion"),
- (Point::new(2, 6)..Point::new(2, 8), "fifth_completion"),
- (Point::new(2, 6)..Point::new(2, 8), "sixth_completion"),
- ],
+ &mut cx,
+ indoc! {"
+ one.second_completion
+ two si
+ three <si|>
+ additional edit"},
+ vec!["fourth_completion", "fifth_completion", "sixth_completion"],
)
.await;
- editor
- .condition(&cx, |editor, _| editor.context_menu_visible())
+ cx.condition(|editor, _| editor.context_menu_visible())
.await;
- let apply_additional_edits = editor.update(cx, |editor, cx| {
- let apply_additional_edits = editor
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
.confirm_completion(&ConfirmCompletion::default(), cx)
- .unwrap();
- assert_eq!(
- editor.text(cx),
- "
- one.second_completion
- two sixth_completion
- three sixth_completion
- additional edit
- "
- .unindent()
- );
- apply_additional_edits
+ .unwrap()
+ });
+ cx.assert_editor_state(indoc! {"
+ one.second_completion
+ two sixth_completion|
+ three sixth_completion|
+ additional edit"});
+
+ handle_resolve_completion_request(&mut cx, None).await;
+ apply_additional_edits.await.unwrap();
+
+ cx.update(|cx| {
+ cx.update_global::<Settings, _, _>(|settings, _| {
+ settings.show_completions_on_input = false;
+ })
+ });
+ cx.set_state("editor|");
+ cx.simulate_keystroke(".");
+ assert!(cx.editor(|e, _| e.context_menu.is_none()));
+ cx.simulate_keystrokes(["c", "l", "o"]);
+ cx.assert_editor_state("editor.clo|");
+ assert!(cx.editor(|e, _| e.context_menu.is_none()));
+ cx.update_editor(|editor, cx| {
+ editor.show_completions(&ShowCompletions, cx);
+ });
+ handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
});
- handle_resolve_completion_request(&mut fake_server, None).await;
+ cx.assert_editor_state("editor.close|");
+ handle_resolve_completion_request(&mut cx, None).await;
apply_additional_edits.await.unwrap();
- async fn handle_completion_request(
- fake: &mut FakeLanguageServer,
- path: &'static str,
- position: Point,
- completions: Vec<(Range<Point>, &'static str)>,
+ // Handle completion request passing a marked string specifying where the completion
+ // should be triggered from using '|' character, what range should be replaced, and what completions
+ // should be returned using '<' and '>' to delimit the range
+ async fn handle_completion_request<'a>(
+ cx: &mut EditorLspTestContext<'a>,
+ marked_string: &str,
+ completions: Vec<&'static str>,
) {
- fake.handle_request::<lsp::request::Completion, _, _>(move |params, _| {
+ let complete_from_marker: TextRangeMarker = '|'.into();
+ let replace_range_marker: TextRangeMarker = ('<', '>').into();
+ let (_, mut marked_ranges) = marked_text_ranges_by(
+ marked_string,
+ vec![complete_from_marker.clone(), replace_range_marker.clone()],
+ );
+
+ let complete_from_position =
+ cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
+ let replace_range =
+ cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+ cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
let completions = completions.clone();
async move {
- assert_eq!(
- params.text_document_position.text_document.uri,
- lsp::Url::from_file_path(path).unwrap()
- );
+ assert_eq!(params.text_document_position.text_document.uri, url.clone());
assert_eq!(
params.text_document_position.position,
- lsp::Position::new(position.row, position.column)
+ complete_from_position
);
Ok(Some(lsp::CompletionResponse::Array(
completions
.iter()
- .map(|(range, new_text)| lsp::CompletionItem {
- label: new_text.to_string(),
+ .map(|completion_text| lsp::CompletionItem {
+ label: completion_text.to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
- range: lsp::Range::new(
- lsp::Position::new(range.start.row, range.start.column),
- lsp::Position::new(range.start.row, range.start.column),
- ),
- new_text: new_text.to_string(),
+ range: replace_range.clone(),
+ new_text: completion_text.to_string(),
})),
..Default::default()
})
@@ -9728,23 +9717,26 @@ mod tests {
.await;
}
- async fn handle_resolve_completion_request(
- fake: &mut FakeLanguageServer,
- edit: Option<(Range<Point>, &'static str)>,
+ async fn handle_resolve_completion_request<'a>(
+ cx: &mut EditorLspTestContext<'a>,
+ edit: Option<(&'static str, &'static str)>,
) {
- fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
+ let edit = edit.map(|(marked_string, new_text)| {
+ let replace_range_marker: TextRangeMarker = ('<', '>').into();
+ let (_, mut marked_ranges) =
+ marked_text_ranges_by(marked_string, vec![replace_range_marker.clone()]);
+
+ let replace_range = cx
+ .to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+ vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
+ });
+
+ cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
let edit = edit.clone();
async move {
Ok(lsp::CompletionItem {
- additional_text_edits: edit.map(|(range, new_text)| {
- vec![lsp::TextEdit::new(
- lsp::Range::new(
- lsp::Position::new(range.start.row, range.start.column),
- lsp::Position::new(range.end.row, range.end.column),
- ),
- new_text.to_string(),
- )]
- }),
+ additional_text_edits: edit,
..Default::default()
})
}
@@ -342,17 +342,16 @@ mod tests {
test();"});
let mut requests =
- cx.lsp
- .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
- Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
- lsp::LocationLink {
- origin_selection_range: Some(symbol_range),
- target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
- target_range,
- target_selection_range: target_range,
- },
- ])))
- });
+ cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
@@ -387,18 +386,17 @@ mod tests {
// Response without source range still highlights word
cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None);
let mut requests =
- cx.lsp
- .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
- Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
- lsp::LocationLink {
- // No origin range
- origin_selection_range: None,
- target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
- target_range,
- target_selection_range: target_range,
- },
- ])))
- });
+ cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ // No origin range
+ origin_selection_range: None,
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
cx.update_editor(|editor, cx| {
update_go_to_definition_link(
editor,
@@ -495,17 +493,16 @@ mod tests {
test();"});
let mut requests =
- cx.lsp
- .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
- Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
- lsp::LocationLink {
- origin_selection_range: Some(symbol_range),
- target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
- target_range,
- target_selection_range: target_range,
- },
- ])))
- });
+ cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
cx.update_editor(|editor, cx| {
cmd_changed(editor, &CmdChanged { cmd_down: true }, cx);
});
@@ -584,17 +581,16 @@ mod tests {
test();"});
let mut requests =
- cx.lsp
- .handle_request::<lsp::request::GotoDefinition, _, _>(move |_, _| async move {
- Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
- lsp::LocationLink {
- origin_selection_range: None,
- target_uri: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
- target_range,
- target_selection_range: target_range,
- },
- ])))
- });
+ cx.handle_request::<lsp::request::GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
cx.update_workspace(|workspace, cx| {
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
});
@@ -4,12 +4,14 @@ use std::{
sync::Arc,
};
-use futures::StreamExt;
+use anyhow::Result;
+use futures::{Future, StreamExt};
use indoc::indoc;
use collections::BTreeMap;
use gpui::{json, keymap::Keystroke, AppContext, ModelHandle, ViewContext, ViewHandle};
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, Selection};
+use lsp::request;
use project::Project;
use settings::Settings;
use util::{
@@ -110,6 +112,13 @@ impl<'a> EditorTestContext<'a> {
}
}
+ pub fn condition(
+ &self,
+ predicate: impl FnMut(&Editor, &AppContext) -> bool,
+ ) -> impl Future<Output = ()> {
+ self.editor.condition(self.cx, predicate)
+ }
+
pub fn editor<F, T>(&mut self, read: F) -> T
where
F: FnOnce(&Editor, &AppContext) -> T,
@@ -424,6 +433,7 @@ pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
+ pub editor_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
@@ -497,6 +507,7 @@ impl<'a> EditorLspTestContext<'a> {
},
lsp,
workspace,
+ editor_lsp_url: lsp::Url::from_file_path("/root/dir/file.rs").unwrap(),
}
}
@@ -520,11 +531,15 @@ impl<'a> EditorLspTestContext<'a> {
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let (unmarked, mut ranges) = marked_text_ranges_by(marked_text, vec![('[', ']').into()]);
assert_eq!(unmarked, self.cx.buffer_text());
+ let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
+ self.to_lsp_range(offset_range)
+ }
+
+ pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+ let start_point = range.start.to_point(&snapshot.buffer_snapshot);
+ let end_point = range.end.to_point(&snapshot.buffer_snapshot);
- let offset_range = ranges.remove(&('[', ']').into()).unwrap()[0].clone();
- let start_point = offset_range.start.to_point(&snapshot.buffer_snapshot);
- let end_point = offset_range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
@@ -546,12 +561,45 @@ impl<'a> EditorLspTestContext<'a> {
})
}
+ pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
+ let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+ let point = offset.to_point(&snapshot.buffer_snapshot);
+
+ self.editor(|editor, cx| {
+ let buffer = editor.buffer().read(cx);
+ point_to_lsp(
+ buffer
+ .point_to_buffer_offset(point, cx)
+ .unwrap()
+ .1
+ .to_point_utf16(&buffer.read(cx)),
+ )
+ })
+ }
+
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
+
+ pub fn handle_request<T, F, Fut>(
+ &self,
+ mut handler: F,
+ ) -> futures::channel::mpsc::UnboundedReceiver<()>
+ where
+ T: 'static + request::Request,
+ T::Params: 'static + Send,
+ F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
+ Fut: 'static + Send + Future<Output = Result<T::Result>>,
+ {
+ let url = self.editor_lsp_url.clone();
+ self.lsp.handle_request::<T, _, _>(move |params, cx| {
+ let url = url.clone();
+ handler(url, params, cx)
+ })
+ }
}
impl<'a> Deref for EditorLspTestContext<'a> {