Start language servers based on buffers' languages

Max Brunsfeld and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                      |   1 
crates/language/Cargo.toml      |   8 +
crates/language/src/language.rs |  31 ++++++
crates/language/src/lib.rs      |   5 
crates/language/src/tests.rs    |   8 -
crates/lsp/src/lib.rs           |   7 +
crates/project/Cargo.toml       |   1 
crates/project/src/lib.rs       |   9 -
crates/project/src/worktree.rs  | 159 +++++++++++++++++++++-------------
crates/server/src/rpc.rs        |  59 ++++++++----
crates/workspace/src/items.rs   |  14 ++
crates/zed/src/language.rs      |   2 
12 files changed, 197 insertions(+), 107 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3831,6 +3831,7 @@ dependencies = [
  "rpc",
  "serde 1.0.125",
  "serde_json 1.0.64",
+ "simplelog",
  "smol",
  "sum_tree",
  "tempdir",

crates/language/Cargo.toml 🔗

@@ -4,7 +4,12 @@ version = "0.1.0"
 edition = "2018"
 
 [features]
-test-support = ["rand", "buffer/test-support", "lsp/test-support"]
+test-support = [
+    "rand",
+    "buffer/test-support",
+    "lsp/test-support",
+    "tree-sitter-rust",
+]
 
 [dependencies]
 buffer = { path = "../buffer" }
@@ -25,6 +30,7 @@ serde = { version = "1", features = ["derive"] }
 similar = "1.3"
 smol = "1.2"
 tree-sitter = "0.19.5"
+tree-sitter-rust = { version = "0.19.0", optional = true }
 
 [dev-dependencies]
 buffer = { path = "../buffer", features = ["test-support"] }

crates/language/src/language.rs 🔗

@@ -1,6 +1,7 @@
 use crate::HighlightMap;
 use anyhow::Result;
-use gpui::AppContext;
+use gpui::{executor::Background, AppContext};
+use lsp::LanguageServer;
 use parking_lot::Mutex;
 use serde::Deserialize;
 use std::{collections::HashSet, path::Path, str, sync::Arc};
@@ -16,10 +17,13 @@ pub struct LanguageConfig {
     pub language_server: Option<LanguageServerConfig>,
 }
 
-#[derive(Deserialize)]
+#[derive(Default, Deserialize)]
 pub struct LanguageServerConfig {
     pub binary: String,
     pub disk_based_diagnostic_sources: HashSet<String>,
+    #[cfg(any(test, feature = "test-support"))]
+    #[serde(skip)]
+    pub fake_server: Option<(Arc<LanguageServer>, Arc<std::sync::atomic::AtomicBool>)>,
 }
 
 #[derive(Clone, Debug, Deserialize)]
@@ -117,6 +121,12 @@ impl Language {
         cx: &AppContext,
     ) -> Result<Option<Arc<lsp::LanguageServer>>> {
         if let Some(config) = &self.config.language_server {
+            #[cfg(any(test, feature = "test-support"))]
+            if let Some((server, started)) = &config.fake_server {
+                started.store(true, std::sync::atomic::Ordering::SeqCst);
+                return Ok(Some(server.clone()));
+            }
+
             const ZED_BUNDLE: Option<&'static str> = option_env!("ZED_BUNDLE");
             let binary_path = if ZED_BUNDLE.map_or(Ok(false), |b| b.parse())? {
                 cx.platform()
@@ -151,6 +161,23 @@ impl Language {
     }
 }
 
+#[cfg(any(test, feature = "test-support"))]
+impl LanguageServerConfig {
+    pub async fn fake(executor: Arc<Background>) -> (Self, lsp::FakeLanguageServer) {
+        let (server, fake) = lsp::LanguageServer::fake(executor).await;
+        fake.started
+            .store(false, std::sync::atomic::Ordering::SeqCst);
+        let started = fake.started.clone();
+        (
+            Self {
+                fake_server: Some((server, started)),
+                ..Default::default()
+            },
+            fake,
+        )
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/language/src/lib.rs 🔗

@@ -6,7 +6,7 @@ mod tests;
 
 pub use self::{
     highlight_map::{HighlightId, HighlightMap},
-    language::{BracketPair, Language, LanguageConfig, LanguageRegistry},
+    language::{BracketPair, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig},
 };
 use anyhow::{anyhow, Result};
 pub use buffer::{Buffer as TextBuffer, Operation as _, *};
@@ -37,6 +37,9 @@ use std::{
 use tree_sitter::{InputEdit, Parser, QueryCursor, Tree};
 use util::{post_inc, TryFutureExt as _};
 
+#[cfg(any(test, feature = "test-support"))]
+pub use tree_sitter_rust;
+
 pub use lsp::DiagnosticSeverity;
 
 thread_local! {

crates/language/src/tests.rs 🔗

@@ -1,7 +1,6 @@
 use super::*;
-use crate::language::LanguageServerConfig;
 use gpui::{ModelHandle, MutableAppContext};
-use std::{iter::FromIterator, rc::Rc};
+use std::rc::Rc;
 use unindent::Unindent as _;
 
 #[gpui::test]
@@ -676,10 +675,7 @@ fn rust_lang() -> Option<Arc<Language>> {
             LanguageConfig {
                 name: "Rust".to_string(),
                 path_suffixes: vec!["rs".to_string()],
-                language_server: Some(LanguageServerConfig {
-                    binary: "rust-analyzer".to_string(),
-                    disk_based_diagnostic_sources: HashSet::from_iter(vec!["rustc".to_string()]),
-                }),
+                language_server: None,
                 ..Default::default()
             },
             tree_sitter_rust::language(),

crates/lsp/src/lib.rs 🔗

@@ -16,7 +16,7 @@ use std::{
     io::Write,
     str::FromStr,
     sync::{
-        atomic::{AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
 };
@@ -427,6 +427,7 @@ pub struct FakeLanguageServer {
     buffer: Vec<u8>,
     stdin: smol::io::BufReader<async_pipe::PipeReader>,
     stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
+    pub started: Arc<AtomicBool>,
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -444,6 +445,7 @@ impl LanguageServer {
             stdin: smol::io::BufReader::new(stdin.1),
             stdout: smol::io::BufWriter::new(stdout.0),
             buffer: Vec::new(),
+            started: Arc::new(AtomicBool::new(true)),
         };
 
         let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
@@ -460,6 +462,9 @@ impl LanguageServer {
 #[cfg(any(test, feature = "test-support"))]
 impl FakeLanguageServer {
     pub async fn notify<T: notification::Notification>(&mut self, params: T::Params) {
+        if !self.started.load(std::sync::atomic::Ordering::SeqCst) {
+            panic!("can't simulate an LSP notification before the server has been started");
+        }
         let message = serde_json::to_vec(&Notification {
             jsonrpc: JSON_RPC_VERSION,
             method: T::METHOD,

crates/project/Cargo.toml 🔗

@@ -40,5 +40,6 @@ lsp = { path = "../lsp", features = ["test-support"] }
 util = { path = "../util", features = ["test-support"] }
 rpc = { path = "../rpc", features = ["test-support"] }
 rand = "0.8.3"
+simplelog = "0.9"
 tempdir = { version = "0.3.7" }
 unindent = "0.1.7"

crates/project/src/lib.rs 🔗

@@ -12,7 +12,7 @@ use std::{
     path::Path,
     sync::{atomic::AtomicBool, Arc},
 };
-use util::{ResultExt, TryFutureExt as _};
+use util::TryFutureExt as _;
 
 pub use fs::*;
 pub use worktree::*;
@@ -73,13 +73,8 @@ impl Project {
         let rpc = self.client.clone();
         let languages = self.languages.clone();
         let path = Arc::from(abs_path);
-        let language_server = languages
-            .get_language("Rust")
-            .map(|language| language.start_server(&path, cx));
         cx.spawn(|this, mut cx| async move {
-            let language_server = language_server.and_then(|language| language.log_err().flatten());
-            let worktree =
-                Worktree::open_local(rpc, path, fs, languages, language_server, &mut cx).await?;
+            let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
             this.update(&mut cx, |this, cx| {
                 this.add_worktree(worktree.clone(), cx);
             });

crates/project/src/worktree.rs 🔗

@@ -12,7 +12,7 @@ use gpui::{
     executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
     Task, UpgradeModelHandle, WeakModelHandle,
 };
-use language::{Buffer, LanguageRegistry, Operation, Rope};
+use language::{Buffer, Language, LanguageRegistry, Operation, Rope};
 use lazy_static::lazy_static;
 use lsp::LanguageServer;
 use parking_lot::Mutex;
@@ -98,17 +98,21 @@ impl Entity for Worktree {
     ) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
         use futures::FutureExt;
 
-        if let Some(server) = self.language_server() {
-            if let Some(shutdown) = server.shutdown() {
-                return Some(
-                    async move {
-                        shutdown.await.log_err();
-                    }
-                    .boxed(),
-                );
-            }
+        if let Self::Local(worktree) = self {
+            let shutdown_futures = worktree
+                .language_servers
+                .drain()
+                .filter_map(|(_, server)| server.shutdown())
+                .collect::<Vec<_>>();
+            Some(
+                async move {
+                    futures::future::join_all(shutdown_futures).await;
+                }
+                .boxed(),
+            )
+        } else {
+            None
         }
-        None
     }
 }
 
@@ -118,11 +122,10 @@ impl Worktree {
         path: impl Into<Arc<Path>>,
         fs: Arc<dyn Fs>,
         languages: Arc<LanguageRegistry>,
-        language_server: Option<Arc<LanguageServer>>,
         cx: &mut AsyncAppContext,
     ) -> Result<ModelHandle<Self>> {
         let (tree, scan_states_tx) =
-            LocalWorktree::new(rpc, path, fs.clone(), languages, language_server, cx).await?;
+            LocalWorktree::new(rpc, path, fs.clone(), languages, cx).await?;
         tree.update(cx, |tree, cx| {
             let tree = tree.as_local_mut().unwrap();
             let abs_path = tree.snapshot.abs_path.clone();
@@ -315,13 +318,6 @@ impl Worktree {
         }
     }
 
-    pub fn language_server(&self) -> Option<&Arc<LanguageServer>> {
-        match self {
-            Worktree::Local(worktree) => worktree.language_server.as_ref(),
-            Worktree::Remote(_) => None,
-        }
-    }
-
     pub fn handle_add_peer(
         &mut self,
         envelope: TypedEnvelope<proto::AddPeer>,
@@ -781,7 +777,7 @@ pub struct LocalWorktree {
     languages: Arc<LanguageRegistry>,
     rpc: Arc<Client>,
     fs: Arc<dyn Fs>,
-    language_server: Option<Arc<LanguageServer>>,
+    language_servers: HashMap<String, Arc<LanguageServer>>,
 }
 
 #[derive(Default, Deserialize)]
@@ -795,7 +791,6 @@ impl LocalWorktree {
         path: impl Into<Arc<Path>>,
         fs: Arc<dyn Fs>,
         languages: Arc<LanguageRegistry>,
-        language_server: Option<Arc<LanguageServer>>,
         cx: &mut AsyncAppContext,
     ) -> Result<(ModelHandle<Worktree>, Sender<ScanState>)> {
         let abs_path = path.into();
@@ -896,7 +891,7 @@ impl LocalWorktree {
                 languages,
                 rpc,
                 fs,
-                language_server,
+                language_servers: Default::default(),
             };
 
             cx.spawn_weak(|this, mut cx| async move {
@@ -926,33 +921,57 @@ impl LocalWorktree {
             })
             .detach();
 
-            if let Some(language_server) = &tree.language_server {
-                let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
-                language_server
-                    .on_notification::<lsp::notification::PublishDiagnostics, _>(move |params| {
-                        smol::block_on(diagnostics_tx.send(params)).ok();
-                    })
-                    .detach();
-                cx.spawn_weak(|this, mut cx| async move {
-                    while let Ok(diagnostics) = diagnostics_rx.recv().await {
-                        if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
-                            handle.update(&mut cx, |this, cx| {
-                                this.update_diagnostics(diagnostics, cx).log_err();
-                            });
-                        } else {
-                            break;
-                        }
-                    }
-                })
-                .detach();
-            }
-
             Worktree::Local(tree)
         });
 
         Ok((tree, scan_states_tx))
     }
 
+    pub fn languages(&self) -> &LanguageRegistry {
+        &self.languages
+    }
+
+    pub fn ensure_language_server(
+        &mut self,
+        language: &Language,
+        cx: &mut ModelContext<Worktree>,
+    ) -> Option<Arc<LanguageServer>> {
+        if let Some(server) = self.language_servers.get(language.name()) {
+            return Some(server.clone());
+        }
+
+        if let Some(language_server) = language
+            .start_server(self.abs_path(), cx)
+            .log_err()
+            .flatten()
+        {
+            let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
+            language_server
+                .on_notification::<lsp::notification::PublishDiagnostics, _>(move |params| {
+                    smol::block_on(diagnostics_tx.send(params)).ok();
+                })
+                .detach();
+            cx.spawn_weak(|this, mut cx| async move {
+                while let Ok(diagnostics) = diagnostics_rx.recv().await {
+                    if let Some(handle) = cx.read(|cx| this.upgrade(cx)) {
+                        handle.update(&mut cx, |this, cx| {
+                            this.update_diagnostics(diagnostics, cx).log_err();
+                        });
+                    } else {
+                        break;
+                    }
+                }
+            })
+            .detach();
+
+            self.language_servers
+                .insert(language.name().to_string(), language_server.clone());
+            Some(language_server.clone())
+        } else {
+            None
+        }
+    }
+
     pub fn open_buffer(
         &mut self,
         path: &Path,
@@ -976,7 +995,6 @@ impl LocalWorktree {
         });
 
         let path = Arc::from(path);
-        let language_server = self.language_server.clone();
         cx.spawn(|this, mut cx| async move {
             if let Some(existing_buffer) = existing_buffer {
                 Ok(existing_buffer)
@@ -988,11 +1006,14 @@ impl LocalWorktree {
                     use language::File;
                     this.languages().select_language(file.full_path()).cloned()
                 });
-                let diagnostics = this.update(&mut cx, |this, _| {
-                    this.as_local_mut()
-                        .unwrap()
-                        .diagnostics
-                        .remove(path.as_ref())
+                let (diagnostics, language_server) = this.update(&mut cx, |this, cx| {
+                    let this = this.as_local_mut().unwrap();
+                    (
+                        this.diagnostics.remove(path.as_ref()),
+                        language
+                            .as_ref()
+                            .and_then(|language| this.ensure_language_server(language, cx)),
+                    )
                 });
                 let buffer = cx.add_model(|cx| {
                     let mut buffer = Buffer::from_file(0, contents, Box::new(file), cx);
@@ -2925,7 +2946,8 @@ mod tests {
     use buffer::Point;
     use client::test::FakeServer;
     use fs::RealFs;
-    use language::Diagnostic;
+    use language::{tree_sitter_rust, LanguageServerConfig};
+    use language::{Diagnostic, LanguageConfig};
     use lsp::Url;
     use rand::prelude::*;
     use serde_json::json;
@@ -2957,7 +2979,6 @@ mod tests {
             Arc::from(Path::new("/root")),
             Arc::new(fs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -2990,7 +3011,6 @@ mod tests {
             dir.path(),
             Arc::new(RealFs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3021,7 +3041,6 @@ mod tests {
             file_path.clone(),
             Arc::new(RealFs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3068,7 +3087,6 @@ mod tests {
             dir.path(),
             Arc::new(RealFs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3229,7 +3247,6 @@ mod tests {
             dir.path(),
             Arc::new(RealFs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3284,7 +3301,6 @@ mod tests {
             "/path/to/the-dir".as_ref(),
             fs,
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3333,7 +3349,6 @@ mod tests {
             dir.path(),
             Arc::new(RealFs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3467,7 +3482,6 @@ mod tests {
             dir.path(),
             Arc::new(RealFs),
             Default::default(),
-            None,
             &mut cx.to_async(),
         )
         .await
@@ -3555,7 +3569,21 @@ mod tests {
 
     #[gpui::test]
     async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) {
-        let (language_server, mut fake_lsp) = LanguageServer::fake(cx.background()).await;
+        simplelog::SimpleLogger::init(log::LevelFilter::Info, Default::default()).unwrap();
+
+        let (language_server_config, mut fake_server) =
+            LanguageServerConfig::fake(cx.background()).await;
+        let mut languages = LanguageRegistry::new();
+        languages.add(Arc::new(Language::new(
+            LanguageConfig {
+                name: "Rust".to_string(),
+                path_suffixes: vec!["rs".to_string()],
+                language_server: Some(language_server_config),
+                ..Default::default()
+            },
+            tree_sitter_rust::language(),
+        )));
+
         let dir = temp_tree(json!({
             "a.rs": "fn a() { A }",
             "b.rs": "const y: i32 = 1",
@@ -3565,8 +3593,7 @@ mod tests {
             Client::new(),
             dir.path(),
             Arc::new(RealFs),
-            Default::default(),
-            Some(language_server),
+            Arc::new(languages),
             &mut cx.to_async(),
         )
         .await
@@ -3574,7 +3601,13 @@ mod tests {
         cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
             .await;
 
-        fake_lsp
+        // Cause worktree to start the fake language server
+        let _buffer = tree
+            .update(&mut cx, |tree, cx| tree.open_buffer("b.rs", cx))
+            .await
+            .unwrap();
+
+        fake_server
             .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
                 uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(),
                 version: None,

crates/server/src/rpc.rs 🔗

@@ -982,7 +982,10 @@ mod tests {
         },
         editor::{Editor, EditorSettings, Input},
         fs::{FakeFs, Fs as _},
-        language::{Diagnostic, LanguageRegistry, Point},
+        language::{
+            tree_sitter_rust, Diagnostic, Language, LanguageConfig, LanguageRegistry,
+            LanguageServerConfig, Point,
+        },
         lsp,
         people_panel::JoinWorktree,
         project::{ProjectPath, Worktree},
@@ -1017,7 +1020,6 @@ mod tests {
             "/a".as_ref(),
             fs,
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1126,7 +1128,6 @@ mod tests {
             "/a".as_ref(),
             fs,
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1219,7 +1220,6 @@ mod tests {
             "/a".as_ref(),
             fs.clone(),
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1356,7 +1356,6 @@ mod tests {
             "/dir".as_ref(),
             fs,
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1441,7 +1440,6 @@ mod tests {
             "/dir".as_ref(),
             fs,
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1508,7 +1506,6 @@ mod tests {
             "/dir".as_ref(),
             fs,
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1570,7 +1567,6 @@ mod tests {
             "/a".as_ref(),
             fs,
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await
@@ -1609,9 +1605,20 @@ mod tests {
         mut cx_b: TestAppContext,
     ) {
         cx_a.foreground().forbid_parking();
-        let lang_registry = Arc::new(LanguageRegistry::new());
+        let (language_server_config, mut fake_language_server) =
+            LanguageServerConfig::fake(cx_a.background()).await;
+        let mut lang_registry = LanguageRegistry::new();
+        lang_registry.add(Arc::new(Language::new(
+            LanguageConfig {
+                name: "Rust".to_string(),
+                path_suffixes: vec!["rs".to_string()],
+                language_server: Some(language_server_config),
+                ..Default::default()
+            },
+            tree_sitter_rust::language(),
+        )));
 
-        let (language_server, mut fake_lsp) = lsp::LanguageServer::fake(cx_a.background()).await;
+        let lang_registry = Arc::new(lang_registry);
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start().await;
@@ -1624,8 +1631,8 @@ mod tests {
             "/a",
             json!({
                 ".zed.toml": r#"collaborators = ["user_b"]"#,
-                "a.txt": "one two three",
-                "b.txt": "b-contents",
+                "a.rs": "let one = two",
+                "other.rs": "",
             }),
         )
         .await;
@@ -1634,7 +1641,6 @@ mod tests {
             "/a".as_ref(),
             fs,
             lang_registry.clone(),
-            Some(language_server),
             &mut cx_a.to_async(),
         )
         .await
@@ -1647,21 +1653,33 @@ mod tests {
             .await
             .unwrap();
 
+        // Cause language server to start.
+        let _ = cx_a
+            .background()
+            .spawn(worktree_a.update(&mut cx_a, |worktree, cx| {
+                worktree.open_buffer("other.rs", cx)
+            }))
+            .await
+            .unwrap();
+
         // Simulate a language server reporting errors for a file.
-        fake_lsp
+        fake_language_server
             .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
-                uri: lsp::Url::from_file_path("/a/a.txt").unwrap(),
+                uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
                 version: None,
                 diagnostics: vec![
                     lsp::Diagnostic {
                         severity: Some(lsp::DiagnosticSeverity::ERROR),
-                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 3)),
+                        range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
                         message: "message 1".to_string(),
                         ..Default::default()
                     },
                     lsp::Diagnostic {
                         severity: Some(lsp::DiagnosticSeverity::WARNING),
-                        range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 13)),
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 10),
+                            lsp::Position::new(0, 13),
+                        ),
                         message: "message 2".to_string(),
                         ..Default::default()
                     },
@@ -1682,7 +1700,7 @@ mod tests {
         // Open the file with the errors.
         let buffer_b = cx_b
             .background()
-            .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.txt", cx)))
+            .spawn(worktree_b.update(&mut cx_b, |worktree, cx| worktree.open_buffer("a.rs", cx)))
             .await
             .unwrap();
 
@@ -1693,14 +1711,14 @@ mod tests {
                     .collect::<Vec<_>>(),
                 &[
                     (
-                        Point::new(0, 0)..Point::new(0, 3),
+                        Point::new(0, 4)..Point::new(0, 7),
                         &Diagnostic {
                             message: "message 1".to_string(),
                             severity: lsp::DiagnosticSeverity::ERROR,
                         }
                     ),
                     (
-                        Point { row: 0, column: 8 }..Point { row: 0, column: 13 },
+                        Point::new(0, 10)..Point::new(0, 13),
                         &Diagnostic {
                             severity: lsp::DiagnosticSeverity::WARNING,
                             message: "message 2".to_string()
@@ -2149,7 +2167,6 @@ mod tests {
             "/a".as_ref(),
             fs.clone(),
             lang_registry.clone(),
-            None,
             &mut cx_a.to_async(),
         )
         .await

crates/workspace/src/items.rs 🔗

@@ -127,10 +127,16 @@ impl ItemView for Editor {
 
             cx.spawn(|buffer, mut cx| async move {
                 save_as.await.map(|new_file| {
-                    let (language, language_server) = worktree.read_with(&cx, |worktree, _| {
-                        let language = worktree.languages().select_language(new_file.full_path());
-                        let language_server = worktree.language_server();
-                        (language.cloned(), language_server.cloned())
+                    let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
+                        let worktree = worktree.as_local_mut().unwrap();
+                        let language = worktree
+                            .languages()
+                            .select_language(new_file.full_path())
+                            .cloned();
+                        let language_server = language
+                            .as_ref()
+                            .and_then(|language| worktree.ensure_language_server(language, cx));
+                        (language, language_server.clone())
                     });
 
                     buffer.update(&mut cx, |buffer, cx| {

crates/zed/src/language.rs 🔗

@@ -1,4 +1,4 @@
-pub use language::{Buffer, Diagnostic, Language, LanguageRegistry, Point};
+pub use language::*;
 use rust_embed::RustEmbed;
 use std::borrow::Cow;
 use std::{str, sync::Arc};