Merge pull request #1330 from zed-industries/completions-setting

Keith Simmons created

Completions Menu Setting

Change summary

assets/settings/default.json               |  16 
crates/editor/src/editor.rs                | 324 +++++++++++------------
crates/editor/src/link_go_to_definition.rs |  86 +++---
crates/editor/src/test.rs                  |  56 +++
crates/settings/src/settings.rs            |   9 
crates/util/src/test/marked_text.rs        |   2 
styles/package-lock.json                   |   1 
7 files changed, 265 insertions(+), 229 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1,29 +1,25 @@
 {
     // The name of the Zed theme to use for the UI
     "theme": "cave-dark",
-
     // The name of a font to use for rendering text in the editor
     "buffer_font_family": "Zed Mono",
-
     // The default font size for text in the editor
     "buffer_font_size": 15,
-
     // Whether to enable vim modes and key bindings
     "vim_mode": false,
-
     // Whether to show the informational hover box when moving the mouse
     // over symbols in the editor.
     "hover_popover_enabled": true,
-
+    // Whether to pop the completions menu while typing in an editor without
+    // explicitly requesting it.
+    "show_completions_on_input": true,
     // Whether new projects should start out 'online'. Online projects
     // appear in the contacts panel under your name, so that your contacts
     // can see which projects you are working on. Regardless of this
     // setting, projects keep their last online status when you reopen them.
     "projects_online_by_default": true,
-
     // Whether to use language servers to provide code intelligence.
     "enable_language_server": true,
-
     // When to automatically save edited buffers. This setting can
     // take four values.
     //
@@ -36,7 +32,6 @@
     // 4. Save when idle for a certain amount of time:
     //     "autosave": { "after_delay": {"milliseconds": 500} },
     "autosave": "off",
-
     // How to auto-format modified buffers when saving them. This
     // setting can take three values:
     //
@@ -52,7 +47,6 @@
     //       }
     //     },
     "format_on_save": "language_server",
-
     // How to soft-wrap long lines of text. This setting can take
     // three values:
     //
@@ -63,18 +57,14 @@
     // 2. Soft wrap lines at the preferred line length
     //      "soft_wrap": "preferred_line_length",
     "soft_wrap": "none",
-
     // The column at which to soft-wrap lines, for buffers where soft-wrap
     // is enabled.
     "preferred_line_length": 80,
-
     // Whether to indent lines using tab characters, as opposed to multiple
     // spaces.
     "hard_tabs": false,
-
     // How many columns a tab should occupy.
     "tab_size": 4,
-
     // Different settings for specific languages.
     "languages": {
         "Plain Text": {

crates/editor/src/editor.rs 🔗

@@ -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);
         });

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<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> {

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<bool>,
     #[serde(default)]
+    pub show_completions_on_input: Option<bool>,
+    #[serde(default)]
     pub vim_mode: Option<bool>,
     #[serde(default)]
     pub autosave: Option<Autosave>,
@@ -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(),
@@ -160,6 +164,10 @@ impl Settings {
         merge(&mut self.buffer_font_size, data.buffer_font_size);
         merge(&mut self.default_buffer_font_size, data.buffer_font_size);
         merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
+        merge(
+            &mut self.show_completions_on_input,
+            data.show_completions_on_input,
+        );
         merge(&mut self.vim_mode, data.vim_mode);
         merge(&mut self.autosave, data.autosave);
 
@@ -219,6 +227,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 {

crates/util/src/test/marked_text.rs 🔗

@@ -24,7 +24,7 @@ pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
     (unmarked_text, markers.remove(&'|').unwrap_or_default())
 }
 
-#[derive(Eq, PartialEq, Hash)]
+#[derive(Clone, Eq, PartialEq, Hash)]
 pub enum TextRangeMarker {
     Empty(char),
     Range(char, char),

styles/package-lock.json 🔗

@@ -5,6 +5,7 @@
     "requires": true,
     "packages": {
         "": {
+            "name": "styles",
             "version": "1.0.0",
             "license": "ISC",
             "dependencies": {