lsp: Send DidOpen notifications when changing selections in multi buffer (#22958)

Piotr Osiewicz created

Fixes #22773

Release Notes:

- Fixed an edge case with multibuffers that could break language
features within them.

Change summary

crates/diagnostics/src/diagnostics_tests.rs | 34 +++++++++++-----------
crates/editor/src/editor.rs                 | 17 ++++++++++
crates/editor/src/editor_tests.rs           | 12 ++++----
crates/project/src/lsp_store.rs             |  7 ++++
crates/search/src/project_search.rs         | 18 ++++++------
5 files changed, 54 insertions(+), 34 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -18,7 +18,7 @@ use std::{
     path::{Path, PathBuf},
 };
 use unindent::Unindent as _;
-use util::{post_inc, RandomCharIter};
+use util::{path, post_inc, RandomCharIter};
 
 #[ctor::ctor]
 fn init_logger() {
@@ -33,7 +33,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
-        "/test",
+        path!("/test"),
         json!({
             "consts.rs": "
                 const a: i32 = 'a';
@@ -59,7 +59,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     .await;
 
     let language_server_id = LanguageServerId(0);
-    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
     let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
     let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*window, cx);
@@ -70,7 +70,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 language_server_id,
-                PathBuf::from("/test/main.rs"),
+                PathBuf::from(path!("/test/main.rs")),
                 None,
                 vec![
                     DiagnosticEntry {
@@ -234,7 +234,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 language_server_id,
-                PathBuf::from("/test/consts.rs"),
+                PathBuf::from(path!("/test/consts.rs")),
                 None,
                 vec![DiagnosticEntry {
                     range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
@@ -341,7 +341,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 language_server_id,
-                PathBuf::from("/test/consts.rs"),
+                PathBuf::from(path!("/test/consts.rs")),
                 None,
                 vec![
                     DiagnosticEntry {
@@ -464,7 +464,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
-        "/test",
+        path!("/test"),
         json!({
             "main.js": "
                 a();
@@ -479,7 +479,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     let server_id_1 = LanguageServerId(100);
     let server_id_2 = LanguageServerId(101);
-    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
     let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
     let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*window, cx);
@@ -504,7 +504,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 server_id_1,
-                PathBuf::from("/test/main.js"),
+                PathBuf::from(path!("/test/main.js")),
                 None,
                 vec![DiagnosticEntry {
                     range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
@@ -557,7 +557,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 server_id_2,
-                PathBuf::from("/test/main.js"),
+                PathBuf::from(path!("/test/main.js")),
                 None,
                 vec![DiagnosticEntry {
                     range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
@@ -619,7 +619,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 server_id_1,
-                PathBuf::from("/test/main.js"),
+                PathBuf::from(path!("/test/main.js")),
                 None,
                 vec![DiagnosticEntry {
                     range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
@@ -638,7 +638,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 server_id_2,
-                PathBuf::from("/test/main.rs"),
+                PathBuf::from(path!("/test/main.rs")),
                 None,
                 vec![],
                 cx,
@@ -689,7 +689,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store
             .update_diagnostic_entries(
                 server_id_2,
-                PathBuf::from("/test/main.js"),
+                PathBuf::from(path!("/test/main.js")),
                 None,
                 vec![DiagnosticEntry {
                     range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
@@ -755,9 +755,9 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
         .unwrap_or(10);
 
     let fs = FakeFs::new(cx.executor());
-    fs.insert_tree("/test", json!({})).await;
+    fs.insert_tree(path!("/test"), json!({})).await;
 
-    let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
+    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
     let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
     let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*window, cx);
@@ -817,7 +817,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
                         // insert a set of diagnostics for a new path
                         _ => {
                             let path: PathBuf =
-                                format!("/test/{}.rs", post_inc(&mut next_filename)).into();
+                                format!(path!("/test/{}.rs"), post_inc(&mut next_filename)).into();
                             let len = rng.gen_range(128..256);
                             let content =
                                 RandomCharIter::new(&mut rng).take(len).collect::<String>();
@@ -891,7 +891,7 @@ async fn test_random_diagnostics(cx: &mut TestAppContext, mut rng: StdRng) {
         for diagnostic in diagnostics {
             let found_excerpt = reference_excerpts.iter().any(|info| {
                 let row_range = info.range.context.start.row..info.range.context.end.row;
-                info.path == path.strip_prefix("/test").unwrap()
+                info.path == path.strip_prefix(path!("/test")).unwrap()
                     && info.language_server == language_server_id
                     && row_range.contains(&diagnostic.range.start.0.row)
             });

crates/editor/src/editor.rs 🔗

@@ -1793,7 +1793,7 @@ impl Editor {
         self.collapse_matches = collapse_matches;
     }
 
-    pub fn register_buffers_with_language_servers(&mut self, cx: &mut Context<Self>) {
+    fn register_buffers_with_language_servers(&mut self, cx: &mut Context<Self>) {
         let buffers = self.buffer.read(cx).all_buffers();
         let Some(lsp_store) = self.lsp_store(cx) else {
             return;
@@ -2020,6 +2020,21 @@ impl Editor {
                     None
                 }
             };
+            if let Some(buffer_id) = new_cursor_position.buffer_id {
+                if !self.registered_buffers.contains_key(&buffer_id) {
+                    if let Some(lsp_store) = self.lsp_store(cx) {
+                        lsp_store.update(cx, |lsp_store, cx| {
+                            let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else {
+                                return;
+                            };
+                            self.registered_buffers.insert(
+                                buffer_id,
+                                lsp_store.register_buffer_with_language_servers(&buffer, cx),
+                            );
+                        })
+                    }
+                }
+            }
 
             if let Some(completion_menu) = completion_menu {
                 let cursor_position = new_cursor_position.to_offset(buffer);

crates/editor/src/editor_tests.rs 🔗

@@ -14875,7 +14875,7 @@ async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
-        "/a",
+        path!("/a"),
         json!({
             "first.rs": sample_text_1,
             "second.rs": sample_text_2,
@@ -14883,7 +14883,7 @@ async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
         }),
     )
     .await;
-    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
     let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
     let worktree = project.update(cx, |project, cx| {
@@ -15059,7 +15059,7 @@ async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
-        "/a",
+        path!("/a"),
         json!({
             "first.rs": sample_text_1,
             "second.rs": sample_text_2,
@@ -15067,7 +15067,7 @@ async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext
         }),
     )
     .await;
-    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
     let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
     let worktree = project.update(cx, |project, cx| {
@@ -15206,13 +15206,13 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
 
     let fs = FakeFs::new(cx.executor());
     fs.insert_tree(
-        "/a",
+        path!("/a"),
         json!({
             "main.rs": sample_text,
         }),
     )
     .await;
-    let project = Project::test(fs, ["/a".as_ref()], cx).await;
+    let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
     let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
     let worktree = project.update(cx, |project, cx| {

crates/project/src/lsp_store.rs 🔗

@@ -1977,7 +1977,12 @@ impl LocalLspStore {
             Some(local) => local.abs_path(cx),
             None => return,
         };
-        let file_url = lsp::Url::from_file_path(old_path).unwrap();
+        let file_url = lsp::Url::from_file_path(old_path.as_path()).unwrap_or_else(|_| {
+            panic!(
+                "`{}` is not parseable as an URI",
+                old_path.to_string_lossy()
+            )
+        });
         self.unregister_buffer_from_language_servers(buffer, file_url, cx);
     }
 

crates/search/src/project_search.rs 🔗

@@ -2197,7 +2197,7 @@ pub mod tests {
 
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
-            "/dir",
+            path!("/dir"),
             json!({
                 "one.rs": "const ONE: usize = 1;",
                 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
@@ -2206,7 +2206,7 @@ pub mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
         let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
         let workspace = window.root(cx).unwrap();
         let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx));
@@ -2564,7 +2564,7 @@ pub mod tests {
 
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
-            "/dir",
+            path!("/dir"),
             json!({
                 "one.rs": "const ONE: usize = 1;",
                 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
@@ -2573,7 +2573,7 @@ pub mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
         let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
         let workspace = window;
         let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
@@ -2859,7 +2859,7 @@ pub mod tests {
 
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
-            "/dir",
+            path!("/dir"),
             json!({
                 "a": {
                     "one.rs": "const ONE: usize = 1;",
@@ -2984,7 +2984,7 @@ pub mod tests {
 
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
-            "/dir",
+            path!("/dir"),
             json!({
                 "one.rs": "const ONE: usize = 1;",
                 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
@@ -2993,7 +2993,7 @@ pub mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
         let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
         let workspace = window.root(cx).unwrap();
         let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new());
@@ -3693,7 +3693,7 @@ pub mod tests {
         // We need many lines in the search results to be able to scroll the window
         let fs = FakeFs::new(cx.background_executor.clone());
         fs.insert_tree(
-            "/dir",
+            path!("/dir"),
             json!({
                 "1.txt": "\n\n\n\n\n A \n\n\n\n\n",
                 "2.txt": "\n\n\n\n\n A \n\n\n\n\n",
@@ -3718,7 +3718,7 @@ pub mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+        let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
         let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
         let workspace = window.root(cx).unwrap();
         let search = cx.new(|cx| ProjectSearch::new(project, cx));