Address some TODOs in editor2 crate (#3445)

Max Brunsfeld created

* Fix crash when jumping to definition
* Enabling resolution of completions
* Make links in interactive text clickable
* Enable code paths that use `select_anchors`

Change summary

crates/editor2/src/editor.rs                | 299 ++++-----
crates/editor2/src/editor_tests.rs          | 682 +++++++++++-----------
crates/editor2/src/link_go_to_definition.rs |  76 +-
crates/editor2/src/selections_collection.rs |  51 
crates/gpui2/src/elements/text.rs           |  22 
crates/project2/src/lsp_command.rs          |   3 
6 files changed, 576 insertions(+), 557 deletions(-)

Detailed changes

crates/editor2/src/editor.rs 🔗

@@ -73,7 +73,7 @@ use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock};
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::prelude::*;
-use rpc::proto::*;
+use rpc::proto::{self, *};
 use scroll::{
     autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
 };
@@ -155,7 +155,6 @@ pub fn render_parsed_markdown(
     );
     let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights);
 
-    // todo!("add the ability to change cursor style for link ranges")
     let mut links = Vec::new();
     let mut link_ranges = Vec::new();
     for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
@@ -972,95 +971,94 @@ impl CompletionsMenu {
 
     fn pre_resolve_completion_documentation(
         &self,
-        _editor: &Editor,
-        _cx: &mut ViewContext<Editor>,
+        editor: &Editor,
+        cx: &mut ViewContext<Editor>,
     ) -> Option<Task<()>> {
-        // todo!("implementation below ");
-        None
-    }
-    // {
-    //     let settings = EditorSettings::get_global(cx);
-    //     if !settings.show_completion_documentation {
-    //         return None;
-    //     }
+        let settings = EditorSettings::get_global(cx);
+        if !settings.show_completion_documentation {
+            return None;
+        }
 
-    //     let Some(project) = editor.project.clone() else {
-    //         return None;
-    //     };
+        let Some(project) = editor.project.clone() else {
+            return None;
+        };
 
-    //     let client = project.read(cx).client();
-    //     let language_registry = project.read(cx).languages().clone();
+        let client = project.read(cx).client();
+        let language_registry = project.read(cx).languages().clone();
 
-    //     let is_remote = project.read(cx).is_remote();
-    //     let project_id = project.read(cx).remote_id();
+        let is_remote = project.read(cx).is_remote();
+        let project_id = project.read(cx).remote_id();
 
-    //     let completions = self.completions.clone();
-    //     let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
+        let completions = self.completions.clone();
+        let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
 
-    //     Some(cx.spawn(move |this, mut cx| async move {
-    //         if is_remote {
-    //             let Some(project_id) = project_id else {
-    //                 log::error!("Remote project without remote_id");
-    //                 return;
-    //             };
+        Some(cx.spawn(move |this, mut cx| async move {
+            if is_remote {
+                let Some(project_id) = project_id else {
+                    log::error!("Remote project without remote_id");
+                    return;
+                };
 
-    //             for completion_index in completion_indices {
-    //                 let completions_guard = completions.read();
-    //                 let completion = &completions_guard[completion_index];
-    //                 if completion.documentation.is_some() {
-    //                     continue;
-    //                 }
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
 
-    //                 let server_id = completion.server_id;
-    //                 let completion = completion.lsp_completion.clone();
-    //                 drop(completions_guard);
-
-    //                 Self::resolve_completion_documentation_remote(
-    //                     project_id,
-    //                     server_id,
-    //                     completions.clone(),
-    //                     completion_index,
-    //                     completion,
-    //                     client.clone(),
-    //                     language_registry.clone(),
-    //                 )
-    //                 .await;
-
-    //                 _ = this.update(&mut cx, |_, cx| cx.notify());
-    //             }
-    //         } else {
-    //             for completion_index in completion_indices {
-    //                 let completions_guard = completions.read();
-    //                 let completion = &completions_guard[completion_index];
-    //                 if completion.documentation.is_some() {
-    //                     continue;
-    //                 }
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    Self::resolve_completion_documentation_remote(
+                        project_id,
+                        server_id,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        client.clone(),
+                        language_registry.clone(),
+                    )
+                    .await;
 
-    //                 let server_id = completion.server_id;
-    //                 let completion = completion.lsp_completion.clone();
-    //                 drop(completions_guard);
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            } else {
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
 
-    //                 let server = project.read_with(&mut cx, |project, _| {
-    //                     project.language_server_for_id(server_id)
-    //                 });
-    //                 let Some(server) = server else {
-    //                     return;
-    //                 };
-
-    //                 Self::resolve_completion_documentation_local(
-    //                     server,
-    //                     completions.clone(),
-    //                     completion_index,
-    //                     completion,
-    //                     language_registry.clone(),
-    //                 )
-    //                 .await;
-
-    //                 _ = this.update(&mut cx, |_, cx| cx.notify());
-    //             }
-    //         }
-    //     }))
-    // }
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    let server = project
+                        .read_with(&mut cx, |project, _| {
+                            project.language_server_for_id(server_id)
+                        })
+                        .ok()
+                        .flatten();
+                    let Some(server) = server else {
+                        return;
+                    };
+
+                    Self::resolve_completion_documentation_local(
+                        server,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        language_registry.clone(),
+                    )
+                    .await;
+
+                    _ = this.update(&mut cx, |_, cx| cx.notify());
+                }
+            }
+        }))
+    }
 
     fn attempt_resolve_selected_completion_documentation(
         &mut self,
@@ -1081,10 +1079,9 @@ impl CompletionsMenu {
         let completions = self.completions.clone();
         let completions_guard = completions.read();
         let completion = &completions_guard[completion_index];
-        // todo!()
-        // if completion.documentation.is_some() {
-        //     return;
-        // }
+        if completion.documentation.is_some() {
+            return;
+        }
 
         let server_id = completion.server_id;
         let completion = completion.lsp_completion.clone();
@@ -1143,41 +1140,40 @@ impl CompletionsMenu {
         client: Arc<Client>,
         language_registry: Arc<LanguageRegistry>,
     ) {
-        // todo!()
-        // let request = proto::ResolveCompletionDocumentation {
-        //     project_id,
-        //     language_server_id: server_id.0 as u64,
-        //     lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
-        // };
-
-        // let Some(response) = client
-        //     .request(request)
-        //     .await
-        //     .context("completion documentation resolve proto request")
-        //     .log_err()
-        // else {
-        //     return;
-        // };
-
-        // if response.text.is_empty() {
-        //     let mut completions = completions.write();
-        //     let completion = &mut completions[completion_index];
-        //     completion.documentation = Some(Documentation::Undocumented);
-        // }
-
-        // let documentation = if response.is_markdown {
-        //     Documentation::MultiLineMarkdown(
-        //         markdown::parse_markdown(&response.text, &language_registry, None).await,
-        //     )
-        // } else if response.text.lines().count() <= 1 {
-        //     Documentation::SingleLine(response.text)
-        // } else {
-        //     Documentation::MultiLinePlainText(response.text)
-        // };
-
-        // let mut completions = completions.write();
-        // let completion = &mut completions[completion_index];
-        // completion.documentation = Some(documentation);
+        let request = proto::ResolveCompletionDocumentation {
+            project_id,
+            language_server_id: server_id.0 as u64,
+            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+        };
+
+        let Some(response) = client
+            .request(request)
+            .await
+            .context("completion documentation resolve proto request")
+            .log_err()
+        else {
+            return;
+        };
+
+        if response.text.is_empty() {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+
+        let documentation = if response.is_markdown {
+            Documentation::MultiLineMarkdown(
+                markdown::parse_markdown(&response.text, &language_registry, None).await,
+            )
+        } else if response.text.lines().count() <= 1 {
+            Documentation::SingleLine(response.text)
+        } else {
+            Documentation::MultiLinePlainText(response.text)
+        };
+
+        let mut completions = completions.write();
+        let completion = &mut completions[completion_index];
+        completion.documentation = Some(documentation);
     }
 
     async fn resolve_completion_documentation_local(
@@ -1187,38 +1183,37 @@ impl CompletionsMenu {
         completion: lsp::CompletionItem,
         language_registry: Arc<LanguageRegistry>,
     ) {
-        // todo!()
-        // let can_resolve = server
-        //     .capabilities()
-        //     .completion_provider
-        //     .as_ref()
-        //     .and_then(|options| options.resolve_provider)
-        //     .unwrap_or(false);
-        // if !can_resolve {
-        //     return;
-        // }
-
-        // let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
-        // let Some(completion_item) = request.await.log_err() else {
-        //     return;
-        // };
-
-        // if let Some(lsp_documentation) = completion_item.documentation {
-        //     let documentation = language::prepare_completion_documentation(
-        //         &lsp_documentation,
-        //         &language_registry,
-        //         None, // TODO: Try to reasonably work out which language the completion is for
-        //     )
-        //     .await;
-
-        //     let mut completions = completions.write();
-        //     let completion = &mut completions[completion_index];
-        //     completion.documentation = Some(documentation);
-        // } else {
-        //     let mut completions = completions.write();
-        //     let completion = &mut completions[completion_index];
-        //     completion.documentation = Some(Documentation::Undocumented);
-        // }
+        let can_resolve = server
+            .capabilities()
+            .completion_provider
+            .as_ref()
+            .and_then(|options| options.resolve_provider)
+            .unwrap_or(false);
+        if !can_resolve {
+            return;
+        }
+
+        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+        let Some(completion_item) = request.await.log_err() else {
+            return;
+        };
+
+        if let Some(lsp_documentation) = completion_item.documentation {
+            let documentation = language::prepare_completion_documentation(
+                &lsp_documentation,
+                &language_registry,
+                None, // TODO: Try to reasonably work out which language the completion is for
+            )
+            .await;
+
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(documentation);
+        } else {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
     }
 
     fn visible(&self) -> bool {

crates/editor2/src/editor_tests.rs 🔗

@@ -5427,178 +5427,177 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
     );
 }
 
-//todo!(completion)
-// #[gpui::test]
-// async fn test_completion(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_completion(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-//                 resolve_provider: Some(true),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
 
-//     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.context_menu_next(&Default::default(), cx);
-//         editor
-//             .confirm_completion(&ConfirmCompletion::default(), cx)
-//             .unwrap()
-//     });
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completionˇ
-//         two
-//         three
-//     "});
+    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.context_menu_next(&Default::default(), cx);
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .unwrap()
+    });
+    cx.assert_editor_state(indoc! {"
+        one.second_completionˇ
+        two
+        three
+    "});
 
-//     handle_resolve_completion_request(
-//         &mut cx,
-//         Some(vec![
-//             (
-//                 //This overlaps with the primary completion edit which is
-//                 //misbehavior from the LSP spec, test that we filter it out
-//                 indoc! {"
-//                     one.second_ˇcompletion
-//                     two
-//                     threeˇ
-//                 "},
-//                 "overlapping additional edit",
-//             ),
-//             (
-//                 indoc! {"
-//                     one.second_completion
-//                     two
-//                     threeˇ
-//                 "},
-//                 "\nadditional edit",
-//             ),
-//         ]),
-//     )
-//     .await;
-//     apply_additional_edits.await.unwrap();
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completionˇ
-//         two
-//         three
-//         additional edit
-//     "});
+    handle_resolve_completion_request(
+        &mut cx,
+        Some(vec![
+            (
+                //This overlaps with the primary completion edit which is
+                //misbehavior from the LSP spec, test that we filter it out
+                indoc! {"
+                    one.second_ˇcompletion
+                    two
+                    threeˇ
+                "},
+                "overlapping additional edit",
+            ),
+            (
+                indoc! {"
+                    one.second_completion
+                    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.read().is_none()));
-//     cx.simulate_keystroke("s");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.set_state(indoc! {"
+        one.second_completion
+        twoˇ
+        threeˇ
+        additional edit
+    "});
+    cx.simulate_keystroke(" ");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.simulate_keystroke("s");
+    assert!(cx.editor(|e, _| e.context_menu.read().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 <s|>
-//             additional edit
-//         "},
-//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
+    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 <s|>
+            additional edit
+        "},
+        vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+    )
+    .await;
+    cx.condition(|editor, _| editor.context_menu_visible())
+        .await;
 
-//     cx.simulate_keystroke("i");
+    cx.simulate_keystroke("i");
 
-//     handle_completion_request(
-//         &mut cx,
-//         indoc! {"
-//             one.second_completion
-//             two si
-//             three <si|>
-//             additional edit
-//         "},
-//         vec!["fourth_completion", "fifth_completion", "sixth_completion"],
-//     )
-//     .await;
-//     cx.condition(|editor, _| editor.context_menu_visible())
-//         .await;
+    handle_completion_request(
+        &mut cx,
+        indoc! {"
+            one.second_completion
+            two si
+            three <si|>
+            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()
-//     });
-//     cx.assert_editor_state(indoc! {"
-//         one.second_completion
-//         two sixth_completionˇ
-//         three sixth_completionˇ
-//         additional edit
-//     "});
+    let apply_additional_edits = cx.update_editor(|editor, cx| {
+        editor
+            .confirm_completion(&ConfirmCompletion::default(), cx)
+            .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();
+    handle_resolve_completion_request(&mut cx, None).await;
+    apply_additional_edits.await.unwrap();
 
-//     cx.update(|cx| {
-//         cx.update_global::<SettingsStore, _, _>(|settings, cx| {
-//             settings.update_user_settings::<EditorSettings>(cx, |settings| {
-//                 settings.show_completions_on_input = Some(false);
-//             });
-//         })
-//     });
-//     cx.set_state("editorˇ");
-//     cx.simulate_keystroke(".");
-//     assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
-//     cx.simulate_keystroke("c");
-//     cx.simulate_keystroke("l");
-//     cx.simulate_keystroke("o");
-//     cx.assert_editor_state("editor.cloˇ");
-//     assert!(cx.editor(|e, _| e.context_menu.read().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()
-//     });
-//     cx.assert_editor_state("editor.closeˇ");
-//     handle_resolve_completion_request(&mut cx, None).await;
-//     apply_additional_edits.await.unwrap();
-// }
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _>(|settings, cx| {
+            settings.update_user_settings::<EditorSettings>(cx, |settings| {
+                settings.show_completions_on_input = Some(false);
+            });
+        })
+    });
+    cx.set_state("editorˇ");
+    cx.simulate_keystroke(".");
+    assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+    cx.simulate_keystroke("c");
+    cx.simulate_keystroke("l");
+    cx.simulate_keystroke("o");
+    cx.assert_editor_state("editor.cloˇ");
+    assert!(cx.editor(|e, _| e.context_menu.read().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()
+    });
+    cx.assert_editor_state("editor.closeˇ");
+    handle_resolve_completion_request(&mut cx, None).await;
+    apply_additional_edits.await.unwrap();
+}
 
 #[gpui::test]
 async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
@@ -7803,197 +7802,196 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
     );
 }
 
-//todo!(completions)
-// #[gpui::test]
-// async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+#[gpui::test]
+async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     let mut cx = EditorLspTestContext::new_rust(
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![".".to_string()]),
-//                 resolve_provider: Some(true),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+    let mut cx = EditorLspTestContext::new_rust(
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![".".to_string()]),
+                resolve_provider: Some(true),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
 
-//     cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
-//     cx.simulate_keystroke(".");
-//     let completion_item = lsp::CompletionItem {
-//         label: "some".into(),
-//         kind: Some(lsp::CompletionItemKind::SNIPPET),
-//         detail: Some("Wrap the expression in an `Option::Some`".to_string()),
-//         documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
-//             kind: lsp::MarkupKind::Markdown,
-//             value: "```rust\nSome(2)\n```".to_string(),
-//         })),
-//         deprecated: Some(false),
-//         sort_text: Some("fffffff2".to_string()),
-//         filter_text: Some("some".to_string()),
-//         insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
-//         text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-//             range: lsp::Range {
-//                 start: lsp::Position {
-//                     line: 0,
-//                     character: 22,
-//                 },
-//                 end: lsp::Position {
-//                     line: 0,
-//                     character: 22,
-//                 },
-//             },
-//             new_text: "Some(2)".to_string(),
-//         })),
-//         additional_text_edits: Some(vec![lsp::TextEdit {
-//             range: lsp::Range {
-//                 start: lsp::Position {
-//                     line: 0,
-//                     character: 20,
-//                 },
-//                 end: lsp::Position {
-//                     line: 0,
-//                     character: 22,
-//                 },
-//             },
-//             new_text: "".to_string(),
-//         }]),
-//         ..Default::default()
-//     };
-
-//     let closure_completion_item = completion_item.clone();
-//     let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
-//         let task_completion_item = closure_completion_item.clone();
-//         async move {
-//             Ok(Some(lsp::CompletionResponse::Array(vec![
-//                 task_completion_item,
-//             ])))
-//         }
-//     });
+    cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
+    cx.simulate_keystroke(".");
+    let completion_item = lsp::CompletionItem {
+        label: "some".into(),
+        kind: Some(lsp::CompletionItemKind::SNIPPET),
+        detail: Some("Wrap the expression in an `Option::Some`".to_string()),
+        documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+            kind: lsp::MarkupKind::Markdown,
+            value: "```rust\nSome(2)\n```".to_string(),
+        })),
+        deprecated: Some(false),
+        sort_text: Some("fffffff2".to_string()),
+        filter_text: Some("some".to_string()),
+        insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+        text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "Some(2)".to_string(),
+        })),
+        additional_text_edits: Some(vec![lsp::TextEdit {
+            range: lsp::Range {
+                start: lsp::Position {
+                    line: 0,
+                    character: 20,
+                },
+                end: lsp::Position {
+                    line: 0,
+                    character: 22,
+                },
+            },
+            new_text: "".to_string(),
+        }]),
+        ..Default::default()
+    };
 
-//     request.next().await;
+    let closure_completion_item = completion_item.clone();
+    let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
+        let task_completion_item = closure_completion_item.clone();
+        async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                task_completion_item,
+            ])))
+        }
+    });
 
-//     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(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
-
-//     cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
-//         let task_completion_item = completion_item.clone();
-//         async move { Ok(task_completion_item) }
-//     })
-//     .next()
-//     .await
-//     .unwrap();
-//     apply_additional_edits.await.unwrap();
-//     cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
-// }
+    request.next().await;
 
-// #[gpui::test]
-// async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
-//     init_test(cx, |_| {});
+    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(indoc! {"fn main() { let a = 2.Some(2)ˇ; }"});
+
+    cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+        let task_completion_item = completion_item.clone();
+        async move { Ok(task_completion_item) }
+    })
+    .next()
+    .await
+    .unwrap();
+    apply_additional_edits.await.unwrap();
+    cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ˇ; }"});
+}
 
-//     let mut cx = EditorLspTestContext::new(
-//         Language::new(
-//             LanguageConfig {
-//                 path_suffixes: vec!["jsx".into()],
-//                 overrides: [(
-//                     "element".into(),
-//                     LanguageConfigOverride {
-//                         word_characters: Override::Set(['-'].into_iter().collect()),
-//                         ..Default::default()
-//                     },
-//                 )]
-//                 .into_iter()
-//                 .collect(),
-//                 ..Default::default()
-//             },
-//             Some(tree_sitter_typescript::language_tsx()),
-//         )
-//         .with_override_query("(jsx_self_closing_element) @element")
-//         .unwrap(),
-//         lsp::ServerCapabilities {
-//             completion_provider: Some(lsp::CompletionOptions {
-//                 trigger_characters: Some(vec![":".to_string()]),
-//                 ..Default::default()
-//             }),
-//             ..Default::default()
-//         },
-//         cx,
-//     )
-//     .await;
+#[gpui::test]
+async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
+    init_test(cx, |_| {});
 
-//     cx.lsp
-//         .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
-//             Ok(Some(lsp::CompletionResponse::Array(vec![
-//                 lsp::CompletionItem {
-//                     label: "bg-blue".into(),
-//                     ..Default::default()
-//                 },
-//                 lsp::CompletionItem {
-//                     label: "bg-red".into(),
-//                     ..Default::default()
-//                 },
-//                 lsp::CompletionItem {
-//                     label: "bg-yellow".into(),
-//                     ..Default::default()
-//                 },
-//             ])))
-//         });
+    let mut cx = EditorLspTestContext::new(
+        Language::new(
+            LanguageConfig {
+                path_suffixes: vec!["jsx".into()],
+                overrides: [(
+                    "element".into(),
+                    LanguageConfigOverride {
+                        word_characters: Override::Set(['-'].into_iter().collect()),
+                        ..Default::default()
+                    },
+                )]
+                .into_iter()
+                .collect(),
+                ..Default::default()
+            },
+            Some(tree_sitter_typescript::language_tsx()),
+        )
+        .with_override_query("(jsx_self_closing_element) @element")
+        .unwrap(),
+        lsp::ServerCapabilities {
+            completion_provider: Some(lsp::CompletionOptions {
+                trigger_characters: Some(vec![":".to_string()]),
+                ..Default::default()
+            }),
+            ..Default::default()
+        },
+        cx,
+    )
+    .await;
 
-//     cx.set_state(r#"<p class="bgˇ" />"#);
+    cx.lsp
+        .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
+            Ok(Some(lsp::CompletionResponse::Array(vec![
+                lsp::CompletionItem {
+                    label: "bg-blue".into(),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "bg-red".into(),
+                    ..Default::default()
+                },
+                lsp::CompletionItem {
+                    label: "bg-yellow".into(),
+                    ..Default::default()
+                },
+            ])))
+        });
 
-//     // Trigger completion when typing a dash, because the dash is an extra
-//     // word character in the 'element' scope, which contains the cursor.
-//     cx.simulate_keystroke("-");
-//     cx.executor().run_until_parked();
-//     cx.update_editor(|editor, _| {
-//         if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
-//             assert_eq!(
-//                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-//                 &["bg-red", "bg-blue", "bg-yellow"]
-//             );
-//         } else {
-//             panic!("expected completion menu to be open");
-//         }
-//     });
+    cx.set_state(r#"<p class="bgˇ" />"#);
 
-//     cx.simulate_keystroke("l");
-//     cx.executor().run_until_parked();
-//     cx.update_editor(|editor, _| {
-//         if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
-//             assert_eq!(
-//                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-//                 &["bg-blue", "bg-yellow"]
-//             );
-//         } else {
-//             panic!("expected completion menu to be open");
-//         }
-//     });
+    // Trigger completion when typing a dash, because the dash is an extra
+    // word character in the 'element' scope, which contains the cursor.
+    cx.simulate_keystroke("-");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-red", "bg-blue", "bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
 
-//     // When filtering completions, consider the character after the '-' to
-//     // be the start of a subword.
-//     cx.set_state(r#"<p class="yelˇ" />"#);
-//     cx.simulate_keystroke("l");
-//     cx.executor().run_until_parked();
-//     cx.update_editor(|editor, _| {
-//         if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
-//             assert_eq!(
-//                 menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-//                 &["bg-yellow"]
-//             );
-//         } else {
-//             panic!("expected completion menu to be open");
-//         }
-//     });
-// }
+    cx.simulate_keystroke("l");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-blue", "bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+
+    // When filtering completions, consider the character after the '-' to
+    // be the start of a subword.
+    cx.set_state(r#"<p class="yelˇ" />"#);
+    cx.simulate_keystroke("l");
+    cx.executor().run_until_parked();
+    cx.update_editor(|editor, _| {
+        if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+            assert_eq!(
+                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                &["bg-yellow"]
+            );
+        } else {
+            panic!("expected completion menu to be open");
+        }
+    });
+}
 
 #[gpui::test]
 async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
@@ -5,7 +5,7 @@ use crate::{
     Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, InlayId,
     SelectPhase,
 };
-use gpui::{Task, ViewContext};
+use gpui::{px, Task, ViewContext};
 use language::{Bias, ToOffset};
 use lsp::LanguageServerId;
 use project::{
@@ -13,6 +13,7 @@ use project::{
     ResolveState,
 };
 use std::ops::Range;
+use theme::ActiveTheme as _;
 use util::TryFutureExt;
 
 #[derive(Debug, Default)]
@@ -485,40 +486,45 @@ pub fn show_link_definition(
                         });
 
                     if any_definition_does_not_contain_current_location {
-                        // todo!()
-                        // // Highlight symbol using theme link definition highlight style
-                        // let style = theme::current(cx).editor.link_definition;
-                        // let highlight_range =
-                        //     symbol_range.unwrap_or_else(|| match &trigger_point {
-                        //         TriggerPoint::Text(trigger_anchor) => {
-                        //             let snapshot = &snapshot.buffer_snapshot;
-                        //             // If no symbol range returned from language server, use the surrounding word.
-                        //             let (offset_range, _) =
-                        //                 snapshot.surrounding_word(*trigger_anchor);
-                        //             RangeInEditor::Text(
-                        //                 snapshot.anchor_before(offset_range.start)
-                        //                     ..snapshot.anchor_after(offset_range.end),
-                        //             )
-                        //         }
-                        //         TriggerPoint::InlayHint(highlight, _, _) => {
-                        //             RangeInEditor::Inlay(highlight.clone())
-                        //         }
-                        //     });
-
-                        // match highlight_range {
-                        //     RangeInEditor::Text(text_range) => this
-                        //         .highlight_text::<LinkGoToDefinitionState>(
-                        //             vec![text_range],
-                        //             style,
-                        //             cx,
-                        //         ),
-                        //     RangeInEditor::Inlay(highlight) => this
-                        //         .highlight_inlays::<LinkGoToDefinitionState>(
-                        //             vec![highlight],
-                        //             style,
-                        //             cx,
-                        //         ),
-                        // }
+                        let style = gpui::HighlightStyle {
+                            underline: Some(gpui::UnderlineStyle {
+                                thickness: px(1.),
+                                ..Default::default()
+                            }),
+                            color: Some(gpui::red()),
+                            ..Default::default()
+                        };
+                        let highlight_range =
+                            symbol_range.unwrap_or_else(|| match &trigger_point {
+                                TriggerPoint::Text(trigger_anchor) => {
+                                    let snapshot = &snapshot.buffer_snapshot;
+                                    // If no symbol range returned from language server, use the surrounding word.
+                                    let (offset_range, _) =
+                                        snapshot.surrounding_word(*trigger_anchor);
+                                    RangeInEditor::Text(
+                                        snapshot.anchor_before(offset_range.start)
+                                            ..snapshot.anchor_after(offset_range.end),
+                                    )
+                                }
+                                TriggerPoint::InlayHint(highlight, _, _) => {
+                                    RangeInEditor::Inlay(highlight.clone())
+                                }
+                            });
+
+                        match highlight_range {
+                            RangeInEditor::Text(text_range) => this
+                                .highlight_text::<LinkGoToDefinitionState>(
+                                    vec![text_range],
+                                    style,
+                                    cx,
+                                ),
+                            RangeInEditor::Inlay(highlight) => this
+                                .highlight_inlays::<LinkGoToDefinitionState>(
+                                    vec![highlight],
+                                    style,
+                                    cx,
+                                ),
+                        }
                     } else {
                         hide_link_definition(this, cx);
                     }

crates/editor2/src/selections_collection.rs 🔗

@@ -595,31 +595,32 @@ impl<'a> MutableSelectionsCollection<'a> {
         self.select(selections)
     }
 
-    pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
-        todo!()
-        // let buffer = self.buffer.read(self.cx).snapshot(self.cx);
-        // let selections = ranges
-        //     .into_iter()
-        //     .map(|range| {
-        //         let mut start = range.start;
-        //         let mut end = range.end;
-        //         let reversed = if start.cmp(&end, &buffer).is_gt() {
-        //             mem::swap(&mut start, &mut end);
-        //             true
-        //         } else {
-        //             false
-        //         };
-        //         Selection {
-        //             id: post_inc(&mut self.collection.next_selection_id),
-        //             start,
-        //             end,
-        //             reversed,
-        //             goal: SelectionGoal::None,
-        //         }
-        //     })
-        //     .collect::<Vec<_>>();
-
-        // self.select_anchors(selections)
+    pub fn select_anchor_ranges<I>(&mut self, ranges: I)
+    where
+        I: IntoIterator<Item = Range<Anchor>>,
+    {
+        let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+        let selections = ranges
+            .into_iter()
+            .map(|range| {
+                let mut start = range.start;
+                let mut end = range.end;
+                let reversed = if start.cmp(&end, &buffer).is_gt() {
+                    mem::swap(&mut start, &mut end);
+                    true
+                } else {
+                    false
+                };
+                Selection {
+                    id: post_inc(&mut self.collection.next_selection_id),
+                    start,
+                    end,
+                    reversed,
+                    goal: SelectionGoal::None,
+                }
+            })
+            .collect::<Vec<_>>();
+        self.select_anchors(selections)
     }
 
     pub fn new_selection_id(&mut self) -> usize {

crates/gpui2/src/elements/text.rs 🔗

@@ -265,7 +265,9 @@ impl TextState {
 pub struct InteractiveText {
     element_id: ElementId,
     text: StyledText,
-    click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
+    click_listener:
+        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
+    clickable_ranges: Vec<Range<usize>>,
 }
 
 struct InteractiveTextClickEvent {
@@ -284,6 +286,7 @@ impl InteractiveText {
             element_id: id.into(),
             text,
             click_listener: None,
+            clickable_ranges: Vec::new(),
         }
     }
 
@@ -292,7 +295,7 @@ impl InteractiveText {
         ranges: Vec<Range<usize>>,
         listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
     ) -> Self {
-        self.click_listener = Some(Box::new(move |event, cx| {
+        self.click_listener = Some(Box::new(move |ranges, event, cx| {
             for (range_ix, range) in ranges.iter().enumerate() {
                 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
                 {
@@ -300,6 +303,7 @@ impl InteractiveText {
                 }
             }
         }));
+        self.clickable_ranges = ranges;
         self
     }
 }
@@ -334,6 +338,19 @@ impl Element for InteractiveText {
 
     fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
         if let Some(click_listener) = self.click_listener {
+            if let Some(ix) = state
+                .text_state
+                .index_for_position(bounds, cx.mouse_position())
+            {
+                if self
+                    .clickable_ranges
+                    .iter()
+                    .any(|range| range.contains(&ix))
+                {
+                    cx.set_cursor_style(crate::CursorStyle::PointingHand)
+                }
+            }
+
             let text_state = state.text_state.clone();
             let mouse_down = state.mouse_down_index.clone();
             if let Some(mouse_down_index) = mouse_down.get() {
@@ -343,6 +360,7 @@ impl Element for InteractiveText {
                             text_state.index_for_position(bounds, event.position)
                         {
                             click_listener(
+                                &self.clickable_ranges,
                                 InteractiveTextClickEvent {
                                     mouse_down_index,
                                     mouse_up_index,

crates/project2/src/lsp_command.rs 🔗

@@ -717,8 +717,9 @@ async fn location_links_from_lsp(
             })?
             .await?;
 
-        buffer.update(&mut cx, |origin_buffer, cx| {
+        cx.update(|cx| {
             let origin_location = origin_range.map(|origin_range| {
+                let origin_buffer = buffer.read(cx);
                 let origin_start =
                     origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
                 let origin_end =