From 7f3018c3f63cfee9036dba8b017d598f075469a7 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 12 Jul 2022 13:09:01 -0700 Subject: [PATCH] add show_completions_on_input setting to disable popping the completions menu automatically --- crates/editor/src/editor.rs | 362 ++++++++++----------- crates/editor/src/link_go_to_definition.rs | 86 +++-- crates/editor/src/test.rs | 58 +++- crates/settings/src/settings.rs | 5 + crates/util/src/test/marked_text.rs | 2 +- styles/package-lock.json | 1 + 6 files changed, 278 insertions(+), 236 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e58f1fc341..b6d59ab3f8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1937,6 +1937,10 @@ impl Editor { } fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { + if !cx.global::().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()], + 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() }, - 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() - }), - ..Default::default() - }, - ..Default::default() - })) - .await; - - let text = " - 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); - }); - - 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"), - ], + cx, ) .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - let apply_additional_edits = editor.update(cx, |editor, cx| { + cx.set_state(indoc! {" + one| + two + three"}); + cx.simulate_keystroke("."); + handle_completion_request( + &mut cx, + indoc! {" + one.|<> + two + three"}, + vec!["first_completion", "second_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + 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")), - ) - .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), - ]) - }); - - 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()); - }); - - 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"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - editor.update(cx, |editor, cx| { - editor.handle_input(&Input("i".to_string()), cx); - }); - - 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"), - ], - ) - .await; - editor - .condition(&cx, |editor, _| editor.context_menu_visible()) - .await; - - let apply_additional_edits = editor.update(cx, |editor, cx| { - let apply_additional_edits = editor - .confirm_completion(&ConfirmCompletion::default(), cx) - .unwrap(); - assert_eq!( - editor.text(cx), - " + &mut cx, + Some(( + indoc! {" one.second_completion - two sixth_completion - three sixth_completion - additional edit - " - .unindent() - ); - apply_additional_edits + two + three<>"}, + "\nadditional edit", + )), + ) + .await; + apply_additional_edits.await.unwrap(); + cx.assert_editor_state(indoc! {" + one.second_completion| + two + three + additional edit"}); + + 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 cx, + indoc! {" + one.second_completion + two s + three + additional edit"}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + + cx.simulate_keystroke("i"); + + handle_completion_request( + &mut cx, + indoc! {" + one.second_completion + two si + three + additional edit"}, + vec!["fourth_completion", "fifth_completion", "sixth_completion"], + ) + .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(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(); - async fn handle_completion_request( - fake: &mut FakeLanguageServer, - path: &'static str, - position: Point, - completions: Vec<(Range, &'static str)>, + cx.update(|cx| { + cx.update_global::(|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.", 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() + }); + cx.assert_editor_state("editor.close|"); + handle_resolve_completion_request(&mut cx, None).await; + apply_additional_edits.await.unwrap(); + + // 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::(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::(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, &'static str)>, + async fn handle_resolve_completion_request<'a>( + cx: &mut EditorLspTestContext<'a>, + edit: Option<(&'static str, &'static str)>, ) { - fake.handle_request::(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::(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() }) } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 2e59a72402..f034df64b6 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -342,17 +342,16 @@ mod tests { test();"}); let mut requests = - cx.lsp - .handle_request::(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::(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::(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::(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::(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::(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::(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::(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); }); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 4e10f516c1..dd05a14bd6 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -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 { + self.editor.condition(self.cx, predicate) + } + pub fn editor(&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, + 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 snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); - 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.to_lsp_range(offset_range) + } + + pub fn to_lsp_range(&mut self, range: Range) -> 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); + 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(&mut self, update: F) -> T where F: FnOnce(&mut Workspace, &mut ViewContext) -> T, { self.workspace.update(self.cx.cx, update) } + + pub fn handle_request( + &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>, + { + let url = self.editor_lsp_url.clone(); + self.lsp.handle_request::(move |params, cx| { + let url = url.clone(); + handler(url, params, cx) + }) + } } impl<'a> Deref for EditorLspTestContext<'a> { diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 6073b1ef15..807587ac86 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -25,6 +25,7 @@ pub struct Settings { pub buffer_font_size: f32, pub default_buffer_font_size: f32, pub hover_popover_enabled: bool, + pub show_completions_on_input: bool, pub vim_mode: bool, pub autosave: Autosave, pub editor_defaults: EditorSettings, @@ -83,6 +84,8 @@ pub struct SettingsFileContent { #[serde(default)] pub hover_popover_enabled: Option, #[serde(default)] + pub show_completions_on_input: Option, + #[serde(default)] pub vim_mode: Option, #[serde(default)] pub autosave: Option, @@ -118,6 +121,7 @@ impl Settings { buffer_font_size: defaults.buffer_font_size.unwrap(), default_buffer_font_size: defaults.buffer_font_size.unwrap(), hover_popover_enabled: defaults.hover_popover_enabled.unwrap(), + show_completions_on_input: defaults.show_completions_on_input.unwrap(), projects_online_by_default: defaults.projects_online_by_default.unwrap(), vim_mode: defaults.vim_mode.unwrap(), autosave: defaults.autosave.unwrap(), @@ -219,6 +223,7 @@ impl Settings { buffer_font_size: 14., default_buffer_font_size: 14., hover_popover_enabled: true, + show_completions_on_input: true, vim_mode: false, autosave: Autosave::Off, editor_defaults: EditorSettings { diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs index 4529c8c803..2a5969c265 100644 --- a/crates/util/src/test/marked_text.rs +++ b/crates/util/src/test/marked_text.rs @@ -24,7 +24,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec) { (unmarked_text, markers.remove(&'|').unwrap_or_default()) } -#[derive(Eq, PartialEq, Hash)] +#[derive(Clone, Eq, PartialEq, Hash)] pub enum TextRangeMarker { Empty(char), Range(char, char), diff --git a/styles/package-lock.json b/styles/package-lock.json index 2eb6d3a1bf..49304dc2fa 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "styles", "version": "1.0.0", "license": "ISC", "dependencies": {