Merge branch 'main' into prompt-on-close

Max Brunsfeld created

Change summary

Cargo.lock                                         |  11 
crates/editor/src/editor.rs                        |  67 
crates/language/src/buffer.rs                      |   3 
crates/language/src/diagnostic_set.rs              |  17 
crates/language/src/language.rs                    | 234 ++--
crates/language/src/tests.rs                       |   1 
crates/lsp/src/lsp.rs                              | 497 +++------
crates/project/src/lsp_command.rs                  |  37 
crates/project/src/project.rs                      | 623 +++++++-----
crates/rpc/proto/zed.proto                         |   2 
crates/server/src/rpc.rs                           | 795 +++++++--------
crates/text/src/tests.rs                           |  49 
crates/text/src/text.rs                            |  24 
crates/zed/Cargo.toml                              |   1 
crates/zed/src/languages.rs                        | 112 ++
crates/zed/src/languages/c.rs                      | 114 ++
crates/zed/src/languages/c/brackets.scm            |   0 
crates/zed/src/languages/c/config.toml             |   3 
crates/zed/src/languages/c/highlights.scm          |   0 
crates/zed/src/languages/c/indents.scm             |   0 
crates/zed/src/languages/c/outline.scm             |   0 
crates/zed/src/languages/installation.rs           | 111 ++
crates/zed/src/languages/json.rs                   | 130 ++
crates/zed/src/languages/json/brackets.scm         |   0 
crates/zed/src/languages/json/config.toml          |   3 
crates/zed/src/languages/json/highlights.scm       |   0 
crates/zed/src/languages/json/indents.scm          |   0 
crates/zed/src/languages/json/outline.scm          |   0 
crates/zed/src/languages/markdown/config.toml      |   0 
crates/zed/src/languages/markdown/highlights.scm   |   0 
crates/zed/src/languages/rust.rs                   | 408 -------
crates/zed/src/languages/rust/brackets.scm         |   0 
crates/zed/src/languages/rust/config.toml          |   4 
crates/zed/src/languages/rust/highlights.scm       |   0 
crates/zed/src/languages/rust/indents.scm          |   0 
crates/zed/src/languages/rust/outline.scm          |   0 
crates/zed/src/languages/tsx/brackets.scm          |   1 
crates/zed/src/languages/tsx/config.toml           |  12 
crates/zed/src/languages/tsx/highlights-jsx.scm    |   0 
crates/zed/src/languages/tsx/highlights.scm        |   1 
crates/zed/src/languages/tsx/indents.scm           |   1 
crates/zed/src/languages/tsx/outline.scm           |   1 
crates/zed/src/languages/typescript.rs             | 146 ++
crates/zed/src/languages/typescript/brackets.scm   |   5 
crates/zed/src/languages/typescript/config.toml    |  12 
crates/zed/src/languages/typescript/highlights.scm | 219 ++++
crates/zed/src/languages/typescript/indents.scm    |  15 
crates/zed/src/languages/typescript/outline.scm    |  55 +
crates/zed/src/main.rs                             |   6 
crates/zed/src/zed.rs                              |   6 
50 files changed, 2,207 insertions(+), 1,519 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5428,6 +5428,16 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-typescript"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "ttf-parser"
 version = "0.9.0"
@@ -6039,6 +6049,7 @@ dependencies = [
  "tree-sitter-json",
  "tree-sitter-markdown",
  "tree-sitter-rust",
+ "tree-sitter-typescript",
  "unindent",
  "url",
  "util",

crates/editor/src/editor.rs 🔗

@@ -2593,6 +2593,8 @@ impl Editor {
                     }
                 }
             }
+        } else {
+            return Ok(());
         }
 
         let mut ranges_to_highlight = Vec::new();
@@ -6451,13 +6453,12 @@ pub fn styled_runs_for_code_label<'a>(
 
 #[cfg(test)]
 mod tests {
-
     use super::*;
     use gpui::{
         geometry::rect::RectF,
         platform::{WindowBounds, WindowOptions},
     };
-    use language::{LanguageConfig, LanguageServerConfig};
+    use language::{FakeLspAdapter, LanguageConfig};
     use lsp::FakeLanguageServer;
     use project::FakeFs;
     use smol::stream::StreamExt;
@@ -8893,26 +8894,27 @@ mod tests {
         cx.foreground().forbid_parking();
         cx.update(populate_settings);
 
-        let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake();
-        language_server_config.set_fake_capabilities(lsp::ServerCapabilities {
-            document_formatting_provider: Some(lsp::OneOf::Left(true)),
-            ..Default::default()
-        });
-        let language = Arc::new(Language::new(
+        let mut language = Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
-                language_server: Some(language_server_config),
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        ));
+        );
+        let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                document_formatting_provider: Some(lsp::OneOf::Left(true)),
+                ..Default::default()
+            },
+            ..Default::default()
+        });
 
         let fs = FakeFs::new(cx.background().clone());
         fs.insert_file("/file.rs", Default::default()).await;
 
         let project = Project::test(fs, cx);
-        project.update(cx, |project, _| project.languages().add(language));
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 
         let worktree_id = project
             .update(cx, |project, cx| {
@@ -8926,7 +8928,9 @@ mod tests {
             .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
             .await
             .unwrap();
-        let mut fake_server = fake_servers.next().await.unwrap();
+
+        cx.foreground().start_waiting();
+        let fake_server = fake_servers.next().await.unwrap();
 
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
         let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx));
@@ -8940,13 +8944,14 @@ mod tests {
                     params.text_document.uri,
                     lsp::Url::from_file_path("/file.rs").unwrap()
                 );
-                Some(vec![lsp::TextEdit::new(
+                Ok(Some(vec![lsp::TextEdit::new(
                     lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
                     ", ".to_string(),
-                )])
+                )]))
             })
             .next()
             .await;
+        cx.foreground().start_waiting();
         save.await.unwrap();
         assert_eq!(
             editor.read_with(cx, |editor, cx| editor.text(cx)),
@@ -8968,6 +8973,7 @@ mod tests {
         });
         let save = cx.update(|cx| editor.save(project.clone(), cx));
         cx.foreground().advance_clock(items::FORMAT_TIMEOUT);
+        cx.foreground().start_waiting();
         save.await.unwrap();
         assert_eq!(
             editor.read_with(cx, |editor, cx| editor.text(cx)),
@@ -8980,23 +8986,24 @@ mod tests {
     async fn test_completion(cx: &mut gpui::TestAppContext) {
         cx.update(populate_settings);
 
-        let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake();
-        language_server_config.set_fake_capabilities(lsp::ServerCapabilities {
-            completion_provider: Some(lsp::CompletionOptions {
-                trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
-                ..Default::default()
-            }),
-            ..Default::default()
-        });
-        let language = Arc::new(Language::new(
+        let mut language = Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
-                language_server: Some(language_server_config),
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        ));
+        );
+        let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
+            ..Default::default()
+        });
 
         let text = "
             one
@@ -9009,7 +9016,7 @@ mod tests {
         fs.insert_file("/file.rs", text).await;
 
         let project = Project::test(fs, cx);
-        project.update(cx, |project, _| project.languages().add(language));
+        project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 
         let worktree_id = project
             .update(cx, |project, cx| {
@@ -9168,7 +9175,7 @@ mod tests {
                         params.text_document_position.position,
                         lsp::Position::new(position.row, position.column)
                     );
-                    Some(lsp::CompletionResponse::Array(
+                    Ok(Some(lsp::CompletionResponse::Array(
                         completions
                             .iter()
                             .map(|(range, new_text)| lsp::CompletionItem {
@@ -9183,7 +9190,7 @@ mod tests {
                                 ..Default::default()
                             })
                             .collect(),
-                    ))
+                    )))
                 }
             })
             .next()
@@ -9197,7 +9204,7 @@ mod tests {
             fake.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _| {
                 let edit = edit.clone();
                 async move {
-                    lsp::CompletionItem {
+                    Ok(lsp::CompletionItem {
                         additional_text_edits: edit.map(|(range, new_text)| {
                             vec![lsp::TextEdit::new(
                                 lsp::Range::new(
@@ -9208,7 +9215,7 @@ mod tests {
                             )]
                         }),
                         ..Default::default()
-                    }
+                    })
                 }
             })
             .next()

crates/language/src/buffer.rs 🔗

@@ -1,8 +1,7 @@
 pub use crate::{
     diagnostic_set::DiagnosticSet,
     highlight_map::{HighlightId, HighlightMap},
-    proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig,
-    PLAIN_TEXT,
+    proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT,
 };
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},

crates/language/src/diagnostic_set.rs 🔗

@@ -34,6 +34,23 @@ pub struct Summary {
     count: usize,
 }
 
+impl<T> DiagnosticEntry<T> {
+    // Used to provide diagnostic context to lsp codeAction request
+    pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic {
+        let code = self
+            .diagnostic
+            .code
+            .clone()
+            .map(lsp::NumberOrString::String);
+
+        lsp::Diagnostic {
+            code,
+            severity: Some(self.diagnostic.severity),
+            ..Default::default()
+        }
+    }
+}
+
 impl DiagnosticSet {
     pub fn from_sorted_entries<I>(iter: I, buffer: &text::BufferSnapshot) -> Self
     where

crates/language/src/language.rs 🔗

@@ -7,8 +7,8 @@ pub mod proto;
 mod tests;
 
 use anyhow::{anyhow, Context, Result};
-use client::http::{self, HttpClient};
-use collections::HashSet;
+use client::http::HttpClient;
+use collections::HashMap;
 use futures::{
     future::{BoxFuture, Shared},
     FutureExt, TryFutureExt,
@@ -20,6 +20,7 @@ use parking_lot::{Mutex, RwLock};
 use serde::Deserialize;
 use serde_json::Value;
 use std::{
+    any::Any,
     cell::RefCell,
     ops::Range,
     path::{Path, PathBuf},
@@ -51,7 +52,6 @@ lazy_static! {
             brackets: Default::default(),
             autoclose_before: Default::default(),
             line_comment: None,
-            language_server: None,
         },
         None,
     ));
@@ -61,20 +61,18 @@ pub trait ToLspPosition {
     fn to_lsp_position(self) -> lsp::Position;
 }
 
-pub struct LspBinaryVersion {
-    pub name: String,
-    pub url: Option<http::Url>,
-}
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct LanguageServerName(pub Arc<str>);
 
 pub trait LspAdapter: 'static + Send + Sync {
-    fn name(&self) -> &'static str;
+    fn name(&self) -> LanguageServerName;
     fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>>;
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>>;
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>>;
@@ -96,6 +94,14 @@ pub trait LspAdapter: 'static + Send + Sync {
     fn initialization_options(&self) -> Option<Value> {
         None
     }
+
+    fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
+        Default::default()
+    }
+
+    fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
+        None
+    }
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -113,7 +119,6 @@ pub struct LanguageConfig {
     #[serde(default)]
     pub autoclose_before: String,
     pub line_comment: Option<String>,
-    pub language_server: Option<LanguageServerConfig>,
 }
 
 impl Default for LanguageConfig {
@@ -124,25 +129,17 @@ impl Default for LanguageConfig {
             brackets: Default::default(),
             autoclose_before: Default::default(),
             line_comment: Default::default(),
-            language_server: Default::default(),
         }
     }
 }
 
-#[derive(Default, Deserialize)]
-pub struct LanguageServerConfig {
-    pub disk_based_diagnostic_sources: HashSet<String>,
-    pub disk_based_diagnostics_progress_token: Option<String>,
-    #[cfg(any(test, feature = "test-support"))]
-    #[serde(skip)]
-    fake_config: Option<FakeLanguageServerConfig>,
-}
-
 #[cfg(any(test, feature = "test-support"))]
-struct FakeLanguageServerConfig {
-    servers_tx: mpsc::UnboundedSender<lsp::FakeLanguageServer>,
-    capabilities: lsp::ServerCapabilities,
-    initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
+pub struct FakeLspAdapter {
+    pub name: &'static str,
+    pub capabilities: lsp::ServerCapabilities,
+    pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
+    pub disk_based_diagnostics_progress_token: Option<&'static str>,
+    pub disk_based_diagnostics_sources: &'static [&'static str],
 }
 
 #[derive(Clone, Debug, Deserialize)]
@@ -157,7 +154,12 @@ pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
     pub(crate) adapter: Option<Arc<dyn LspAdapter>>,
-    lsp_binary_path: Mutex<Option<Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>>>,
+
+    #[cfg(any(test, feature = "test-support"))]
+    fake_adapter: Option<(
+        mpsc::UnboundedSender<lsp::FakeLanguageServer>,
+        Arc<FakeLspAdapter>,
+    )>,
 }
 
 pub struct Grammar {
@@ -184,6 +186,12 @@ pub struct LanguageRegistry {
     lsp_binary_statuses_tx: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
     lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)>,
     login_shell_env_loaded: Shared<Task<()>>,
+    lsp_binary_paths: Mutex<
+        HashMap<
+            LanguageServerName,
+            Shared<BoxFuture<'static, Result<PathBuf, Arc<anyhow::Error>>>>,
+        >,
+    >,
 }
 
 impl LanguageRegistry {
@@ -195,6 +203,7 @@ impl LanguageRegistry {
             lsp_binary_statuses_tx,
             lsp_binary_statuses_rx,
             login_shell_env_loaded: login_shell_env_loaded.shared(),
+            lsp_binary_paths: Default::default(),
         }
     }
 
@@ -244,7 +253,7 @@ impl LanguageRegistry {
     }
 
     pub fn start_language_server(
-        &self,
+        self: &Arc<Self>,
         server_id: usize,
         language: Arc<Language>,
         root_path: Arc<Path>,
@@ -252,34 +261,20 @@ impl LanguageRegistry {
         cx: &mut MutableAppContext,
     ) -> Option<Task<Result<lsp::LanguageServer>>> {
         #[cfg(any(test, feature = "test-support"))]
-        if language
-            .config
-            .language_server
-            .as_ref()
-            .and_then(|config| config.fake_config.as_ref())
-            .is_some()
-        {
+        if language.fake_adapter.is_some() {
             let language = language.clone();
-            return Some(cx.spawn(|mut cx| async move {
-                let fake_config = language
-                    .config
-                    .language_server
-                    .as_ref()
-                    .unwrap()
-                    .fake_config
-                    .as_ref()
-                    .unwrap();
-                let (server, mut fake_server) = cx.update(|cx| {
-                    lsp::LanguageServer::fake_with_capabilities(
-                        fake_config.capabilities.clone(),
-                        cx,
-                    )
-                });
-                if let Some(initializer) = &fake_config.initializer {
+            return Some(cx.spawn(|cx| async move {
+                let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap();
+                let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities(
+                    fake_adapter.capabilities.clone(),
+                    cx.clone(),
+                );
+
+                if let Some(initializer) = &fake_adapter.initializer {
                     initializer(&mut fake_server);
                 }
 
-                let servers_tx = fake_config.servers_tx.clone();
+                let servers_tx = servers_tx.clone();
                 cx.background()
                     .spawn(async move {
                         fake_server
@@ -298,16 +293,17 @@ impl LanguageRegistry {
             .ok_or_else(|| anyhow!("language server download directory has not been assigned"))
             .log_err()?;
 
+        let this = self.clone();
         let adapter = language.adapter.clone()?;
-        let background = cx.background().clone();
         let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
-        Some(cx.background().spawn(async move {
+        Some(cx.spawn(|cx| async move {
             login_shell_env_loaded.await;
-            let server_binary_path = language
-                .lsp_binary_path
+            let server_binary_path = this
+                .lsp_binary_paths
                 .lock()
-                .get_or_insert_with(|| {
+                .entry(adapter.name())
+                .or_insert_with(|| {
                     get_server_binary_path(
                         adapter.clone(),
                         language.clone(),
@@ -329,8 +325,7 @@ impl LanguageRegistry {
                 &server_binary_path,
                 server_args,
                 &root_path,
-                adapter.initialization_options(),
-                background,
+                cx,
             )?;
             Ok(server)
         }))
@@ -350,7 +345,7 @@ async fn get_server_binary_path(
     download_dir: Arc<Path>,
     statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
 ) -> Result<PathBuf> {
-    let container_dir = download_dir.join(adapter.name());
+    let container_dir = download_dir.join(adapter.name().0.as_ref());
     if !container_dir.exists() {
         smol::fs::create_dir_all(&container_dir)
             .await
@@ -423,10 +418,16 @@ impl Language {
                 })
             }),
             adapter: None,
-            lsp_binary_path: Default::default(),
+
+            #[cfg(any(test, feature = "test-support"))]
+            fake_adapter: None,
         }
     }
 
+    pub fn lsp_adapter(&self) -> Option<Arc<dyn LspAdapter>> {
+        self.adapter.clone()
+    }
+
     pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
         let grammar = self
             .grammar
@@ -467,11 +468,23 @@ impl Language {
         Ok(self)
     }
 
-    pub fn with_lsp_adapter(mut self, lsp_adapter: impl LspAdapter) -> Self {
-        self.adapter = Some(Arc::new(lsp_adapter));
+    pub fn with_lsp_adapter(mut self, lsp_adapter: Arc<dyn LspAdapter>) -> Self {
+        self.adapter = Some(lsp_adapter);
         self
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn set_fake_lsp_adapter(
+        &mut self,
+        fake_lsp_adapter: FakeLspAdapter,
+    ) -> mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
+        let (servers_tx, servers_rx) = mpsc::unbounded();
+        let adapter = Arc::new(fake_lsp_adapter);
+        self.fake_adapter = Some((servers_tx, adapter.clone()));
+        self.adapter = Some(adapter);
+        servers_rx
+    }
+
     pub fn name(&self) -> Arc<str> {
         self.config.name.clone()
     }
@@ -480,18 +493,16 @@ impl Language {
         self.config.line_comment.as_deref()
     }
 
-    pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet<String>> {
-        self.config
-            .language_server
-            .as_ref()
-            .map(|config| &config.disk_based_diagnostic_sources)
+    pub fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
+        self.adapter.as_ref().map_or(&[] as &[_], |adapter| {
+            adapter.disk_based_diagnostic_sources()
+        })
     }
 
-    pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> {
-        self.config
-            .language_server
+    pub fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
+        self.adapter
             .as_ref()
-            .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref())
+            .and_then(|adapter| adapter.disk_based_diagnostics_progress_token())
     }
 
     pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) {
@@ -598,47 +609,70 @@ impl CodeLabel {
 }
 
 #[cfg(any(test, feature = "test-support"))]
-impl LanguageServerConfig {
-    pub fn fake() -> (Self, mpsc::UnboundedReceiver<lsp::FakeLanguageServer>) {
-        let (servers_tx, servers_rx) = mpsc::unbounded();
-        (
-            Self {
-                fake_config: Some(FakeLanguageServerConfig {
-                    servers_tx,
-                    capabilities: lsp::LanguageServer::full_capabilities(),
-                    initializer: None,
-                }),
-                disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()),
-                ..Default::default()
-            },
-            servers_rx,
-        )
+impl Default for FakeLspAdapter {
+    fn default() -> Self {
+        Self {
+            name: "the-fake-language-server",
+            capabilities: lsp::LanguageServer::full_capabilities(),
+            initializer: None,
+            disk_based_diagnostics_progress_token: None,
+            disk_based_diagnostics_sources: &[],
+        }
     }
+}
 
-    pub fn set_fake_capabilities(&mut self, capabilities: lsp::ServerCapabilities) {
-        self.fake_config.as_mut().unwrap().capabilities = capabilities;
+#[cfg(any(test, feature = "test-support"))]
+impl LspAdapter for FakeLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName(self.name.into())
     }
 
-    pub fn set_fake_initializer(
-        &mut self,
-        initializer: impl 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer),
-    ) {
-        self.fake_config.as_mut().unwrap().initializer = Some(Box::new(initializer));
+    fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
+        unreachable!();
     }
-}
 
-impl ToLspPosition for PointUtf16 {
-    fn to_lsp_position(self) -> lsp::Position {
-        lsp::Position::new(self.row, self.column)
+    fn fetch_server_binary(
+        &self,
+        _: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        _: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        unreachable!();
     }
+
+    fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        unreachable!();
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+
+    fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
+        self.disk_based_diagnostics_sources
+    }
+
+    fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
+        self.disk_based_diagnostics_progress_token
+    }
+}
+
+pub fn point_to_lsp(point: PointUtf16) -> lsp::Position {
+    lsp::Position::new(point.row, point.column)
 }
 
 pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 {
     PointUtf16::new(point.line, point.character)
 }
 
+pub fn range_to_lsp(range: Range<PointUtf16>) -> lsp::Range {
+    lsp::Range {
+        start: point_to_lsp(range.start),
+        end: point_to_lsp(range.end),
+    }
+}
+
 pub fn range_from_lsp(range: lsp::Range) -> Range<PointUtf16> {
-    let start = PointUtf16::new(range.start.line, range.start.character);
-    let end = PointUtf16::new(range.end.line, range.end.character);
-    start..end
+    point_from_lsp(range.start)..point_from_lsp(range.end)
 }

crates/language/src/tests.rs 🔗

@@ -931,7 +931,6 @@ fn rust_lang() -> Language {
         LanguageConfig {
             name: "Rust".into(),
             path_suffixes: vec!["rs".to_string()],
-            language_server: None,
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),

crates/lsp/src/lsp.rs 🔗

@@ -1,15 +1,17 @@
+pub use lsp_types::*;
+
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
 use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite};
-use gpui::{executor, Task};
-use parking_lot::{Mutex, RwLock};
+use gpui::{executor, AsyncAppContext, Task};
+use parking_lot::Mutex;
 use postage::{barrier, prelude::Stream};
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use serde_json::{json, value::RawValue, Value};
 use smol::{
     channel,
     io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
-    process::Command,
+    process,
 };
 use std::{
     future::Future,
@@ -22,15 +24,12 @@ use std::{
     },
 };
 use std::{path::Path, process::Stdio};
-use util::TryFutureExt;
-
-pub use lsp_types::*;
+use util::{ResultExt, TryFutureExt};
 
 const JSON_RPC_VERSION: &'static str = "2.0";
 const CONTENT_LEN_HEADER: &'static str = "Content-Length: ";
 
-type NotificationHandler =
-    Box<dyn Send + Sync + FnMut(Option<usize>, &str, &mut channel::Sender<Vec<u8>>) -> Result<()>>;
+type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppContext)>;
 type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
 
 pub struct LanguageServer {
@@ -39,18 +38,17 @@ pub struct LanguageServer {
     outbound_tx: channel::Sender<Vec<u8>>,
     name: String,
     capabilities: ServerCapabilities,
-    notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
+    notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
     executor: Arc<executor::Background>,
     io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
     output_done_rx: Mutex<Option<barrier::Receiver>>,
     root_path: PathBuf,
-    options: Option<Value>,
 }
 
 pub struct Subscription {
     method: &'static str,
-    notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
+    notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -61,18 +59,6 @@ struct Request<'a, T> {
     params: T,
 }
 
-#[cfg(any(test, feature = "test-support"))]
-#[derive(Deserialize)]
-struct AnyRequest<'a> {
-    id: usize,
-    #[serde(borrow)]
-    jsonrpc: &'a str,
-    #[serde(borrow)]
-    method: &'a str,
-    #[serde(borrow)]
-    params: &'a RawValue,
-}
-
 #[derive(Serialize, Deserialize)]
 struct AnyResponse<'a> {
     id: usize,
@@ -85,7 +71,8 @@ struct AnyResponse<'a> {
 #[derive(Serialize)]
 struct Response<T> {
     id: usize,
-    result: T,
+    result: Option<T>,
+    error: Option<Error>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -118,15 +105,14 @@ impl LanguageServer {
         binary_path: &Path,
         args: &[&str],
         root_path: &Path,
-        options: Option<Value>,
-        background: Arc<executor::Background>,
+        cx: AsyncAppContext,
     ) -> Result<Self> {
         let working_dir = if root_path.is_dir() {
             root_path
         } else {
             root_path.parent().unwrap_or(Path::new("/"))
         };
-        let mut server = Command::new(binary_path)
+        let mut server = process::Command::new(binary_path)
             .current_dir(working_dir)
             .args(args)
             .stdin(Stdio::piped())
@@ -136,99 +122,97 @@ impl LanguageServer {
         let stdin = server.stdin.take().unwrap();
         let stdout = server.stdout.take().unwrap();
         let mut server =
-            Self::new_internal(server_id, stdin, stdout, root_path, options, background);
+            Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| {
+                log::info!(
+                    "unhandled notification {}:\n{}",
+                    notification.method,
+                    serde_json::to_string_pretty(
+                        &Value::from_str(notification.params.get()).unwrap()
+                    )
+                    .unwrap()
+                );
+            });
         if let Some(name) = binary_path.file_name() {
             server.name = name.to_string_lossy().to_string();
         }
         Ok(server)
     }
 
-    fn new_internal<Stdin, Stdout>(
+    fn new_internal<Stdin, Stdout, F>(
         server_id: usize,
         stdin: Stdin,
         stdout: Stdout,
         root_path: &Path,
-        options: Option<Value>,
-        executor: Arc<executor::Background>,
+        cx: AsyncAppContext,
+        mut on_unhandled_notification: F,
     ) -> Self
     where
         Stdin: AsyncWrite + Unpin + Send + 'static,
         Stdout: AsyncRead + Unpin + Send + 'static,
+        F: FnMut(AnyNotification) + 'static + Send,
     {
         let mut stdin = BufWriter::new(stdin);
         let mut stdout = BufReader::new(stdout);
         let (outbound_tx, outbound_rx) = channel::unbounded::<Vec<u8>>();
         let notification_handlers =
-            Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::default()));
+            Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
         let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::default()));
-        let input_task = executor.spawn(
-            {
-                let notification_handlers = notification_handlers.clone();
-                let response_handlers = response_handlers.clone();
-                let mut outbound_tx = outbound_tx.clone();
-                async move {
-                    let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone());
-                    let mut buffer = Vec::new();
-                    loop {
-                        buffer.clear();
-                        stdout.read_until(b'\n', &mut buffer).await?;
-                        stdout.read_until(b'\n', &mut buffer).await?;
-                        let message_len: usize = std::str::from_utf8(&buffer)?
-                            .strip_prefix(CONTENT_LEN_HEADER)
-                            .ok_or_else(|| anyhow!("invalid header"))?
-                            .trim_end()
-                            .parse()?;
-
-                        buffer.resize(message_len, 0);
-                        stdout.read_exact(&mut buffer).await?;
-
-                        if let Ok(AnyNotification { id, method, params }) =
-                            serde_json::from_slice(&buffer)
-                        {
-                            if let Some(handler) = notification_handlers.write().get_mut(method) {
-                                if let Err(e) = handler(id, params.get(), &mut outbound_tx) {
-                                    log::error!("error handling {} message: {:?}", method, e);
-                                }
+        let input_task = cx.spawn(|cx| {
+            let notification_handlers = notification_handlers.clone();
+            let response_handlers = response_handlers.clone();
+            async move {
+                let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone());
+                let mut buffer = Vec::new();
+                loop {
+                    buffer.clear();
+                    stdout.read_until(b'\n', &mut buffer).await?;
+                    stdout.read_until(b'\n', &mut buffer).await?;
+                    let message_len: usize = std::str::from_utf8(&buffer)?
+                        .strip_prefix(CONTENT_LEN_HEADER)
+                        .ok_or_else(|| anyhow!("invalid header"))?
+                        .trim_end()
+                        .parse()?;
+
+                    buffer.resize(message_len, 0);
+                    stdout.read_exact(&mut buffer).await?;
+                    log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer));
+
+                    if let Ok(msg) = serde_json::from_slice::<AnyNotification>(&buffer) {
+                        if let Some(handler) = notification_handlers.lock().get_mut(msg.method) {
+                            handler(msg.id, msg.params.get(), cx.clone());
+                        } else {
+                            on_unhandled_notification(msg);
+                        }
+                    } else if let Ok(AnyResponse { id, error, result }) =
+                        serde_json::from_slice(&buffer)
+                    {
+                        if let Some(handler) = response_handlers.lock().remove(&id) {
+                            if let Some(error) = error {
+                                handler(Err(error));
+                            } else if let Some(result) = result {
+                                handler(Ok(result.get()));
                             } else {
-                                log::info!(
-                                    "unhandled notification {}:\n{}",
-                                    method,
-                                    serde_json::to_string_pretty(
-                                        &Value::from_str(params.get()).unwrap()
-                                    )
-                                    .unwrap()
-                                );
-                            }
-                        } else if let Ok(AnyResponse { id, error, result }) =
-                            serde_json::from_slice(&buffer)
-                        {
-                            if let Some(handler) = response_handlers.lock().remove(&id) {
-                                if let Some(error) = error {
-                                    handler(Err(error));
-                                } else if let Some(result) = result {
-                                    handler(Ok(result.get()));
-                                } else {
-                                    handler(Ok("null"));
-                                }
+                                handler(Ok("null"));
                             }
-                        } else {
-                            return Err(anyhow!(
-                                "failed to deserialize message:\n{}",
-                                std::str::from_utf8(&buffer)?
-                            ));
                         }
+                    } else {
+                        return Err(anyhow!(
+                            "failed to deserialize message:\n{}",
+                            std::str::from_utf8(&buffer)?
+                        ));
                     }
                 }
             }
-            .log_err(),
-        );
+            .log_err()
+        });
         let (output_done_tx, output_done_rx) = barrier::channel();
-        let output_task = executor.spawn({
+        let output_task = cx.background().spawn({
             let response_handlers = response_handlers.clone();
             async move {
                 let _clear_response_handlers = ClearResponseHandlers(response_handlers);
                 let mut content_len_buffer = Vec::new();
                 while let Ok(message) = outbound_rx.recv().await {
+                    log::trace!("outgoing message:{}", String::from_utf8_lossy(&message));
                     content_len_buffer.clear();
                     write!(content_len_buffer, "{}", message.len()).unwrap();
                     stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
@@ -251,18 +235,15 @@ impl LanguageServer {
             capabilities: Default::default(),
             next_id: Default::default(),
             outbound_tx,
-            executor: executor.clone(),
+            executor: cx.background().clone(),
             io_tasks: Mutex::new(Some((input_task, output_task))),
             output_done_rx: Mutex::new(Some(output_done_rx)),
             root_path: root_path.to_path_buf(),
-            options,
         }
     }
 
-    pub async fn initialize(mut self) -> Result<Arc<Self>> {
-        let options = self.options.take();
-        let mut this = Arc::new(self);
-        let root_uri = Url::from_file_path(&this.root_path).unwrap();
+    pub async fn initialize(mut self, options: Option<Value>) -> Result<Arc<Self>> {
+        let root_uri = Url::from_file_path(&self.root_path).unwrap();
         #[allow(deprecated)]
         let params = InitializeParams {
             process_id: Default::default(),
@@ -288,12 +269,13 @@ impl LanguageServer {
                                 value_set: vec![
                                     CodeActionKind::REFACTOR.as_str().into(),
                                     CodeActionKind::QUICKFIX.as_str().into(),
+                                    CodeActionKind::SOURCE.as_str().into(),
                                 ],
                             },
                         }),
                         data_support: Some(true),
                         resolve_support: Some(CodeActionCapabilityResolveSupport {
-                            properties: vec!["edit".to_string()],
+                            properties: vec!["edit".to_string(), "command".to_string()],
                         }),
                         ..Default::default()
                     }),
@@ -324,16 +306,14 @@ impl LanguageServer {
             locale: Default::default(),
         };
 
-        let response = this.request::<request::Initialize>(params).await?;
-        {
-            let this = Arc::get_mut(&mut this).unwrap();
-            if let Some(info) = response.server_info {
-                this.name = info.name;
-            }
-            this.capabilities = response.capabilities;
+        let response = self.request::<request::Initialize>(params).await?;
+        if let Some(info) = response.server_info {
+            self.name = info.name;
         }
-        this.notify::<notification::Initialized>(InitializedParams {})?;
-        Ok(this)
+        self.capabilities = response.capabilities;
+
+        self.notify::<notification::Initialized>(InitializedParams {})?;
+        Ok(Arc::new(self))
     }
 
     pub fn shutdown(&self) -> Option<impl 'static + Send + Future<Output = Option<()>>> {
@@ -368,37 +348,42 @@ impl LanguageServer {
         }
     }
 
-    pub fn on_notification<T, F>(&mut self, f: F) -> Subscription
+    #[must_use]
+    pub fn on_notification<T, F>(&self, f: F) -> Subscription
     where
         T: notification::Notification,
-        F: 'static + Send + Sync + FnMut(T::Params),
+        F: 'static + Send + FnMut(T::Params, AsyncAppContext),
     {
         self.on_custom_notification(T::METHOD, f)
     }
 
-    pub fn on_request<T, F>(&mut self, f: F) -> Subscription
+    #[must_use]
+    pub fn on_request<T, F, Fut>(&self, f: F) -> Subscription
     where
         T: request::Request,
-        F: 'static + Send + Sync + FnMut(T::Params) -> Result<T::Result>,
+        T::Params: 'static + Send,
+        F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = Result<T::Result>>,
     {
         self.on_custom_request(T::METHOD, f)
     }
 
-    pub fn on_custom_notification<Params, F>(
-        &mut self,
-        method: &'static str,
-        mut f: F,
-    ) -> Subscription
+    pub fn remove_request_handler<T: request::Request>(&self) {
+        self.notification_handlers.lock().remove(T::METHOD);
+    }
+
+    #[must_use]
+    pub fn on_custom_notification<Params, F>(&self, method: &'static str, mut f: F) -> Subscription
     where
-        F: 'static + Send + Sync + FnMut(Params),
+        F: 'static + Send + FnMut(Params, AsyncAppContext),
         Params: DeserializeOwned,
     {
-        let prev_handler = self.notification_handlers.write().insert(
+        let prev_handler = self.notification_handlers.lock().insert(
             method,
-            Box::new(move |_, params, _| {
-                let params = serde_json::from_str(params)?;
-                f(params);
-                Ok(())
+            Box::new(move |_, params, cx| {
+                if let Some(params) = serde_json::from_str(params).log_err() {
+                    f(params, cx);
+                }
             }),
         );
         assert!(
@@ -411,26 +396,52 @@ impl LanguageServer {
         }
     }
 
-    pub fn on_custom_request<Params, Res, F>(
-        &mut self,
+    #[must_use]
+    pub fn on_custom_request<Params, Res, Fut, F>(
+        &self,
         method: &'static str,
         mut f: F,
     ) -> Subscription
     where
-        F: 'static + Send + Sync + FnMut(Params) -> Result<Res>,
-        Params: DeserializeOwned,
+        F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut,
+        Fut: 'static + Future<Output = Result<Res>>,
+        Params: DeserializeOwned + Send + 'static,
         Res: Serialize,
     {
-        let prev_handler = self.notification_handlers.write().insert(
+        let outbound_tx = self.outbound_tx.clone();
+        let prev_handler = self.notification_handlers.lock().insert(
             method,
-            Box::new(move |id, params, tx| {
+            Box::new(move |id, params, cx| {
                 if let Some(id) = id {
-                    let params = serde_json::from_str(params)?;
-                    let result = f(params)?;
-                    let response = serde_json::to_vec(&Response { id, result })?;
-                    tx.try_send(response)?;
+                    if let Some(params) = serde_json::from_str(params).log_err() {
+                        let response = f(params, cx.clone());
+                        cx.foreground()
+                            .spawn({
+                                let outbound_tx = outbound_tx.clone();
+                                async move {
+                                    let response = match response.await {
+                                        Ok(result) => Response {
+                                            id,
+                                            result: Some(result),
+                                            error: None,
+                                        },
+                                        Err(error) => Response {
+                                            id,
+                                            result: None,
+                                            error: Some(Error {
+                                                message: error.to_string(),
+                                            }),
+                                        },
+                                    };
+                                    if let Some(response) = serde_json::to_vec(&response).log_err()
+                                    {
+                                        outbound_tx.try_send(response).ok();
+                                    }
+                                }
+                            })
+                            .detach();
+                    }
                 }
-                Ok(())
             }),
         );
         assert!(
@@ -456,7 +467,7 @@ impl LanguageServer {
     }
 
     pub fn request<T: request::Request>(
-        self: &Arc<Self>,
+        &self,
         params: T::Params,
     ) -> impl Future<Output = Result<T::Result>>
     where
@@ -547,36 +558,17 @@ impl Subscription {
 
 impl Drop for Subscription {
     fn drop(&mut self) {
-        self.notification_handlers.write().remove(self.method);
+        self.notification_handlers.lock().remove(self.method);
     }
 }
 
 #[cfg(any(test, feature = "test-support"))]
+#[derive(Clone)]
 pub struct FakeLanguageServer {
-    handlers: FakeLanguageServerHandlers,
-    outgoing_tx: futures::channel::mpsc::UnboundedSender<Vec<u8>>,
-    incoming_rx: futures::channel::mpsc::UnboundedReceiver<Vec<u8>>,
-    _input_task: Task<Result<()>>,
-    _output_task: Task<Result<()>>,
+    pub server: Arc<LanguageServer>,
+    notifications_rx: channel::Receiver<(String, String)>,
 }
 
-#[cfg(any(test, feature = "test-support"))]
-type FakeLanguageServerHandlers = Arc<
-    Mutex<
-        HashMap<
-            &'static str,
-            Box<
-                dyn Send
-                    + FnMut(
-                        usize,
-                        &[u8],
-                        gpui::AsyncAppContext,
-                    ) -> futures::future::BoxFuture<'static, Vec<u8>>,
-            >,
-        >,
-    >,
->;
-
 #[cfg(any(test, feature = "test-support"))]
 impl LanguageServer {
     pub fn full_capabilities() -> ServerCapabilities {
@@ -589,177 +581,101 @@ impl LanguageServer {
         }
     }
 
-    pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) {
+    pub fn fake(cx: AsyncAppContext) -> (Self, FakeLanguageServer) {
         Self::fake_with_capabilities(Self::full_capabilities(), cx)
     }
 
     pub fn fake_with_capabilities(
         capabilities: ServerCapabilities,
-        cx: &mut gpui::MutableAppContext,
+        cx: AsyncAppContext,
     ) -> (Self, FakeLanguageServer) {
         let (stdin_writer, stdin_reader) = async_pipe::pipe();
         let (stdout_writer, stdout_reader) = async_pipe::pipe();
+        let (notifications_tx, notifications_rx) = channel::unbounded();
 
-        let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx);
+        let server = Self::new_internal(
+            0,
+            stdin_writer,
+            stdout_reader,
+            Path::new("/"),
+            cx.clone(),
+            |_| {},
+        );
+        let fake = FakeLanguageServer {
+            server: Arc::new(Self::new_internal(
+                0,
+                stdout_writer,
+                stdin_reader,
+                Path::new("/"),
+                cx.clone(),
+                move |msg| {
+                    notifications_tx
+                        .try_send((msg.method.to_string(), msg.params.get().to_string()))
+                        .ok();
+                },
+            )),
+            notifications_rx,
+        };
         fake.handle_request::<request::Initialize, _, _>({
             let capabilities = capabilities.clone();
             move |_, _| {
                 let capabilities = capabilities.clone();
                 async move {
-                    InitializeResult {
+                    Ok(InitializeResult {
                         capabilities,
                         ..Default::default()
-                    }
+                    })
                 }
             }
         });
 
-        let executor = cx.background().clone();
-        let server = Self::new_internal(
-            0,
-            stdin_writer,
-            stdout_reader,
-            Path::new("/"),
-            None,
-            executor,
-        );
         (server, fake)
     }
 }
 
 #[cfg(any(test, feature = "test-support"))]
 impl FakeLanguageServer {
-    fn new(
-        stdin: async_pipe::PipeReader,
-        stdout: async_pipe::PipeWriter,
-        cx: &mut gpui::MutableAppContext,
-    ) -> Self {
-        use futures::StreamExt as _;
-
-        let (incoming_tx, incoming_rx) = futures::channel::mpsc::unbounded();
-        let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded();
-        let handlers = FakeLanguageServerHandlers::default();
-
-        let input_task = cx.spawn(|cx| {
-            let handlers = handlers.clone();
-            let outgoing_tx = outgoing_tx.clone();
-            async move {
-                let mut buffer = Vec::new();
-                let mut stdin = smol::io::BufReader::new(stdin);
-                while Self::receive(&mut stdin, &mut buffer).await.is_ok() {
-                    cx.background().simulate_random_delay().await;
-
-                    if let Ok(request) = serde_json::from_slice::<AnyRequest>(&buffer) {
-                        assert_eq!(request.jsonrpc, JSON_RPC_VERSION);
-
-                        let response;
-                        if let Some(handler) = handlers.lock().get_mut(request.method) {
-                            response =
-                                handler(request.id, request.params.get().as_bytes(), cx.clone())
-                                    .await;
-                            log::debug!("handled lsp request. method:{}", request.method);
-                        } else {
-                            response = serde_json::to_vec(&AnyResponse {
-                                id: request.id,
-                                error: Some(Error {
-                                    message: "no handler".to_string(),
-                                }),
-                                result: None,
-                            })
-                            .unwrap();
-                            log::debug!("unhandled lsp request. method:{}", request.method);
-                        }
-                        outgoing_tx.unbounded_send(response)?;
-                    } else {
-                        incoming_tx.unbounded_send(buffer.clone())?;
-                    }
-                }
-                Ok::<_, anyhow::Error>(())
-            }
-        });
-
-        let output_task = cx.background().spawn(async move {
-            let mut stdout = smol::io::BufWriter::new(stdout);
-            while let Some(message) = outgoing_rx.next().await {
-                stdout.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
-                stdout
-                    .write_all((format!("{}", message.len())).as_bytes())
-                    .await?;
-                stdout.write_all("\r\n\r\n".as_bytes()).await?;
-                stdout.write_all(&message).await?;
-                stdout.flush().await?;
-            }
-            Ok(())
-        });
-
-        Self {
-            outgoing_tx,
-            incoming_rx,
-            handlers,
-            _input_task: input_task,
-            _output_task: output_task,
-        }
-    }
-
-    pub fn notify<T: notification::Notification>(&mut self, params: T::Params) {
-        let message = serde_json::to_vec(&Notification {
-            jsonrpc: JSON_RPC_VERSION,
-            method: T::METHOD,
-            params,
-        })
-        .unwrap();
-        self.outgoing_tx.unbounded_send(message).unwrap();
+    pub fn notify<T: notification::Notification>(&self, params: T::Params) {
+        self.server.notify::<T>(params).ok();
     }
 
     pub async fn receive_notification<T: notification::Notification>(&mut self) -> T::Params {
         use futures::StreamExt as _;
 
         loop {
-            let bytes = self.incoming_rx.next().await.unwrap();
-            if let Ok(notification) = serde_json::from_slice::<Notification<T::Params>>(&bytes) {
-                assert_eq!(notification.method, T::METHOD);
-                return notification.params;
+            let (method, params) = self.notifications_rx.next().await.unwrap();
+            if &method == T::METHOD {
+                return serde_json::from_str::<T::Params>(&params).unwrap();
             } else {
-                log::info!(
-                    "skipping message in fake language server {:?}",
-                    std::str::from_utf8(&bytes)
-                );
+                log::info!("skipping message in fake language server {:?}", params);
             }
         }
     }
 
     pub fn handle_request<T, F, Fut>(
-        &mut self,
+        &self,
         mut handler: F,
     ) -> futures::channel::mpsc::UnboundedReceiver<()>
     where
         T: 'static + request::Request,
+        T::Params: 'static + Send,
         F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut,
-        Fut: 'static + Send + Future<Output = T::Result>,
+        Fut: 'static + Send + Future<Output = Result<T::Result>>,
     {
-        use futures::FutureExt as _;
-
         let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded();
-        self.handlers.lock().insert(
-            T::METHOD,
-            Box::new(move |id, params, cx| {
-                let result = handler(serde_json::from_slice::<T::Params>(params).unwrap(), cx);
+        self.server.remove_request_handler::<T>();
+        self.server
+            .on_request::<T, _, _>(move |params, cx| {
+                let result = handler(params, cx.clone());
                 let responded_tx = responded_tx.clone();
                 async move {
+                    cx.background().simulate_random_delay().await;
                     let result = result.await;
-                    let result = serde_json::to_string(&result).unwrap();
-                    let result = serde_json::from_str::<&RawValue>(&result).unwrap();
-                    let response = AnyResponse {
-                        id,
-                        error: None,
-                        result: Some(result),
-                    };
                     responded_tx.unbounded_send(()).ok();
-                    serde_json::to_vec(&response).unwrap()
+                    result
                 }
-                .boxed()
-            }),
-        );
+            })
+            .detach();
         responded_rx
     }
 
@@ -767,7 +683,7 @@ impl FakeLanguageServer {
     where
         T: 'static + request::Request,
     {
-        self.handlers.lock().remove(T::METHOD);
+        self.server.remove_request_handler::<T>();
     }
 
     pub async fn start_progress(&mut self, token: impl Into<String>) {
@@ -783,25 +699,6 @@ impl FakeLanguageServer {
             value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())),
         });
     }
-
-    async fn receive(
-        stdin: &mut smol::io::BufReader<async_pipe::PipeReader>,
-        buffer: &mut Vec<u8>,
-    ) -> Result<()> {
-        buffer.clear();
-        stdin.read_until(b'\n', buffer).await?;
-        stdin.read_until(b'\n', buffer).await?;
-        let message_len: usize = std::str::from_utf8(buffer)
-            .unwrap()
-            .strip_prefix(CONTENT_LEN_HEADER)
-            .ok_or_else(|| anyhow!("invalid content length header"))?
-            .trim_end()
-            .parse()
-            .unwrap();
-        buffer.resize(message_len, 0);
-        stdin.read_exact(buffer).await?;
-        Ok(())
-    }
 }
 
 struct ClearResponseHandlers(Arc<Mutex<HashMap<usize, ResponseHandler>>>);
@@ -826,22 +723,22 @@ mod tests {
 
     #[gpui::test]
     async fn test_fake(cx: &mut TestAppContext) {
-        let (mut server, mut fake) = cx.update(LanguageServer::fake);
+        let (server, mut fake) = LanguageServer::fake(cx.to_async());
 
         let (message_tx, message_rx) = channel::unbounded();
         let (diagnostics_tx, diagnostics_rx) = channel::unbounded();
         server
-            .on_notification::<notification::ShowMessage, _>(move |params| {
+            .on_notification::<notification::ShowMessage, _>(move |params, _| {
                 message_tx.try_send(params).unwrap()
             })
             .detach();
         server
-            .on_notification::<notification::PublishDiagnostics, _>(move |params| {
+            .on_notification::<notification::PublishDiagnostics, _>(move |params, _| {
                 diagnostics_tx.try_send(params).unwrap()
             })
             .detach();
 
-        let server = server.initialize().await.unwrap();
+        let server = server.initialize(None).await.unwrap();
         server
             .notify::<notification::DidOpenTextDocument>(DidOpenTextDocumentParams {
                 text_document: TextDocumentItem::new(
@@ -876,7 +773,7 @@ mod tests {
             "file://b/c"
         );
 
-        fake.handle_request::<request::Shutdown, _, _>(|_, _| async move {});
+        fake.handle_request::<request::Shutdown, _, _>(|_, _| async move { Ok(()) });
 
         drop(server);
         fake.receive_notification::<notification::Exit>().await;

crates/project/src/lsp_command.rs 🔗

@@ -4,9 +4,9 @@ use async_trait::async_trait;
 use client::{proto, PeerId};
 use gpui::{AppContext, AsyncAppContext, ModelHandle};
 use language::{
-    point_from_lsp,
+    point_from_lsp, point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
-    range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16,
+    range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16,
 };
 use lsp::{DocumentHighlightKind, ServerCapabilities};
 use std::{cmp::Reverse, ops::Range, path::Path};
@@ -91,7 +91,7 @@ impl LspCommand for PrepareRename {
             text_document: lsp::TextDocumentIdentifier {
                 uri: lsp::Url::from_file_path(path).unwrap(),
             },
-            position: self.position.to_lsp_position(),
+            position: point_to_lsp(self.position),
         }
     }
 
@@ -208,7 +208,7 @@ impl LspCommand for PerformRename {
                 text_document: lsp::TextDocumentIdentifier {
                     uri: lsp::Url::from_file_path(path).unwrap(),
                 },
-                position: self.position.to_lsp_position(),
+                position: point_to_lsp(self.position),
             },
             new_name: self.new_name.clone(),
             work_done_progress_params: Default::default(),
@@ -223,22 +223,19 @@ impl LspCommand for PerformRename {
         mut cx: AsyncAppContext,
     ) -> Result<ProjectTransaction> {
         if let Some(edit) = message {
-            let language_server = project
+            let (lsp_adapter, lsp_server) = project
                 .read_with(&cx, |project, cx| {
                     project
                         .language_server_for_buffer(buffer.read(cx), cx)
                         .cloned()
                 })
                 .ok_or_else(|| anyhow!("no language server found for buffer"))?;
-            let language = buffer
-                .read_with(&cx, |buffer, _| buffer.language().cloned())
-                .ok_or_else(|| anyhow!("no language for buffer"))?;
             Project::deserialize_workspace_edit(
                 project,
                 edit,
                 self.push_to_history,
-                language.name(),
-                language_server,
+                lsp_adapter,
+                lsp_server,
                 &mut cx,
             )
             .await
@@ -328,7 +325,7 @@ impl LspCommand for GetDefinition {
                 text_document: lsp::TextDocumentIdentifier {
                     uri: lsp::Url::from_file_path(path).unwrap(),
                 },
-                position: self.position.to_lsp_position(),
+                position: point_to_lsp(self.position),
             },
             work_done_progress_params: Default::default(),
             partial_result_params: Default::default(),
@@ -343,16 +340,13 @@ impl LspCommand for GetDefinition {
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Location>> {
         let mut definitions = Vec::new();
-        let language_server = project
+        let (lsp_adapter, language_server) = project
             .read_with(&cx, |project, cx| {
                 project
                     .language_server_for_buffer(buffer.read(cx), cx)
                     .cloned()
             })
             .ok_or_else(|| anyhow!("no language server found for buffer"))?;
-        let language = buffer
-            .read_with(&cx, |buffer, _| buffer.language().cloned())
-            .ok_or_else(|| anyhow!("no language for buffer"))?;
 
         if let Some(message) = message {
             let mut unresolved_locations = Vec::new();
@@ -377,7 +371,7 @@ impl LspCommand for GetDefinition {
                     .update(&mut cx, |this, cx| {
                         this.open_local_buffer_via_lsp(
                             target_uri,
-                            language.name(),
+                            lsp_adapter.clone(),
                             language_server.clone(),
                             cx,
                         )
@@ -503,7 +497,7 @@ impl LspCommand for GetReferences {
                 text_document: lsp::TextDocumentIdentifier {
                     uri: lsp::Url::from_file_path(path).unwrap(),
                 },
-                position: self.position.to_lsp_position(),
+                position: point_to_lsp(self.position),
             },
             work_done_progress_params: Default::default(),
             partial_result_params: Default::default(),
@@ -521,16 +515,13 @@ impl LspCommand for GetReferences {
         mut cx: AsyncAppContext,
     ) -> Result<Vec<Location>> {
         let mut references = Vec::new();
-        let language_server = project
+        let (lsp_adapter, language_server) = project
             .read_with(&cx, |project, cx| {
                 project
                     .language_server_for_buffer(buffer.read(cx), cx)
                     .cloned()
             })
             .ok_or_else(|| anyhow!("no language server found for buffer"))?;
-        let language = buffer
-            .read_with(&cx, |buffer, _| buffer.language().cloned())
-            .ok_or_else(|| anyhow!("no language for buffer"))?;
 
         if let Some(locations) = locations {
             for lsp_location in locations {
@@ -538,7 +529,7 @@ impl LspCommand for GetReferences {
                     .update(&mut cx, |this, cx| {
                         this.open_local_buffer_via_lsp(
                             lsp_location.uri,
-                            language.name(),
+                            lsp_adapter.clone(),
                             language_server.clone(),
                             cx,
                         )
@@ -668,7 +659,7 @@ impl LspCommand for GetDocumentHighlights {
                 text_document: lsp::TextDocumentIdentifier {
                     uri: lsp::Url::from_file_path(path).unwrap(),
                 },
-                position: self.position.to_lsp_position(),
+                position: point_to_lsp(self.position),
             },
             work_done_progress_params: Default::default(),
             partial_result_params: Default::default(),

crates/project/src/project.rs 🔗

@@ -15,11 +15,12 @@ use gpui::{
     MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
 };
 use language::{
+    point_to_lsp,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
-    range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic,
-    DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry,
-    LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition,
-    ToOffset, ToPointUtf16, Transaction,
+    range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion,
+    Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language,
+    LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch,
+    PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction,
 };
 use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer};
 use lsp_command::*;
@@ -57,10 +58,13 @@ pub struct Project {
     worktrees: Vec<WorktreeHandle>,
     active_entry: Option<ProjectEntryId>,
     languages: Arc<LanguageRegistry>,
-    language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
-    started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
+    language_servers:
+        HashMap<(WorktreeId, LanguageServerName), (Arc<dyn LspAdapter>, Arc<LanguageServer>)>,
+    started_language_servers:
+        HashMap<(WorktreeId, LanguageServerName), Task<Option<Arc<LanguageServer>>>>,
     language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
     language_server_settings: Arc<Mutex<serde_json::Value>>,
+    last_workspace_edits_by_language_server: HashMap<usize, ProjectTransaction>,
     next_language_server_id: usize,
     client: Arc<client::Client>,
     next_entry_id: Arc<AtomicUsize>,
@@ -128,20 +132,6 @@ pub enum Event {
     CollaboratorLeft(PeerId),
 }
 
-enum LanguageServerEvent {
-    WorkStart {
-        token: String,
-    },
-    WorkProgress {
-        token: String,
-        progress: LanguageServerProgress,
-    },
-    WorkEnd {
-        token: String,
-    },
-    DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
-}
-
 pub struct LanguageServerStatus {
     pub name: String,
     pub pending_work: BTreeMap<String, LanguageServerProgress>,
@@ -185,7 +175,7 @@ pub struct DocumentHighlight {
 pub struct Symbol {
     pub source_worktree_id: WorktreeId,
     pub worktree_id: WorktreeId,
-    pub language_name: String,
+    pub language_server_name: LanguageServerName,
     pub path: PathBuf,
     pub label: CodeLabel,
     pub name: String,
@@ -343,6 +333,7 @@ impl Project {
                 language_servers: Default::default(),
                 started_language_servers: Default::default(),
                 language_server_statuses: Default::default(),
+                last_workspace_edits_by_language_server: Default::default(),
                 language_server_settings: Default::default(),
                 next_language_server_id: 0,
                 nonce: StdRng::from_entropy().gen(),
@@ -430,6 +421,7 @@ impl Project {
                         )
                     })
                     .collect(),
+                last_workspace_edits_by_language_server: Default::default(),
                 next_language_server_id: 0,
                 opened_buffers: Default::default(),
                 buffer_snapshots: Default::default(),
@@ -958,8 +950,8 @@ impl Project {
     fn open_local_buffer_via_lsp(
         &mut self,
         abs_path: lsp::Url,
-        lang_name: Arc<str>,
-        lang_server: Arc<LanguageServer>,
+        lsp_adapter: Arc<dyn LspAdapter>,
+        lsp_server: Arc<LanguageServer>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Buffer>>> {
         cx.spawn(|this, mut cx| async move {
@@ -977,8 +969,10 @@ impl Project {
                     })
                     .await?;
                 this.update(&mut cx, |this, cx| {
-                    this.language_servers
-                        .insert((worktree.read(cx).id(), lang_name), lang_server);
+                    this.language_servers.insert(
+                        (worktree.read(cx).id(), lsp_adapter.name()),
+                        (lsp_adapter, lsp_server),
+                    );
                 });
                 (worktree, PathBuf::new())
             };
@@ -1121,7 +1115,7 @@ impl Project {
                     }
                 }
 
-                if let Some(server) = language_server {
+                if let Some((_, server)) = language_server {
                     server
                         .notify::<lsp::notification::DidOpenTextDocument>(
                             lsp::DidOpenTextDocumentParams {
@@ -1154,7 +1148,7 @@ impl Project {
                     if let Some(file) = File::from_dyn(buffer.file()) {
                         if file.is_local() {
                             let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
-                            if let Some(server) = this.language_server_for_buffer(buffer, cx) {
+                            if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) {
                                 server
                                     .notify::<lsp::notification::DidCloseTextDocument>(
                                         lsp::DidCloseTextDocumentParams {
@@ -1190,7 +1184,7 @@ impl Project {
                 cx.background().spawn(request).detach_and_log_err(cx);
             }
             BufferEvent::Edited { .. } => {
-                let language_server = self
+                let (_, language_server) = self
                     .language_server_for_buffer(buffer.read(cx), cx)?
                     .clone();
                 let buffer = buffer.read(cx);
@@ -1212,8 +1206,8 @@ impl Project {
                             .collect();
                         lsp::TextDocumentContentChangeEvent {
                             range: Some(lsp::Range::new(
-                                edit_start.to_lsp_position(),
-                                edit_end.to_lsp_position(),
+                                point_to_lsp(edit_start),
+                                point_to_lsp(edit_end),
                             )),
                             range_length: None,
                             text: new_text,
@@ -1263,11 +1257,11 @@ impl Project {
     fn language_servers_for_worktree(
         &self,
         worktree_id: WorktreeId,
-    ) -> impl Iterator<Item = (&str, &Arc<LanguageServer>)> {
+    ) -> impl Iterator<Item = &(Arc<dyn LspAdapter>, Arc<LanguageServer>)> {
         self.language_servers.iter().filter_map(
-            move |((language_server_worktree_id, language_name), server)| {
+            move |((language_server_worktree_id, _), server)| {
                 if *language_server_worktree_id == worktree_id {
-                    Some((language_name.as_ref(), server))
+                    Some(server)
                 } else {
                     None
                 }
@@ -1303,7 +1297,12 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        let key = (worktree_id, language.name());
+        let adapter = if let Some(adapter) = language.lsp_adapter() {
+            adapter
+        } else {
+            return;
+        };
+        let key = (worktree_id, adapter.name());
         self.started_language_servers
             .entry(key.clone())
             .or_insert_with(|| {
@@ -1316,109 +1315,100 @@ impl Project {
                     cx,
                 );
                 cx.spawn_weak(|this, mut cx| async move {
-                    let mut language_server = language_server?.await.log_err()?;
+                    let language_server = language_server?.await.log_err()?;
+                    let language_server = language_server
+                        .initialize(adapter.initialization_options())
+                        .await
+                        .log_err()?;
                     let this = this.upgrade(&cx)?;
-                    let (language_server_events_tx, language_server_events_rx) =
-                        smol::channel::unbounded();
+                    let disk_based_diagnostics_progress_token =
+                        adapter.disk_based_diagnostics_progress_token();
 
                     language_server
                         .on_notification::<lsp::notification::PublishDiagnostics, _>({
-                            let language_server_events_tx = language_server_events_tx.clone();
-                            move |params| {
-                                language_server_events_tx
-                                    .try_send(LanguageServerEvent::DiagnosticsUpdate(params))
-                                    .ok();
+                            let this = this.downgrade();
+                            let adapter = adapter.clone();
+                            move |params, mut cx| {
+                                if let Some(this) = this.upgrade(&cx) {
+                                    this.update(&mut cx, |this, cx| {
+                                        this.on_lsp_diagnostics_published(
+                                            server_id,
+                                            params,
+                                            &adapter,
+                                            disk_based_diagnostics_progress_token,
+                                            cx,
+                                        );
+                                    });
+                                }
                             }
                         })
                         .detach();
 
                     language_server
-                        .on_request::<lsp::request::WorkspaceConfiguration, _>({
+                        .on_request::<lsp::request::WorkspaceConfiguration, _, _>({
                             let settings = this
                                 .read_with(&cx, |this, _| this.language_server_settings.clone());
-                            move |params| {
-                                let settings = settings.lock();
-                                Ok(params
-                                    .items
-                                    .into_iter()
-                                    .map(|item| {
-                                        if let Some(section) = &item.section {
-                                            settings
-                                                .get(section)
-                                                .cloned()
-                                                .unwrap_or(serde_json::Value::Null)
-                                        } else {
-                                            settings.clone()
-                                        }
-                                    })
-                                    .collect())
+                            move |params, _| {
+                                let settings = settings.lock().clone();
+                                async move {
+                                    Ok(params
+                                        .items
+                                        .into_iter()
+                                        .map(|item| {
+                                            if let Some(section) = &item.section {
+                                                settings
+                                                    .get(section)
+                                                    .cloned()
+                                                    .unwrap_or(serde_json::Value::Null)
+                                            } else {
+                                                settings.clone()
+                                            }
+                                        })
+                                        .collect())
+                                }
                             }
                         })
                         .detach();
 
                     language_server
-                        .on_notification::<lsp::notification::Progress, _>(move |params| {
-                            let token = match params.token {
-                                lsp::NumberOrString::String(token) => token,
-                                lsp::NumberOrString::Number(token) => {
-                                    log::info!("skipping numeric progress token {}", token);
-                                    return;
-                                }
-                            };
-
-                            match params.value {
-                                lsp::ProgressParamsValue::WorkDone(progress) => match progress {
-                                    lsp::WorkDoneProgress::Begin(_) => {
-                                        language_server_events_tx
-                                            .try_send(LanguageServerEvent::WorkStart { token })
-                                            .ok();
-                                    }
-                                    lsp::WorkDoneProgress::Report(report) => {
-                                        language_server_events_tx
-                                            .try_send(LanguageServerEvent::WorkProgress {
-                                                token,
-                                                progress: LanguageServerProgress {
-                                                    message: report.message,
-                                                    percentage: report
-                                                        .percentage
-                                                        .map(|p| p as usize),
-                                                    last_update_at: Instant::now(),
-                                                },
-                                            })
-                                            .ok();
-                                    }
-                                    lsp::WorkDoneProgress::End(_) => {
-                                        language_server_events_tx
-                                            .try_send(LanguageServerEvent::WorkEnd { token })
-                                            .ok();
-                                    }
-                                },
+                        .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
+                            let this = this.downgrade();
+                            let adapter = adapter.clone();
+                            let language_server = language_server.clone();
+                            move |params, cx| {
+                                Self::on_lsp_workspace_edit(
+                                    this,
+                                    params,
+                                    server_id,
+                                    adapter.clone(),
+                                    language_server.clone(),
+                                    cx,
+                                )
                             }
                         })
                         .detach();
 
-                    // Process all the LSP events.
-                    cx.spawn(|mut cx| {
-                        let this = this.downgrade();
-                        async move {
-                            while let Ok(event) = language_server_events_rx.recv().await {
-                                let this = this.upgrade(&cx)?;
-                                this.update(&mut cx, |this, cx| {
-                                    this.on_lsp_event(server_id, event, &language, cx)
-                                });
-
-                                // Don't starve the main thread when lots of events arrive all at once.
-                                smol::future::yield_now().await;
+                    language_server
+                        .on_notification::<lsp::notification::Progress, _>({
+                            let this = this.downgrade();
+                            move |params, mut cx| {
+                                if let Some(this) = this.upgrade(&cx) {
+                                    this.update(&mut cx, |this, cx| {
+                                        this.on_lsp_progress(
+                                            params,
+                                            server_id,
+                                            disk_based_diagnostics_progress_token,
+                                            cx,
+                                        );
+                                    });
+                                }
                             }
-                            Some(())
-                        }
-                    })
-                    .detach();
+                        })
+                        .detach();
 
-                    let language_server = language_server.initialize().await.log_err()?;
                     this.update(&mut cx, |this, cx| {
                         this.language_servers
-                            .insert(key.clone(), language_server.clone());
+                            .insert(key.clone(), (adapter, language_server.clone()));
                         this.language_server_statuses.insert(
                             server_id,
                             LanguageServerStatus {
@@ -1461,7 +1451,10 @@ impl Project {
                                 } else {
                                     continue;
                                 };
-                                if (file.worktree.read(cx).id(), language.name()) != key {
+                                if file.worktree.read(cx).id() != key.0
+                                    || language.lsp_adapter().map(|a| a.name())
+                                        != Some(key.1.clone())
+                                {
                                     continue;
                                 }
 
@@ -1540,15 +1533,20 @@ impl Project {
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        let key = (worktree_id, language.name());
+        let adapter = if let Some(adapter) = language.lsp_adapter() {
+            adapter
+        } else {
+            return;
+        };
+        let key = (worktree_id, adapter.name());
         let server_to_shutdown = self.language_servers.remove(&key);
         self.started_language_servers.remove(&key);
         server_to_shutdown
             .as_ref()
-            .map(|server| self.language_server_statuses.remove(&server.server_id()));
+            .map(|(_, server)| self.language_server_statuses.remove(&server.server_id()));
         cx.spawn_weak(|this, mut cx| async move {
             if let Some(this) = this.upgrade(&cx) {
-                if let Some(server_to_shutdown) = server_to_shutdown {
+                if let Some((_, server_to_shutdown)) = server_to_shutdown {
                     if let Some(shutdown_task) = server_to_shutdown.shutdown() {
                         shutdown_task.await;
                     }
@@ -1562,116 +1560,138 @@ impl Project {
         .detach();
     }
 
-    fn on_lsp_event(
+    fn on_lsp_diagnostics_published(
         &mut self,
-        language_server_id: usize,
-        event: LanguageServerEvent,
-        language: &Arc<Language>,
+        server_id: usize,
+        mut params: lsp::PublishDiagnosticsParams,
+        adapter: &Arc<dyn LspAdapter>,
+        disk_based_diagnostics_progress_token: Option<&str>,
         cx: &mut ModelContext<Self>,
     ) {
-        let disk_diagnostics_token = language.disk_based_diagnostics_progress_token();
-        let language_server_status =
-            if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
-                status
-            } else {
+        adapter.process_diagnostics(&mut params);
+        if disk_based_diagnostics_progress_token.is_none() {
+            self.disk_based_diagnostics_started(cx);
+            self.broadcast_language_server_update(
+                server_id,
+                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+                    proto::LspDiskBasedDiagnosticsUpdating {},
+                ),
+            );
+        }
+        self.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx)
+            .log_err();
+        if disk_based_diagnostics_progress_token.is_none() {
+            self.disk_based_diagnostics_finished(cx);
+            self.broadcast_language_server_update(
+                server_id,
+                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                    proto::LspDiskBasedDiagnosticsUpdated {},
+                ),
+            );
+        }
+    }
+
+    fn on_lsp_progress(
+        &mut self,
+        progress: lsp::ProgressParams,
+        server_id: usize,
+        disk_based_diagnostics_progress_token: Option<&str>,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let token = match progress.token {
+            lsp::NumberOrString::String(token) => token,
+            lsp::NumberOrString::Number(token) => {
+                log::info!("skipping numeric progress token {}", token);
                 return;
-            };
+            }
+        };
 
-        match event {
-            LanguageServerEvent::WorkStart { token } => {
-                if Some(&token) == disk_diagnostics_token {
-                    language_server_status.pending_diagnostic_updates += 1;
-                    if language_server_status.pending_diagnostic_updates == 1 {
-                        self.disk_based_diagnostics_started(cx);
+        match progress.value {
+            lsp::ProgressParamsValue::WorkDone(progress) => match progress {
+                lsp::WorkDoneProgress::Begin(_) => {
+                    let language_server_status =
+                        if let Some(status) = self.language_server_statuses.get_mut(&server_id) {
+                            status
+                        } else {
+                            return;
+                        };
+
+                    if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+                        language_server_status.pending_diagnostic_updates += 1;
+                        if language_server_status.pending_diagnostic_updates == 1 {
+                            self.disk_based_diagnostics_started(cx);
+                            self.broadcast_language_server_update(
+                                                            server_id,
+                                                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
+                                                                proto::LspDiskBasedDiagnosticsUpdating {},
+                                                            ),
+                                                        );
+                        }
+                    } else {
+                        self.on_lsp_work_start(server_id, token.clone(), cx);
                         self.broadcast_language_server_update(
-                            language_server_id,
-                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
-                                proto::LspDiskBasedDiagnosticsUpdating {},
+                            server_id,
+                            proto::update_language_server::Variant::WorkStart(
+                                proto::LspWorkStart { token },
                             ),
                         );
                     }
-                } else {
-                    self.on_lsp_work_start(language_server_id, token.clone(), cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::WorkStart(proto::LspWorkStart {
-                            token,
-                        }),
-                    );
                 }
-            }
-            LanguageServerEvent::WorkProgress { token, progress } => {
-                if Some(&token) != disk_diagnostics_token {
-                    self.on_lsp_work_progress(
-                        language_server_id,
-                        token.clone(),
-                        progress.clone(),
-                        cx,
-                    );
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::WorkProgress(
-                            proto::LspWorkProgress {
-                                token,
-                                message: progress.message,
-                                percentage: progress.percentage.map(|p| p as u32),
+                lsp::WorkDoneProgress::Report(report) => {
+                    if Some(token.as_str()) != disk_based_diagnostics_progress_token {
+                        self.on_lsp_work_progress(
+                            server_id,
+                            token.clone(),
+                            LanguageServerProgress {
+                                message: report.message.clone(),
+                                percentage: report.percentage.map(|p| p as usize),
+                                last_update_at: Instant::now(),
                             },
-                        ),
-                    );
-                }
-            }
-            LanguageServerEvent::WorkEnd { token } => {
-                if Some(&token) == disk_diagnostics_token {
-                    language_server_status.pending_diagnostic_updates -= 1;
-                    if language_server_status.pending_diagnostic_updates == 0 {
-                        self.disk_based_diagnostics_finished(cx);
+                            cx,
+                        );
                         self.broadcast_language_server_update(
-                            language_server_id,
-                            proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
-                                proto::LspDiskBasedDiagnosticsUpdated {},
+                            server_id,
+                            proto::update_language_server::Variant::WorkProgress(
+                                proto::LspWorkProgress {
+                                    token,
+                                    message: report.message,
+                                    percentage: report.percentage.map(|p| p as u32),
+                                },
                             ),
                         );
                     }
-                } else {
-                    self.on_lsp_work_end(language_server_id, token.clone(), cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
-                            token,
-                        }),
-                    );
                 }
-            }
-            LanguageServerEvent::DiagnosticsUpdate(mut params) => {
-                language.process_diagnostics(&mut params);
+                lsp::WorkDoneProgress::End(_) => {
+                    if Some(token.as_str()) == disk_based_diagnostics_progress_token {
+                        let language_server_status = if let Some(status) =
+                            self.language_server_statuses.get_mut(&server_id)
+                        {
+                            status
+                        } else {
+                            return;
+                        };
 
-                if disk_diagnostics_token.is_none() {
-                    self.disk_based_diagnostics_started(cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
-                            proto::LspDiskBasedDiagnosticsUpdating {},
-                        ),
-                    );
-                }
-                self.update_diagnostics(
-                    params,
-                    language
-                        .disk_based_diagnostic_sources()
-                        .unwrap_or(&Default::default()),
-                    cx,
-                )
-                .log_err();
-                if disk_diagnostics_token.is_none() {
-                    self.disk_based_diagnostics_finished(cx);
-                    self.broadcast_language_server_update(
-                        language_server_id,
-                        proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
-                            proto::LspDiskBasedDiagnosticsUpdated {},
-                        ),
-                    );
+                        language_server_status.pending_diagnostic_updates -= 1;
+                        if language_server_status.pending_diagnostic_updates == 0 {
+                            self.disk_based_diagnostics_finished(cx);
+                            self.broadcast_language_server_update(
+                                server_id,
+                                proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
+                                    proto::LspDiskBasedDiagnosticsUpdated {},
+                                ),
+                            );
+                        }
+                    } else {
+                        self.on_lsp_work_end(server_id, token.clone(), cx);
+                        self.broadcast_language_server_update(
+                            server_id,
+                            proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
+                                token,
+                            }),
+                        );
+                    }
                 }
-            }
+            },
         }
     }
 
@@ -1719,6 +1739,40 @@ impl Project {
         }
     }
 
+    async fn on_lsp_workspace_edit(
+        this: WeakModelHandle<Self>,
+        params: lsp::ApplyWorkspaceEditParams,
+        server_id: usize,
+        adapter: Arc<dyn LspAdapter>,
+        language_server: Arc<LanguageServer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<lsp::ApplyWorkspaceEditResponse> {
+        let this = this
+            .upgrade(&cx)
+            .ok_or_else(|| anyhow!("project project closed"))?;
+        let transaction = Self::deserialize_workspace_edit(
+            this.clone(),
+            params.edit,
+            true,
+            adapter.clone(),
+            language_server.clone(),
+            &mut cx,
+        )
+        .await
+        .log_err();
+        this.update(&mut cx, |this, _| {
+            if let Some(transaction) = transaction {
+                this.last_workspace_edits_by_language_server
+                    .insert(server_id, transaction);
+            }
+        });
+        Ok(lsp::ApplyWorkspaceEditResponse {
+            applied: true,
+            failed_change: None,
+            failure_reason: None,
+        })
+    }
+
     fn broadcast_language_server_update(
         &self,
         language_server_id: usize,
@@ -1736,7 +1790,7 @@ impl Project {
     }
 
     pub fn set_language_server_settings(&mut self, settings: serde_json::Value) {
-        for server in self.language_servers.values() {
+        for (_, server) in self.language_servers.values() {
             server
                 .notify::<lsp::notification::DidChangeConfiguration>(
                     lsp::DidChangeConfigurationParams {
@@ -1757,7 +1811,7 @@ impl Project {
     pub fn update_diagnostics(
         &mut self,
         params: lsp::PublishDiagnosticsParams,
-        disk_based_sources: &HashSet<String>,
+        disk_based_sources: &[&str],
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
         let abs_path = params
@@ -1800,8 +1854,9 @@ impl Project {
                 );
             } else {
                 let group_id = post_inc(&mut next_group_id);
-                let is_disk_based =
-                    source.map_or(false, |source| disk_based_sources.contains(source));
+                let is_disk_based = source.map_or(false, |source| {
+                    disk_based_sources.contains(&source.as_str())
+                });
 
                 sources_by_group_id.insert(group_id, source);
                 primary_diagnostic_group_ids
@@ -2050,7 +2105,7 @@ impl Project {
             let buffer = buffer_handle.read(cx);
             if let Some(file) = File::from_dyn(buffer.file()) {
                 if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) {
-                    if let Some(server) = self.language_server_for_buffer(buffer, cx) {
+                    if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) {
                         local_buffers.push((buffer_handle, buffer_abs_path, server.clone()));
                     }
                 } else {
@@ -2099,7 +2154,12 @@ impl Project {
                     language_server
                         .request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
                             text_document,
-                            options: Default::default(),
+                            options: lsp::FormattingOptions {
+                                tab_size: 4,
+                                insert_spaces: true,
+                                insert_final_newline: Some(true),
+                                ..Default::default()
+                            },
                             work_done_progress_params: Default::default(),
                         })
                         .await?
@@ -2109,15 +2169,19 @@ impl Project {
                     .map_or(false, |provider| *provider != lsp::OneOf::Left(false))
                 {
                     let buffer_start = lsp::Position::new(0, 0);
-                    let buffer_end = buffer
-                        .read_with(&cx, |buffer, _| buffer.max_point_utf16())
-                        .to_lsp_position();
+                    let buffer_end =
+                        buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
                     language_server
                         .request::<lsp::request::RangeFormatting>(
                             lsp::DocumentRangeFormattingParams {
                                 text_document,
                                 range: lsp::Range::new(buffer_start, buffer_end),
-                                options: Default::default(),
+                                options: lsp::FormattingOptions {
+                                    tab_size: 4,
+                                    insert_spaces: true,
+                                    insert_final_newline: Some(true),
+                                    ..Default::default()
+                                },
                                 work_done_progress_params: Default::default(),
                             },
                         )
@@ -2187,25 +2251,24 @@ impl Project {
     pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         if self.is_local() {
             let mut language_servers = HashMap::default();
-            for ((worktree_id, language_name), language_server) in self.language_servers.iter() {
-                if let Some((worktree, language)) = self
+            for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() {
+                if let Some(worktree) = self
                     .worktree_for_id(*worktree_id, cx)
                     .and_then(|worktree| worktree.read(cx).as_local())
-                    .zip(self.languages.get_language(language_name))
                 {
                     language_servers
                         .entry(Arc::as_ptr(language_server))
                         .or_insert((
+                            lsp_adapter.clone(),
                             language_server.clone(),
                             *worktree_id,
                             worktree.abs_path().clone(),
-                            language.clone(),
                         ));
                 }
             }
 
             let mut requests = Vec::new();
-            for (language_server, _, _, _) in language_servers.values() {
+            for (_, language_server, _, _) in language_servers.values() {
                 requests.push(language_server.request::<lsp::request::WorkspaceSymbol>(
                     lsp::WorkspaceSymbolParams {
                         query: query.to_string(),
@@ -2220,7 +2283,7 @@ impl Project {
                 let mut symbols = Vec::new();
                 if let Some(this) = this.upgrade(&cx) {
                     this.read_with(&cx, |this, cx| {
-                        for ((_, source_worktree_id, worktree_abs_path, language), lsp_symbols) in
+                        for ((adapter, _, source_worktree_id, worktree_abs_path), lsp_symbols) in
                             language_servers.into_values().zip(responses)
                         {
                             symbols.extend(lsp_symbols.into_iter().flatten().filter_map(
@@ -2237,8 +2300,13 @@ impl Project {
                                         path = relativize_path(&worktree_abs_path, &abs_path);
                                     }
 
-                                    let label = language
-                                        .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
+                                    let label = this
+                                        .languages
+                                        .select_language(&path)
+                                        .and_then(|language| {
+                                            language
+                                                .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind)
+                                        })
                                         .unwrap_or_else(|| {
                                             CodeLabel::plain(lsp_symbol.name.clone(), None)
                                         });
@@ -2247,7 +2315,7 @@ impl Project {
                                     Some(Symbol {
                                         source_worktree_id,
                                         worktree_id,
-                                        language_name: language.name().to_string(),
+                                        language_server_name: adapter.name(),
                                         name: lsp_symbol.name,
                                         kind: lsp_symbol.kind,
                                         label,
@@ -2294,9 +2362,9 @@ impl Project {
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Buffer>>> {
         if self.is_local() {
-            let language_server = if let Some(server) = self.language_servers.get(&(
+            let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&(
                 symbol.source_worktree_id,
-                Arc::from(symbol.language_name.as_str()),
+                symbol.language_server_name.clone(),
             )) {
                 server.clone()
             } else {
@@ -2321,12 +2389,7 @@ impl Project {
                 return Task::ready(Err(anyhow!("invalid symbol path")));
             };
 
-            self.open_local_buffer_via_lsp(
-                symbol_uri,
-                Arc::from(symbol.language_name.as_str()),
-                language_server,
-                cx,
-            )
+            self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx)
         } else if let Some(project_id) = self.remote_id() {
             let request = self.client.request(proto::OpenBufferForSymbol {
                 project_id,
@@ -2367,7 +2430,7 @@ impl Project {
 
         if worktree.read(cx).as_local().is_some() {
             let buffer_abs_path = buffer_abs_path.unwrap();
-            let lang_server =
+            let (_, lang_server) =
                 if let Some(server) = self.language_server_for_buffer(source_buffer, cx) {
                     server.clone()
                 } else {
@@ -2381,7 +2444,7 @@ impl Project {
                             lsp::TextDocumentIdentifier::new(
                                 lsp::Url::from_file_path(buffer_abs_path).unwrap(),
                             ),
-                            position.to_lsp_position(),
+                            point_to_lsp(position),
                         ),
                         context: Default::default(),
                         work_done_progress_params: Default::default(),
@@ -2403,11 +2466,26 @@ impl Project {
                     Ok(completions
                         .into_iter()
                         .filter_map(|lsp_completion| {
-                            let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? {
-                                lsp::CompletionTextEdit::Edit(edit) => {
+                            let (old_range, new_text) = match lsp_completion.text_edit.as_ref() {
+                                Some(lsp::CompletionTextEdit::Edit(edit)) => {
                                     (range_from_lsp(edit.range), edit.new_text.clone())
                                 }
-                                lsp::CompletionTextEdit::InsertAndReplace(_) => {
+                                None => {
+                                    let clipped_position =
+                                        this.clip_point_utf16(position, Bias::Left);
+                                    if position != clipped_position {
+                                        log::info!("completion out of expected range");
+                                        return None;
+                                    }
+                                    (
+                                        this.common_prefix_at(
+                                            clipped_position,
+                                            &lsp_completion.label,
+                                        ),
+                                        lsp_completion.label.clone(),
+                                    )
+                                }
+                                Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
                                     log::info!("unsupported insert/replace completion");
                                     return None;
                                 }
@@ -2432,6 +2510,7 @@ impl Project {
                                     lsp_completion,
                                 })
                             } else {
+                                log::info!("completion out of expected range");
                                 None
                             }
                         })
@@ -2479,7 +2558,8 @@ impl Project {
         let buffer_id = buffer.remote_id();
 
         if self.is_local() {
-            let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) {
+            let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx)
+            {
                 server.clone()
             } else {
                 return Task::ready(Ok(Default::default()));
@@ -2549,7 +2629,7 @@ impl Project {
         }
     }
 
-    pub fn code_actions<T: ToOffset>(
+    pub fn code_actions<T: Clone + ToOffset>(
         &self,
         buffer_handle: &ModelHandle<Buffer>,
         range: Range<T>,
@@ -2557,6 +2637,11 @@ impl Project {
     ) -> Task<Result<Vec<CodeAction>>> {
         let buffer_handle = buffer_handle.clone();
         let buffer = buffer_handle.read(cx);
+        let snapshot = buffer.snapshot();
+        let relevant_diagnostics = snapshot
+            .diagnostics_in_range::<usize, usize>(range.to_offset(&snapshot), false)
+            .map(|entry| entry.to_lsp_diagnostic_stub())
+            .collect();
         let buffer_id = buffer.remote_id();
         let worktree;
         let buffer_abs_path;
@@ -2570,16 +2655,14 @@ impl Project {
 
         if worktree.read(cx).as_local().is_some() {
             let buffer_abs_path = buffer_abs_path.unwrap();
-            let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) {
+            let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx)
+            {
                 server.clone()
             } else {
                 return Task::ready(Ok(Default::default()));
             };
 
-            let lsp_range = lsp::Range::new(
-                range.start.to_point_utf16(buffer).to_lsp_position(),
-                range.end.to_point_utf16(buffer).to_lsp_position(),
-            );
+            let lsp_range = range_to_lsp(range.to_point_utf16(buffer));
             cx.foreground().spawn(async move {
                 if !lang_server.capabilities().code_action_provider.is_some() {
                     return Ok(Default::default());
@@ -2594,11 +2677,12 @@ impl Project {
                         work_done_progress_params: Default::default(),
                         partial_result_params: Default::default(),
                         context: lsp::CodeActionContext {
-                            diagnostics: Default::default(),
+                            diagnostics: relevant_diagnostics,
                             only: Some(vec![
                                 lsp::CodeActionKind::QUICKFIX,
                                 lsp::CodeActionKind::REFACTOR,
                                 lsp::CodeActionKind::REFACTOR_EXTRACT,
+                                lsp::CodeActionKind::SOURCE,
                             ]),
                         },
                     })
@@ -2657,16 +2741,12 @@ impl Project {
     ) -> Task<Result<ProjectTransaction>> {
         if self.is_local() {
             let buffer = buffer_handle.read(cx);
-            let lang_name = if let Some(lang) = buffer.language() {
-                lang.name()
-            } else {
-                return Task::ready(Ok(Default::default()));
-            };
-            let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) {
-                server.clone()
-            } else {
-                return Task::ready(Ok(Default::default()));
-            };
+            let (lsp_adapter, lang_server) =
+                if let Some(server) = self.language_server_for_buffer(buffer, cx) {
+                    server.clone()
+                } else {
+                    return Task::ready(Ok(Default::default()));
+                };
             let range = action.range.to_point_utf16(buffer);
 
             cx.spawn(|this, mut cx| async move {
@@ -2677,11 +2757,7 @@ impl Project {
                     .and_then(|d| d.get_mut("codeActionParams"))
                     .and_then(|d| d.get_mut("range"))
                 {
-                    *lsp_range = serde_json::to_value(&lsp::Range::new(
-                        range.start.to_lsp_position(),
-                        range.end.to_lsp_position(),
-                    ))
-                    .unwrap();
+                    *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap();
                     action.lsp_action = lang_server
                         .request::<lsp::request::CodeActionResolveRequest>(action.lsp_action)
                         .await?;
@@ -2703,11 +2779,28 @@ impl Project {
                         this,
                         edit,
                         push_to_history,
-                        lang_name,
+                        lsp_adapter,
                         lang_server,
                         &mut cx,
                     )
                     .await
+                } else if let Some(command) = action.lsp_action.command {
+                    this.update(&mut cx, |this, _| {
+                        this.last_workspace_edits_by_language_server
+                            .remove(&lang_server.server_id());
+                    });
+                    lang_server
+                        .request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
+                            command: command.command,
+                            arguments: command.arguments.unwrap_or_default(),
+                            ..Default::default()
+                        })
+                        .await?;
+                    Ok(this.update(&mut cx, |this, _| {
+                        this.last_workspace_edits_by_language_server
+                            .remove(&lang_server.server_id())
+                            .unwrap_or_default()
+                    }))
                 } else {
                     Ok(ProjectTransaction::default())
                 }

crates/rpc/proto/zed.proto 🔗

@@ -231,7 +231,7 @@ message GetProjectSymbolsResponse {
 message Symbol {
     uint64 source_worktree_id = 1;
     uint64 worktree_id = 2;
-    string language_name = 3;
+    string language_server_name = 3;
     string name = 4;
     int32 kind = 5;
     string path = 6;

crates/server/src/rpc.rs 🔗

@@ -1089,10 +1089,10 @@ mod tests {
     };
     use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
     use language::{
-        tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry,
-        LanguageServerConfig, OffsetRangeExt, Point, Rope, ToLspPosition,
+        range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
+        LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
     };
-    use lsp;
+    use lsp::{self, FakeLanguageServer};
     use parking_lot::Mutex;
     use postage::barrier;
     use project::{
@@ -2040,22 +2040,20 @@ mod tests {
         cx_b: &mut TestAppContext,
     ) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2262,29 +2260,29 @@ mod tests {
         cx_b: &mut TestAppContext,
     ) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
 
         // Set up a fake language server.
-        let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        language_server_config.set_fake_capabilities(lsp::ServerCapabilities {
-            completion_provider: Some(lsp::CompletionOptions {
-                trigger_characters: Some(vec![".".to_string()]),
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
                 ..Default::default()
-            }),
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+            capabilities: lsp::ServerCapabilities {
+                completion_provider: Some(lsp::CompletionOptions {
+                    trigger_characters: Some(vec![".".to_string()]),
+                    ..Default::default()
+                }),
+                ..Default::default()
+            },
             ..Default::default()
         });
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2345,7 +2343,7 @@ mod tests {
             Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
         });
 
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         buffer_b
             .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
             .await;
@@ -2371,7 +2369,7 @@ mod tests {
                     lsp::Position::new(0, 14),
                 );
 
-                Some(lsp::CompletionResponse::Array(vec![
+                Ok(Some(lsp::CompletionResponse::Array(vec![
                     lsp::CompletionItem {
                         label: "first_method(…)".into(),
                         detail: Some("fn(&mut self, B) -> C".into()),
@@ -2398,7 +2396,7 @@ mod tests {
                         insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
                         ..Default::default()
                     },
-                ]))
+                ])))
             })
             .next()
             .await
@@ -2428,7 +2426,7 @@ mod tests {
         fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
             |params, _| async move {
                 assert_eq!(params.label, "first_method(…)");
-                lsp::CompletionItem {
+                Ok(lsp::CompletionItem {
                     label: "first_method(…)".into(),
                     detail: Some("fn(&mut self, B) -> C".into()),
                     text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
@@ -2444,7 +2442,7 @@ mod tests {
                     }]),
                     insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
                     ..Default::default()
-                }
+                })
             },
         );
 
@@ -2581,22 +2579,20 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2652,9 +2648,9 @@ mod tests {
             .await
             .unwrap();
 
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
-            Some(vec![
+            Ok(Some(vec![
                 lsp::TextEdit {
                     range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
                     new_text: "h".to_string(),
@@ -2663,7 +2659,7 @@ mod tests {
                     range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
                     new_text: "y".to_string(),
                 },
-            ])
+            ]))
         });
 
         project_b
@@ -2681,7 +2677,7 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
         fs.insert_tree(
             "/root-1",
@@ -2700,18 +2696,16 @@ mod tests {
         .await;
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2761,12 +2755,14 @@ mod tests {
             .unwrap();
 
         // Request the definition of a symbol as the guest.
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
             |_, _| async move {
-                Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                    lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
-                    lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                Ok(Some(lsp::GotoDefinitionResponse::Scalar(
+                    lsp::Location::new(
+                        lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
+                        lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                    ),
                 )))
             },
         );
@@ -2793,9 +2789,11 @@ mod tests {
         // the previous call to `definition`.
         fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
             |_, _| async move {
-                Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                    lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
-                    lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
+                Ok(Some(lsp::GotoDefinitionResponse::Scalar(
+                    lsp::Location::new(
+                        lsp::Url::from_file_path("/root-2/b.rs").unwrap(),
+                        lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
+                    ),
                 )))
             },
         );
@@ -2823,7 +2821,7 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
         fs.insert_tree(
             "/root-1",
@@ -2843,18 +2841,16 @@ mod tests {
         .await;
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -2904,14 +2900,14 @@ mod tests {
             .unwrap();
 
         // Request references to a symbol as the guest.
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server.handle_request::<lsp::request::References, _, _>(
             |params, _| async move {
                 assert_eq!(
                     params.text_document_position.text_document.uri.as_str(),
                     "file:///root-1/one.rs"
                 );
-                Some(vec![
+                Ok(Some(vec![
                     lsp::Location {
                         uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(),
                         range: lsp::Range::new(
@@ -2933,7 +2929,7 @@ mod tests {
                             lsp::Position::new(0, 40),
                         ),
                     },
-                ])
+                ]))
             },
         );
 
@@ -3085,16 +3081,16 @@ mod tests {
         .await;
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        lang_registry.add(Arc::new(Language::new(
+        let mut language = Language::new(
             LanguageConfig {
                 name: "Rust".into(),
                 path_suffixes: vec!["rs".to_string()],
-                language_server: Some(language_server_config),
                 ..Default::default()
             },
             Some(tree_sitter_rust::language()),
-        )));
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3144,7 +3140,7 @@ mod tests {
             .unwrap();
 
         // Request document highlights as the guest.
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
             |params, _| async move {
                 assert_eq!(
@@ -3159,7 +3155,7 @@ mod tests {
                     params.text_document_position_params.position,
                     lsp::Position::new(0, 34)
                 );
-                Some(vec![
+                Ok(Some(vec![
                     lsp::DocumentHighlight {
                         kind: Some(lsp::DocumentHighlightKind::WRITE),
                         range: lsp::Range::new(
@@ -3181,7 +3177,7 @@ mod tests {
                             lsp::Position::new(0, 47),
                         ),
                     },
-                ])
+                ]))
             },
         );
 
@@ -3210,7 +3206,7 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
         fs.insert_tree(
             "/code",
@@ -3230,18 +3226,16 @@ mod tests {
         .await;
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3290,11 +3284,11 @@ mod tests {
             .await
             .unwrap();
 
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(
             |_, _| async move {
                 #[allow(deprecated)]
-                Some(vec![lsp::SymbolInformation {
+                Ok(Some(vec![lsp::SymbolInformation {
                     name: "TWO".into(),
                     location: lsp::Location {
                         uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
@@ -3304,7 +3298,7 @@ mod tests {
                     tags: None,
                     container_name: None,
                     deprecated: None,
-                }])
+                }]))
             },
         );
 
@@ -3349,7 +3343,7 @@ mod tests {
         mut rng: StdRng,
     ) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
         fs.insert_tree(
             "/root",
@@ -3362,19 +3356,16 @@ mod tests {
         .await;
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3423,12 +3414,14 @@ mod tests {
             .await
             .unwrap();
 
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
         fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(
             |_, _| async move {
-                Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new(
-                    lsp::Url::from_file_path("/root/b.rs").unwrap(),
-                    lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                Ok(Some(lsp::GotoDefinitionResponse::Scalar(
+                    lsp::Location::new(
+                        lsp::Url::from_file_path("/root/b.rs").unwrap(),
+                        lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
+                    ),
                 )))
             },
         );
@@ -3455,23 +3448,21 @@ mod tests {
         cx_b: &mut TestAppContext,
     ) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
         cx_b.update(|cx| editor::init(cx));
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3546,7 +3537,7 @@ mod tests {
                 );
                 assert_eq!(params.range.start, lsp::Position::new(0, 0));
                 assert_eq!(params.range.end, lsp::Position::new(0, 0));
-                None
+                Ok(None)
             })
             .next()
             .await;
@@ -3566,7 +3557,7 @@ mod tests {
                 assert_eq!(params.range.start, lsp::Position::new(1, 31));
                 assert_eq!(params.range.end, lsp::Position::new(1, 31));
 
-                Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
                     lsp::CodeAction {
                         title: "Inline into all callers".to_string(),
                         edit: Some(lsp::WorkspaceEdit {
@@ -3608,7 +3599,7 @@ mod tests {
                         })),
                         ..Default::default()
                     },
-                )])
+                )]))
             })
             .next()
             .await;
@@ -3631,7 +3622,7 @@ mod tests {
             .unwrap();
         fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
             |_, _| async move {
-                lsp::CodeAction {
+                Ok(lsp::CodeAction {
                     title: "Inline into all callers".to_string(),
                     edit: Some(lsp::WorkspaceEdit {
                         changes: Some(
@@ -3663,7 +3654,7 @@ mod tests {
                         ..Default::default()
                     }),
                     ..Default::default()
-                }
+                })
             },
         );
 
@@ -3691,23 +3682,21 @@ mod tests {
     #[gpui::test(iterations = 10)]
     async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         cx_a.foreground().forbid_parking();
-        let mut lang_registry = Arc::new(LanguageRegistry::test());
+        let lang_registry = Arc::new(LanguageRegistry::test());
         let fs = FakeFs::new(cx_a.background());
         cx_b.update(|cx| editor::init(cx));
 
         // Set up a fake language server.
-        let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake();
-        Arc::get_mut(&mut lang_registry)
-            .unwrap()
-            .add(Arc::new(Language::new(
-                LanguageConfig {
-                    name: "Rust".into(),
-                    path_suffixes: vec!["rs".to_string()],
-                    language_server: Some(language_server_config),
-                    ..Default::default()
-                },
-                Some(tree_sitter_rust::language()),
-            )));
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        );
+        let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
+        lang_registry.add(Arc::new(language));
 
         // Connect to a server as 2 clients.
         let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
@@ -3772,7 +3761,7 @@ mod tests {
             .unwrap()
             .downcast::<Editor>()
             .unwrap();
-        let mut fake_language_server = fake_language_servers.next().await.unwrap();
+        let fake_language_server = fake_language_servers.next().await.unwrap();
 
         // Move cursor to a location that can be renamed.
         let prepare_rename = editor_b.update(cx_b, |editor, cx| {
@@ -3784,10 +3773,10 @@ mod tests {
             .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
                 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
                 assert_eq!(params.position, lsp::Position::new(0, 7));
-                Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
                     lsp::Position::new(0, 6),
                     lsp::Position::new(0, 9),
-                )))
+                ))))
             })
             .next()
             .await
@@ -3821,7 +3810,7 @@ mod tests {
                     lsp::Position::new(0, 6)
                 );
                 assert_eq!(params.new_name, "THREE");
-                Some(lsp::WorkspaceEdit {
+                Ok(Some(lsp::WorkspaceEdit {
                     changes: Some(
                         [
                             (
@@ -3858,7 +3847,7 @@ mod tests {
                         .collect(),
                     ),
                     ..Default::default()
-                })
+                }))
             })
             .next()
             .await
@@ -4956,7 +4945,7 @@ mod tests {
         let rng = Arc::new(Mutex::new(rng));
 
         let guest_lang_registry = Arc::new(LanguageRegistry::test());
-        let (language_server_config, _fake_language_servers) = LanguageServerConfig::fake();
+        let host_language_registry = Arc::new(LanguageRegistry::test());
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -4970,6 +4959,7 @@ mod tests {
         let operations = Rc::new(Cell::new(0));
         let mut server = TestServer::start(cx.foreground(), cx.background()).await;
         let mut clients = Vec::new();
+        let files = Arc::new(Mutex::new(Vec::new()));
 
         let mut next_entity_id = 100000;
         let mut host_cx = TestAppContext::new(
@@ -4986,7 +4976,7 @@ mod tests {
             Project::local(
                 host.client.clone(),
                 host.user_store.clone(),
-                Arc::new(LanguageRegistry::test()),
+                host_language_registry.clone(),
                 fs.clone(),
                 cx,
             )
@@ -5009,9 +4999,136 @@ mod tests {
             .await
             .unwrap();
 
+        // Set up fake language servers.
+        let mut language = Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            None,
+        );
+        let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
+            name: "the-fake-language-server",
+            capabilities: lsp::LanguageServer::full_capabilities(),
+            initializer: Some(Box::new({
+                let rng = rng.clone();
+                let files = files.clone();
+                let project = host_project.downgrade();
+                move |fake_server: &mut FakeLanguageServer| {
+                    fake_server.handle_request::<lsp::request::Completion, _, _>(
+                        |_, _| async move {
+                            Ok(Some(lsp::CompletionResponse::Array(vec![
+                                lsp::CompletionItem {
+                                    text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+                                        range: lsp::Range::new(
+                                            lsp::Position::new(0, 0),
+                                            lsp::Position::new(0, 0),
+                                        ),
+                                        new_text: "the-new-text".to_string(),
+                                    })),
+                                    ..Default::default()
+                                },
+                            ])))
+                        },
+                    );
+
+                    fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
+                        |_, _| async move {
+                            Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
+                                lsp::CodeAction {
+                                    title: "the-code-action".to_string(),
+                                    ..Default::default()
+                                },
+                            )]))
+                        },
+                    );
+
+                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
+                        |params, _| async move {
+                            Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
+                                params.position,
+                                params.position,
+                            ))))
+                        },
+                    );
+
+                    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
+                        let files = files.clone();
+                        let rng = rng.clone();
+                        move |_, _| {
+                            let files = files.clone();
+                            let rng = rng.clone();
+                            async move {
+                                let files = files.lock();
+                                let mut rng = rng.lock();
+                                let count = rng.gen_range::<usize, _>(1..3);
+                                let files = (0..count)
+                                    .map(|_| files.choose(&mut *rng).unwrap())
+                                    .collect::<Vec<_>>();
+                                log::info!("LSP: Returning definitions in files {:?}", &files);
+                                Ok(Some(lsp::GotoDefinitionResponse::Array(
+                                    files
+                                        .into_iter()
+                                        .map(|file| lsp::Location {
+                                            uri: lsp::Url::from_file_path(file).unwrap(),
+                                            range: Default::default(),
+                                        })
+                                        .collect(),
+                                )))
+                            }
+                        }
+                    });
+
+                    fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
+                        let rng = rng.clone();
+                        let project = project.clone();
+                        move |params, mut cx| {
+                            let highlights = if let Some(project) = project.upgrade(&cx) {
+                                project.update(&mut cx, |project, cx| {
+                                    let path = params
+                                        .text_document_position_params
+                                        .text_document
+                                        .uri
+                                        .to_file_path()
+                                        .unwrap();
+                                    let (worktree, relative_path) =
+                                        project.find_local_worktree(&path, cx)?;
+                                    let project_path =
+                                        ProjectPath::from((worktree.read(cx).id(), relative_path));
+                                    let buffer =
+                                        project.get_open_buffer(&project_path, cx)?.read(cx);
+
+                                    let mut highlights = Vec::new();
+                                    let highlight_count = rng.lock().gen_range(1..=5);
+                                    let mut prev_end = 0;
+                                    for _ in 0..highlight_count {
+                                        let range =
+                                            buffer.random_byte_range(prev_end, &mut *rng.lock());
+
+                                        highlights.push(lsp::DocumentHighlight {
+                                            range: range_to_lsp(range.to_point_utf16(buffer)),
+                                            kind: Some(lsp::DocumentHighlightKind::READ),
+                                        });
+                                        prev_end = range.end;
+                                    }
+                                    Some(highlights)
+                                })
+                            } else {
+                                None
+                            };
+                            async move { Ok(highlights) }
+                        }
+                    });
+                }
+            })),
+            ..Default::default()
+        });
+        host_language_registry.add(Arc::new(language));
+
         clients.push(cx.foreground().spawn(host.simulate_host(
             host_project,
-            language_server_config,
+            files,
             operations.clone(),
             max_operations,
             rng.clone(),
@@ -5442,264 +5559,128 @@ mod tests {
             })
         }
 
-        fn simulate_host(
+        async fn simulate_host(
             mut self,
             project: ModelHandle<Project>,
-            mut language_server_config: LanguageServerConfig,
+            files: Arc<Mutex<Vec<PathBuf>>>,
             operations: Rc<Cell<usize>>,
             max_operations: usize,
             rng: Arc<Mutex<StdRng>>,
             mut cx: TestAppContext,
-        ) -> impl Future<Output = (Self, TestAppContext)> {
-            let files: Arc<Mutex<Vec<PathBuf>>> = Default::default();
-
-            // Set up a fake language server.
-            language_server_config.set_fake_initializer({
-                let rng = rng.clone();
-                let files = files.clone();
-                let project = project.downgrade();
-                move |fake_server| {
-                    fake_server.handle_request::<lsp::request::Completion, _, _>(
-                        |_, _| async move {
-                            Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem {
-                                text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
-                                    range: lsp::Range::new(
-                                        lsp::Position::new(0, 0),
-                                        lsp::Position::new(0, 0),
-                                    ),
-                                    new_text: "the-new-text".to_string(),
-                                })),
-                                ..Default::default()
-                            }]))
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
-                        |_, _| async move {
-                            Some(vec![lsp::CodeActionOrCommand::CodeAction(
-                                lsp::CodeAction {
-                                    title: "the-code-action".to_string(),
-                                    ..Default::default()
-                                },
-                            )])
-                        },
-                    );
-
-                    fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
-                        |params, _| async move {
-                            Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
-                                params.position,
-                                params.position,
-                            )))
-                        },
-                    );
+        ) -> (Self, TestAppContext) {
+            let fs = project.read_with(&cx, |project, _| project.fs().clone());
+            while operations.get() < max_operations {
+                operations.set(operations.get() + 1);
 
-                    fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
-                        let files = files.clone();
-                        let rng = rng.clone();
-                        move |_, _| {
-                            let files = files.clone();
-                            let rng = rng.clone();
-                            async move {
-                                let files = files.lock();
-                                let mut rng = rng.lock();
-                                let count = rng.gen_range::<usize, _>(1..3);
-                                let files = (0..count)
-                                    .map(|_| files.choose(&mut *rng).unwrap())
-                                    .collect::<Vec<_>>();
-                                log::info!("LSP: Returning definitions in files {:?}", &files);
-                                Some(lsp::GotoDefinitionResponse::Array(
-                                    files
-                                        .into_iter()
-                                        .map(|file| lsp::Location {
-                                            uri: lsp::Url::from_file_path(file).unwrap(),
-                                            range: Default::default(),
-                                        })
-                                        .collect(),
-                                ))
+                let distribution = rng.lock().gen_range::<usize, _>(0..100);
+                match distribution {
+                    0..=20 if !files.lock().is_empty() => {
+                        let path = files.lock().choose(&mut *rng.lock()).unwrap().clone();
+                        let mut path = path.as_path();
+                        while let Some(parent_path) = path.parent() {
+                            path = parent_path;
+                            if rng.lock().gen() {
+                                break;
                             }
                         }
-                    });
-
-                    fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
-                        let rng = rng.clone();
-                        let project = project.clone();
-                        move |params, mut cx| {
-                            let highlights = if let Some(project) = project.upgrade(&cx) {
-                                project.update(&mut cx, |project, cx| {
-                                    let path = params
-                                        .text_document_position_params
-                                        .text_document
-                                        .uri
-                                        .to_file_path()
-                                        .unwrap();
-                                    let (worktree, relative_path) =
-                                        project.find_local_worktree(&path, cx)?;
-                                    let project_path =
-                                        ProjectPath::from((worktree.read(cx).id(), relative_path));
-                                    let buffer =
-                                        project.get_open_buffer(&project_path, cx)?.read(cx);
 
-                                    let mut highlights = Vec::new();
-                                    let highlight_count = rng.lock().gen_range(1..=5);
-                                    let mut prev_end = 0;
-                                    for _ in 0..highlight_count {
-                                        let range =
-                                            buffer.random_byte_range(prev_end, &mut *rng.lock());
-                                        let start = buffer
-                                            .offset_to_point_utf16(range.start)
-                                            .to_lsp_position();
-                                        let end = buffer
-                                            .offset_to_point_utf16(range.end)
-                                            .to_lsp_position();
-                                        highlights.push(lsp::DocumentHighlight {
-                                            range: lsp::Range::new(start, end),
-                                            kind: Some(lsp::DocumentHighlightKind::READ),
-                                        });
-                                        prev_end = range.end;
-                                    }
-                                    Some(highlights)
-                                })
-                            } else {
-                                None
-                            };
-                            async move { highlights }
+                        log::info!("Host: find/create local worktree {:?}", path);
+                        let find_or_create_worktree = project.update(&mut cx, |project, cx| {
+                            project.find_or_create_local_worktree(path, true, cx)
+                        });
+                        let find_or_create_worktree = async move {
+                            find_or_create_worktree.await.unwrap();
+                        };
+                        if rng.lock().gen() {
+                            cx.background().spawn(find_or_create_worktree).detach();
+                        } else {
+                            find_or_create_worktree.await;
                         }
-                    });
-                }
-            });
-
-            project.update(&mut cx, |project, _| {
-                project.languages().add(Arc::new(Language::new(
-                    LanguageConfig {
-                        name: "Rust".into(),
-                        path_suffixes: vec!["rs".to_string()],
-                        language_server: Some(language_server_config),
-                        ..Default::default()
-                    },
-                    None,
-                )));
-            });
-
-            async move {
-                let fs = project.read_with(&cx, |project, _| project.fs().clone());
-                while operations.get() < max_operations {
-                    operations.set(operations.get() + 1);
-
-                    let distribution = rng.lock().gen_range::<usize, _>(0..100);
-                    match distribution {
-                        0..=20 if !files.lock().is_empty() => {
-                            let path = files.lock().choose(&mut *rng.lock()).unwrap().clone();
-                            let mut path = path.as_path();
-                            while let Some(parent_path) = path.parent() {
-                                path = parent_path;
-                                if rng.lock().gen() {
-                                    break;
-                                }
-                            }
+                    }
+                    10..=80 if !files.lock().is_empty() => {
+                        let buffer = if self.buffers.is_empty() || rng.lock().gen() {
+                            let file = files.lock().choose(&mut *rng.lock()).unwrap().clone();
+                            let (worktree, path) = project
+                                .update(&mut cx, |project, cx| {
+                                    project.find_or_create_local_worktree(file.clone(), true, cx)
+                                })
+                                .await
+                                .unwrap();
+                            let project_path =
+                                worktree.read_with(&cx, |worktree, _| (worktree.id(), path));
+                            log::info!(
+                                "Host: opening path {:?}, worktree {}, relative_path {:?}",
+                                file,
+                                project_path.0,
+                                project_path.1
+                            );
+                            let buffer = project
+                                .update(&mut cx, |project, cx| {
+                                    project.open_buffer(project_path, cx)
+                                })
+                                .await
+                                .unwrap();
+                            self.buffers.insert(buffer.clone());
+                            buffer
+                        } else {
+                            self.buffers
+                                .iter()
+                                .choose(&mut *rng.lock())
+                                .unwrap()
+                                .clone()
+                        };
 
-                            log::info!("Host: find/create local worktree {:?}", path);
-                            let find_or_create_worktree = project.update(&mut cx, |project, cx| {
-                                project.find_or_create_local_worktree(path, true, cx)
+                        if rng.lock().gen_bool(0.1) {
+                            cx.update(|cx| {
+                                log::info!(
+                                    "Host: dropping buffer {:?}",
+                                    buffer.read(cx).file().unwrap().full_path(cx)
+                                );
+                                self.buffers.remove(&buffer);
+                                drop(buffer);
                             });
-                            let find_or_create_worktree = async move {
-                                find_or_create_worktree.await.unwrap();
-                            };
-                            if rng.lock().gen() {
-                                cx.background().spawn(find_or_create_worktree).detach();
-                            } else {
-                                find_or_create_worktree.await;
-                            }
-                        }
-                        10..=80 if !files.lock().is_empty() => {
-                            let buffer = if self.buffers.is_empty() || rng.lock().gen() {
-                                let file = files.lock().choose(&mut *rng.lock()).unwrap().clone();
-                                let (worktree, path) = project
-                                    .update(&mut cx, |project, cx| {
-                                        project.find_or_create_local_worktree(
-                                            file.clone(),
-                                            true,
-                                            cx,
-                                        )
-                                    })
-                                    .await
-                                    .unwrap();
-                                let project_path =
-                                    worktree.read_with(&cx, |worktree, _| (worktree.id(), path));
+                        } else {
+                            buffer.update(&mut cx, |buffer, cx| {
                                 log::info!(
-                                    "Host: opening path {:?}, worktree {}, relative_path {:?}",
-                                    file,
-                                    project_path.0,
-                                    project_path.1
+                                    "Host: updating buffer {:?} ({})",
+                                    buffer.file().unwrap().full_path(cx),
+                                    buffer.remote_id()
                                 );
-                                let buffer = project
-                                    .update(&mut cx, |project, cx| {
-                                        project.open_buffer(project_path, cx)
-                                    })
-                                    .await
-                                    .unwrap();
-                                self.buffers.insert(buffer.clone());
-                                buffer
-                            } else {
-                                self.buffers
-                                    .iter()
-                                    .choose(&mut *rng.lock())
-                                    .unwrap()
-                                    .clone()
-                            };
-
-                            if rng.lock().gen_bool(0.1) {
-                                cx.update(|cx| {
-                                    log::info!(
-                                        "Host: dropping buffer {:?}",
-                                        buffer.read(cx).file().unwrap().full_path(cx)
-                                    );
-                                    self.buffers.remove(&buffer);
-                                    drop(buffer);
-                                });
-                            } else {
-                                buffer.update(&mut cx, |buffer, cx| {
-                                    log::info!(
-                                        "Host: updating buffer {:?} ({})",
-                                        buffer.file().unwrap().full_path(cx),
-                                        buffer.remote_id()
-                                    );
-                                    buffer.randomly_edit(&mut *rng.lock(), 5, cx)
-                                });
-                            }
+                                buffer.randomly_edit(&mut *rng.lock(), 5, cx)
+                            });
                         }
-                        _ => loop {
-                            let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
-                            let mut path = PathBuf::new();
-                            path.push("/");
-                            for _ in 0..path_component_count {
-                                let letter = rng.lock().gen_range(b'a'..=b'z');
-                                path.push(std::str::from_utf8(&[letter]).unwrap());
-                            }
-                            path.set_extension("rs");
-                            let parent_path = path.parent().unwrap();
-
-                            log::info!("Host: creating file {:?}", path,);
-
-                            if fs.create_dir(&parent_path).await.is_ok()
-                                && fs.create_file(&path, Default::default()).await.is_ok()
-                            {
-                                files.lock().push(path);
-                                break;
-                            } else {
-                                log::info!("Host: cannot create file");
-                            }
-                        },
                     }
+                    _ => loop {
+                        let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
+                        let mut path = PathBuf::new();
+                        path.push("/");
+                        for _ in 0..path_component_count {
+                            let letter = rng.lock().gen_range(b'a'..=b'z');
+                            path.push(std::str::from_utf8(&[letter]).unwrap());
+                        }
+                        path.set_extension("rs");
+                        let parent_path = path.parent().unwrap();
 
-                    cx.background().simulate_random_delay().await;
-                }
+                        log::info!("Host: creating file {:?}", path,);
 
-                log::info!("Host done");
+                        if fs.create_dir(&parent_path).await.is_ok()
+                            && fs.create_file(&path, Default::default()).await.is_ok()
+                        {
+                            files.lock().push(path);
+                            break;
+                        } else {
+                            log::info!("Host: cannot create file");
+                        }
+                    },
+                }
 
-                self.project = Some(project);
-                (self, cx)
+                cx.background().simulate_random_delay().await;
             }
+
+            log::info!("Host done");
+
+            self.project = Some(project);
+            (self, cx)
         }
 
         pub async fn simulate_guest(

crates/text/src/tests.rs 🔗

@@ -164,6 +164,55 @@ fn test_line_len() {
     assert_eq!(buffer.line_len(5), 0);
 }
 
+#[test]
+fn test_common_prefix_at_positionn() {
+    let text = "a = str; b = δα";
+    let buffer = Buffer::new(0, 0, History::new(text.into()));
+
+    let offset1 = offset_after(text, "str");
+    let offset2 = offset_after(text, "δα");
+
+    // the preceding word is a prefix of the suggestion
+    assert_eq!(
+        buffer.common_prefix_at(offset1, "string"),
+        range_of(text, "str"),
+    );
+    // a suffix of the preceding word is a prefix of the suggestion
+    assert_eq!(
+        buffer.common_prefix_at(offset1, "tree"),
+        range_of(text, "tr"),
+    );
+    // the preceding word is a substring of the suggestion, but not a prefix
+    assert_eq!(
+        buffer.common_prefix_at(offset1, "astro"),
+        empty_range_after(text, "str"),
+    );
+
+    // prefix matching is case insenstive.
+    assert_eq!(
+        buffer.common_prefix_at(offset1, "Strαngε"),
+        range_of(text, "str"),
+    );
+    assert_eq!(
+        buffer.common_prefix_at(offset2, "ΔΑΜΝ"),
+        range_of(text, "δα"),
+    );
+
+    fn offset_after(text: &str, part: &str) -> usize {
+        text.find(part).unwrap() + part.len()
+    }
+
+    fn empty_range_after(text: &str, part: &str) -> Range<usize> {
+        let offset = offset_after(text, part);
+        offset..offset
+    }
+
+    fn range_of(text: &str, part: &str) -> Range<usize> {
+        let start = text.find(part).unwrap();
+        start..start + part.len()
+    }
+}
+
 #[test]
 fn test_text_summary_for_range() {
     let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into()));

crates/text/src/text.rs 🔗

@@ -1508,6 +1508,30 @@ impl BufferSnapshot {
                 .eq(needle.bytes())
     }
 
+    pub fn common_prefix_at<T>(&self, position: T, needle: &str) -> Range<T>
+    where
+        T: ToOffset + TextDimension,
+    {
+        let offset = position.to_offset(self);
+        let common_prefix_len = needle
+            .char_indices()
+            .map(|(index, _)| index)
+            .chain([needle.len()])
+            .take_while(|&len| len <= offset)
+            .filter(|&len| {
+                let left = self
+                    .chars_for_range(offset - len..offset)
+                    .flat_map(|c| char::to_lowercase(c));
+                let right = needle[..len].chars().flat_map(|c| char::to_lowercase(c));
+                left.eq(right)
+            })
+            .last()
+            .unwrap_or(0);
+        let start_offset = offset - common_prefix_len;
+        let start = self.text_summary_for_range(0..start_offset);
+        start..position
+    }
+
     pub fn text(&self) -> String {
         self.visible_text.to_string()
     }

crates/zed/Cargo.toml 🔗

@@ -99,6 +99,7 @@ tree-sitter-c = "0.20.1"
 tree-sitter-json = "0.19.0"
 tree-sitter-rust = "0.20.1"
 tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
+tree-sitter-typescript = "0.20.1"
 url = "2.2"
 
 [dev-dependencies]

crates/zed/src/languages.rs 🔗

@@ -0,0 +1,112 @@
+use gpui::Task;
+pub use language::*;
+use rust_embed::RustEmbed;
+use std::{borrow::Cow, str, sync::Arc};
+
+mod c;
+mod installation;
+mod json;
+mod rust;
+mod typescript;
+
+#[derive(RustEmbed)]
+#[folder = "src/languages"]
+#[exclude = "*.rs"]
+struct LanguageDir;
+
+pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
+    let languages = LanguageRegistry::new(login_shell_env_loaded);
+    for (name, grammar, lsp_adapter) in [
+        (
+            "c",
+            tree_sitter_c::language(),
+            Some(Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>),
+        ),
+        (
+            "json",
+            tree_sitter_json::language(),
+            Some(Arc::new(json::JsonLspAdapter)),
+        ),
+        (
+            "markdown",
+            tree_sitter_markdown::language(),
+            None, //
+        ),
+        (
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(rust::RustLspAdapter)),
+        ),
+        (
+            "tsx",
+            tree_sitter_typescript::language_tsx(),
+            Some(Arc::new(typescript::TypeScriptLspAdapter)),
+        ),
+        (
+            "typescript",
+            tree_sitter_typescript::language_typescript(),
+            Some(Arc::new(typescript::TypeScriptLspAdapter)),
+        ),
+    ] {
+        languages.add(Arc::new(language(name, grammar, lsp_adapter)));
+    }
+    languages
+}
+
+fn language(
+    name: &str,
+    grammar: tree_sitter::Language,
+    lsp_adapter: Option<Arc<dyn LspAdapter>>,
+) -> Language {
+    let config = toml::from_slice(
+        &LanguageDir::get(&format!("{}/config.toml", name))
+            .unwrap()
+            .data,
+    )
+    .unwrap();
+    let mut language = Language::new(config, Some(grammar));
+
+    if let Some(query) = load_query(name, "/highlights") {
+        language = language
+            .with_highlights_query(query.as_ref())
+            .expect("failed to evaluate highlights query");
+    }
+    if let Some(query) = load_query(name, "/brackets") {
+        language = language
+            .with_brackets_query(query.as_ref())
+            .expect("failed to load brackets query");
+    }
+    if let Some(query) = load_query(name, "/indents") {
+        language = language
+            .with_indents_query(query.as_ref())
+            .expect("failed to load indents query");
+    }
+    if let Some(query) = load_query(name, "/outline") {
+        language = language
+            .with_outline_query(query.as_ref())
+            .expect("failed to load outline query");
+    }
+    if let Some(lsp_adapter) = lsp_adapter {
+        language = language.with_lsp_adapter(lsp_adapter)
+    }
+    language
+}
+
+fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
+    let mut result = None;
+    for path in LanguageDir::iter() {
+        if let Some(remainder) = path.strip_prefix(name) {
+            if remainder.starts_with(filename_prefix) {
+                let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
+                    Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
+                    Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
+                };
+                match &mut result {
+                    None => result = Some(contents),
+                    Some(r) => r.to_mut().push_str(contents.as_ref()),
+                }
+            }
+        }
+    }
+    result
+}

crates/zed/src/languages/c.rs 🔗

@@ -0,0 +1,114 @@
+use super::installation::{latest_github_release, GitHubLspBinaryVersion};
+use anyhow::{anyhow, Result};
+use client::http::{HttpClient, Method};
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+pub use language::*;
+use smol::fs::{self, File};
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{ResultExt, TryFutureExt};
+
+pub struct CLspAdapter;
+
+impl super::LspAdapter for CLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("clangd".into())
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        http: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
+        async move {
+            let version = latest_github_release("clangd/clangd", http, |release_name| {
+                format!("clangd-mac-{release_name}.zip")
+            })
+            .await?;
+            Ok(Box::new(version) as Box<_>)
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        http: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
+        async move {
+            let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
+            let version_dir = container_dir.join(format!("clangd_{}", version.name));
+            let binary_path = version_dir.join("bin/clangd");
+
+            if fs::metadata(&binary_path).await.is_err() {
+                let response = http
+                    .send(
+                        surf::RequestBuilder::new(Method::Get, version.url)
+                            .middleware(surf::middleware::Redirect::default())
+                            .build(),
+                    )
+                    .await
+                    .map_err(|err| anyhow!("error downloading release: {}", err))?;
+                let mut file = File::create(&zip_path).await?;
+                if !response.status().is_success() {
+                    Err(anyhow!(
+                        "download failed with status {}",
+                        response.status().to_string()
+                    ))?;
+                }
+                futures::io::copy(response, &mut file).await?;
+
+                let unzip_status = smol::process::Command::new("unzip")
+                    .current_dir(&container_dir)
+                    .arg(&zip_path)
+                    .output()
+                    .await?
+                    .status;
+                if !unzip_status.success() {
+                    Err(anyhow!("failed to unzip clangd archive"))?;
+                }
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_clangd_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_clangd_dir = Some(entry.path());
+                }
+            }
+            let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let clangd_bin = clangd_dir.join("bin/clangd");
+            if clangd_bin.exists() {
+                Ok(clangd_bin)
+            } else {
+                Err(anyhow!(
+                    "missing clangd binary in directory {:?}",
+                    clangd_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+}

crates/zed/languages/c/config.toml → crates/zed/src/languages/c/config.toml 🔗

@@ -9,6 +9,3 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false },
     { start = "/*", end = " */", close = true, newline = false },
 ]
-
-[language_server]
-disk_based_diagnostic_sources = []

crates/zed/src/languages/installation.rs 🔗

@@ -0,0 +1,111 @@
+use anyhow::{anyhow, Context, Result};
+use client::http::{self, HttpClient, Method};
+use serde::Deserialize;
+use std::{path::Path, sync::Arc};
+
+pub struct GitHubLspBinaryVersion {
+    pub name: String,
+    pub url: http::Url,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "kebab-case")]
+struct NpmInfo {
+    #[serde(default)]
+    dist_tags: NpmInfoDistTags,
+    versions: Vec<String>,
+}
+
+#[derive(Deserialize, Default)]
+struct NpmInfoDistTags {
+    latest: Option<String>,
+}
+
+#[derive(Deserialize)]
+pub(crate) struct GithubRelease {
+    name: String,
+    assets: Vec<GithubReleaseAsset>,
+}
+
+#[derive(Deserialize)]
+pub(crate) struct GithubReleaseAsset {
+    name: String,
+    browser_download_url: http::Url,
+}
+
+pub async fn npm_package_latest_version(name: &str) -> Result<String> {
+    let output = smol::process::Command::new("npm")
+        .args(["info", name, "--json"])
+        .output()
+        .await?;
+    if !output.status.success() {
+        Err(anyhow!(
+            "failed to execute npm info: {:?}",
+            String::from_utf8_lossy(&output.stderr)
+        ))?;
+    }
+    let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
+    info.dist_tags
+        .latest
+        .or_else(|| info.versions.pop())
+        .ok_or_else(|| anyhow!("no version found for npm package {}", name))
+}
+
+pub async fn npm_install_packages(
+    packages: impl IntoIterator<Item = (&str, &str)>,
+    directory: &Path,
+) -> Result<()> {
+    let output = smol::process::Command::new("npm")
+        .arg("install")
+        .arg("--prefix")
+        .arg(directory)
+        .args(
+            packages
+                .into_iter()
+                .map(|(name, version)| format!("{name}@{version}")),
+        )
+        .output()
+        .await
+        .context("failed to run npm install")?;
+    if !output.status.success() {
+        Err(anyhow!(
+            "failed to execute npm install: {:?}",
+            String::from_utf8_lossy(&output.stderr)
+        ))?;
+    }
+    Ok(())
+}
+
+pub async fn latest_github_release(
+    repo_name_with_owner: &str,
+    http: Arc<dyn HttpClient>,
+    asset_name: impl Fn(&str) -> String,
+) -> Result<GitHubLspBinaryVersion> {
+    let release = http
+        .send(
+            surf::RequestBuilder::new(
+                Method::Get,
+                http::Url::parse(&format!(
+                    "https://api.github.com/repos/{repo_name_with_owner}/releases/latest"
+                ))
+                .unwrap(),
+            )
+            .middleware(surf::middleware::Redirect::default())
+            .build(),
+        )
+        .await
+        .map_err(|err| anyhow!("error fetching latest release: {}", err))?
+        .body_json::<GithubRelease>()
+        .await
+        .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
+    let asset_name = asset_name(&release.name);
+    let asset = release
+        .assets
+        .iter()
+        .find(|asset| asset.name == asset_name)
+        .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
+    Ok(GitHubLspBinaryVersion {
+        name: release.name,
+        url: asset.browser_download_url.clone(),
+    })
+}

crates/zed/src/languages/json.rs 🔗

@@ -0,0 +1,130 @@
+use anyhow::{anyhow, Context, Result};
+use client::http::HttpClient;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use language::{LanguageServerName, LspAdapter};
+use serde::Deserialize;
+use serde_json::json;
+use smol::fs;
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{ResultExt, TryFutureExt};
+
+pub struct JsonLspAdapter;
+
+impl JsonLspAdapter {
+    const BIN_PATH: &'static str =
+        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
+}
+
+impl LspAdapter for JsonLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("vscode-json-languageserver".into())
+    }
+
+    fn server_args(&self) -> &[&str] {
+        &["--stdio"]
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Any + Send>>> {
+        async move {
+            #[derive(Deserialize)]
+            struct NpmInfo {
+                versions: Vec<String>,
+            }
+
+            let output = smol::process::Command::new("npm")
+                .args(["info", "vscode-json-languageserver", "--json"])
+                .output()
+                .await?;
+            if !output.status.success() {
+                Err(anyhow!("failed to execute npm info"))?;
+            }
+            let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
+
+            Ok(Box::new(
+                info.versions
+                    .pop()
+                    .ok_or_else(|| anyhow!("no versions found in npm info"))?,
+            ) as Box<_>)
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        let version = version.downcast::<String>().unwrap();
+        async move {
+            let version_dir = container_dir.join(version.as_str());
+            fs::create_dir_all(&version_dir)
+                .await
+                .context("failed to create version directory")?;
+            let binary_path = version_dir.join(Self::BIN_PATH);
+
+            if fs::metadata(&binary_path).await.is_err() {
+                let output = smol::process::Command::new("npm")
+                    .current_dir(&version_dir)
+                    .arg("install")
+                    .arg(format!("vscode-json-languageserver@{}", version))
+                    .output()
+                    .await
+                    .context("failed to run npm install")?;
+                if !output.status.success() {
+                    Err(anyhow!("failed to install vscode-json-languageserver"))?;
+                }
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_version_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_version_dir = Some(entry.path());
+                }
+            }
+            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let bin_path = last_version_dir.join(Self::BIN_PATH);
+            if bin_path.exists() {
+                Ok(bin_path)
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+
+    fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}

crates/zed/languages/json/config.toml → crates/zed/src/languages/json/config.toml 🔗

@@ -6,6 +6,3 @@ brackets = [
     { start = "[", end = "]", close = true, newline = true },
     { start = "\"", end = "\"", close = true, newline = false },
 ]
-
-[language_server]
-disk_based_diagnostic_sources = []

crates/zed/src/language.rs → crates/zed/src/languages/rust.rs 🔗

@@ -1,92 +1,50 @@
-use anyhow::{anyhow, Context, Result};
+use super::installation::{latest_github_release, GitHubLspBinaryVersion};
+use anyhow::{anyhow, Result};
 use async_compression::futures::bufread::GzipDecoder;
-use client::http::{self, HttpClient, Method};
+use client::http::{HttpClient, Method};
 use futures::{future::BoxFuture, FutureExt, StreamExt};
-use gpui::Task;
 pub use language::*;
 use lazy_static::lazy_static;
 use regex::Regex;
-use rust_embed::RustEmbed;
-use serde::Deserialize;
-use serde_json::json;
 use smol::fs::{self, File};
-use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
+use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
 use util::{ResultExt, TryFutureExt};
 
-#[derive(RustEmbed)]
-#[folder = "languages"]
-struct LanguageDir;
-
-struct RustLspAdapter;
-struct CLspAdapter;
-struct JsonLspAdapter;
-
-#[derive(Deserialize)]
-struct GithubRelease {
-    name: String,
-    assets: Vec<GithubReleaseAsset>,
-}
-
-#[derive(Deserialize)]
-struct GithubReleaseAsset {
-    name: String,
-    browser_download_url: http::Url,
-}
+pub struct RustLspAdapter;
 
 impl LspAdapter for RustLspAdapter {
-    fn name(&self) -> &'static str {
-        "rust-analyzer"
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("rust-analyzer".into())
     }
 
     fn fetch_latest_server_version(
         &self,
         http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
         async move {
-            let release = http
-            .send(
-                surf::RequestBuilder::new(
-                    Method::Get,
-                    http::Url::parse(
-                        "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest",
-                    )
-                    .unwrap(),
-                )
-                .middleware(surf::middleware::Redirect::default())
-                .build(),
-            )
-            .await
-            .map_err(|err| anyhow!("error fetching latest release: {}", err))?
-            .body_json::<GithubRelease>()
-            .await
-            .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
-            let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
-            let asset = release
-                .assets
-                .iter()
-                .find(|asset| asset.name == asset_name)
-                .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
-            Ok(LspBinaryVersion {
-                name: release.name,
-                url: Some(asset.browser_download_url.clone()),
+            let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| {
+                format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH)
             })
+            .await?;
+            Ok(Box::new(version) as Box<_>)
         }
         .boxed()
     }
 
     fn fetch_server_binary(
         &self,
-        version: LspBinaryVersion,
+        version: Box<dyn 'static + Send + Any>,
         http: Arc<dyn HttpClient>,
         container_dir: PathBuf,
     ) -> BoxFuture<'static, Result<PathBuf>> {
         async move {
+            let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
             let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name));
 
             if fs::metadata(&destination_path).await.is_err() {
                 let response = http
                     .send(
-                        surf::RequestBuilder::new(Method::Get, version.url.unwrap())
+                        surf::RequestBuilder::new(Method::Get, version.url)
                             .middleware(surf::middleware::Redirect::default())
                             .build(),
                     )
@@ -131,6 +89,14 @@ impl LspAdapter for RustLspAdapter {
         .boxed()
     }
 
+    fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] {
+        &["rustc"]
+    }
+
+    fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> {
+        Some("rustAnalyzer/cargo check")
+    }
+
     fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) {
         lazy_static! {
             static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap();
@@ -287,325 +253,11 @@ impl LspAdapter for RustLspAdapter {
     }
 }
 
-impl LspAdapter for CLspAdapter {
-    fn name(&self) -> &'static str {
-        "clangd"
-    }
-
-    fn fetch_latest_server_version(
-        &self,
-        http: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
-        async move {
-            let release = http
-                .send(
-                    surf::RequestBuilder::new(
-                        Method::Get,
-                        http::Url::parse(
-                            "https://api.github.com/repos/clangd/clangd/releases/latest",
-                        )
-                        .unwrap(),
-                    )
-                    .middleware(surf::middleware::Redirect::default())
-                    .build(),
-                )
-                .await
-                .map_err(|err| anyhow!("error fetching latest release: {}", err))?
-                .body_json::<GithubRelease>()
-                .await
-                .map_err(|err| anyhow!("error parsing latest release: {}", err))?;
-            let asset_name = format!("clangd-mac-{}.zip", release.name);
-            let asset = release
-                .assets
-                .iter()
-                .find(|asset| asset.name == asset_name)
-                .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?;
-            Ok(LspBinaryVersion {
-                name: release.name,
-                url: Some(asset.browser_download_url.clone()),
-            })
-        }
-        .boxed()
-    }
-
-    fn fetch_server_binary(
-        &self,
-        version: LspBinaryVersion,
-        http: Arc<dyn HttpClient>,
-        container_dir: PathBuf,
-    ) -> BoxFuture<'static, Result<PathBuf>> {
-        async move {
-            let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
-            let version_dir = container_dir.join(format!("clangd_{}", version.name));
-            let binary_path = version_dir.join("bin/clangd");
-
-            if fs::metadata(&binary_path).await.is_err() {
-                let response = http
-                    .send(
-                        surf::RequestBuilder::new(Method::Get, version.url.unwrap())
-                            .middleware(surf::middleware::Redirect::default())
-                            .build(),
-                    )
-                    .await
-                    .map_err(|err| anyhow!("error downloading release: {}", err))?;
-                let mut file = File::create(&zip_path).await?;
-                if !response.status().is_success() {
-                    Err(anyhow!(
-                        "download failed with status {}",
-                        response.status().to_string()
-                    ))?;
-                }
-                futures::io::copy(response, &mut file).await?;
-
-                let unzip_status = smol::process::Command::new("unzip")
-                    .current_dir(&container_dir)
-                    .arg(&zip_path)
-                    .output()
-                    .await?
-                    .status;
-                if !unzip_status.success() {
-                    Err(anyhow!("failed to unzip clangd archive"))?;
-                }
-
-                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                    while let Some(entry) = entries.next().await {
-                        if let Some(entry) = entry.log_err() {
-                            let entry_path = entry.path();
-                            if entry_path.as_path() != version_dir {
-                                fs::remove_dir_all(&entry_path).await.log_err();
-                            }
-                        }
-                    }
-                }
-            }
-
-            Ok(binary_path)
-        }
-        .boxed()
-    }
-
-    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
-        async move {
-            let mut last_clangd_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_clangd_dir = Some(entry.path());
-                }
-            }
-            let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let clangd_bin = clangd_dir.join("bin/clangd");
-            if clangd_bin.exists() {
-                Ok(clangd_bin)
-            } else {
-                Err(anyhow!(
-                    "missing clangd binary in directory {:?}",
-                    clangd_dir
-                ))
-            }
-        }
-        .log_err()
-        .boxed()
-    }
-
-    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
-}
-
-impl JsonLspAdapter {
-    const BIN_PATH: &'static str =
-        "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver";
-}
-
-impl LspAdapter for JsonLspAdapter {
-    fn name(&self) -> &'static str {
-        "vscode-json-languageserver"
-    }
-
-    fn server_args(&self) -> &[&str] {
-        &["--stdio"]
-    }
-
-    fn fetch_latest_server_version(
-        &self,
-        _: Arc<dyn HttpClient>,
-    ) -> BoxFuture<'static, Result<LspBinaryVersion>> {
-        async move {
-            #[derive(Deserialize)]
-            struct NpmInfo {
-                versions: Vec<String>,
-            }
-
-            let output = smol::process::Command::new("npm")
-                .args(["info", "vscode-json-languageserver", "--json"])
-                .output()
-                .await?;
-            if !output.status.success() {
-                Err(anyhow!("failed to execute npm info"))?;
-            }
-            let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
-
-            Ok(LspBinaryVersion {
-                name: info
-                    .versions
-                    .pop()
-                    .ok_or_else(|| anyhow!("no versions found in npm info"))?,
-                url: Default::default(),
-            })
-        }
-        .boxed()
-    }
-
-    fn fetch_server_binary(
-        &self,
-        version: LspBinaryVersion,
-        _: Arc<dyn HttpClient>,
-        container_dir: PathBuf,
-    ) -> BoxFuture<'static, Result<PathBuf>> {
-        async move {
-            let version_dir = container_dir.join(&version.name);
-            fs::create_dir_all(&version_dir)
-                .await
-                .context("failed to create version directory")?;
-            let binary_path = version_dir.join(Self::BIN_PATH);
-
-            if fs::metadata(&binary_path).await.is_err() {
-                let output = smol::process::Command::new("npm")
-                    .current_dir(&version_dir)
-                    .arg("install")
-                    .arg(format!("vscode-json-languageserver@{}", version.name))
-                    .output()
-                    .await
-                    .context("failed to run npm install")?;
-                if !output.status.success() {
-                    Err(anyhow!("failed to install vscode-json-languageserver"))?;
-                }
-
-                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
-                    while let Some(entry) = entries.next().await {
-                        if let Some(entry) = entry.log_err() {
-                            let entry_path = entry.path();
-                            if entry_path.as_path() != version_dir {
-                                fs::remove_dir_all(&entry_path).await.log_err();
-                            }
-                        }
-                    }
-                }
-            }
-
-            Ok(binary_path)
-        }
-        .boxed()
-    }
-
-    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
-        async move {
-            let mut last_version_dir = None;
-            let mut entries = fs::read_dir(&container_dir).await?;
-            while let Some(entry) = entries.next().await {
-                let entry = entry?;
-                if entry.file_type().await?.is_dir() {
-                    last_version_dir = Some(entry.path());
-                }
-            }
-            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
-            let bin_path = last_version_dir.join(Self::BIN_PATH);
-            if bin_path.exists() {
-                Ok(bin_path)
-            } else {
-                Err(anyhow!(
-                    "missing executable in directory {:?}",
-                    last_version_dir
-                ))
-            }
-        }
-        .log_err()
-        .boxed()
-    }
-
-    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
-
-    fn initialization_options(&self) -> Option<serde_json::Value> {
-        Some(json!({
-            "provideFormatter": true
-        }))
-    }
-}
-
-pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry {
-    let languages = LanguageRegistry::new(login_shell_env_loaded);
-    languages.add(Arc::new(c()));
-    languages.add(Arc::new(json()));
-    languages.add(Arc::new(rust()));
-    languages.add(Arc::new(markdown()));
-    languages
-}
-
-fn rust() -> Language {
-    let grammar = tree_sitter_rust::language();
-    let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap();
-    Language::new(config, Some(grammar))
-        .with_highlights_query(load_query("rust/highlights.scm").as_ref())
-        .unwrap()
-        .with_brackets_query(load_query("rust/brackets.scm").as_ref())
-        .unwrap()
-        .with_indents_query(load_query("rust/indents.scm").as_ref())
-        .unwrap()
-        .with_outline_query(load_query("rust/outline.scm").as_ref())
-        .unwrap()
-        .with_lsp_adapter(RustLspAdapter)
-}
-
-fn c() -> Language {
-    let grammar = tree_sitter_c::language();
-    let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap();
-    Language::new(config, Some(grammar))
-        .with_highlights_query(load_query("c/highlights.scm").as_ref())
-        .unwrap()
-        .with_brackets_query(load_query("c/brackets.scm").as_ref())
-        .unwrap()
-        .with_indents_query(load_query("c/indents.scm").as_ref())
-        .unwrap()
-        .with_outline_query(load_query("c/outline.scm").as_ref())
-        .unwrap()
-        .with_lsp_adapter(CLspAdapter)
-}
-
-fn json() -> Language {
-    let grammar = tree_sitter_json::language();
-    let config = toml::from_slice(&LanguageDir::get("json/config.toml").unwrap().data).unwrap();
-    Language::new(config, Some(grammar))
-        .with_highlights_query(load_query("json/highlights.scm").as_ref())
-        .unwrap()
-        .with_brackets_query(load_query("json/brackets.scm").as_ref())
-        .unwrap()
-        .with_indents_query(load_query("json/indents.scm").as_ref())
-        .unwrap()
-        .with_outline_query(load_query("json/outline.scm").as_ref())
-        .unwrap()
-        .with_lsp_adapter(JsonLspAdapter)
-}
-
-fn markdown() -> Language {
-    let grammar = tree_sitter_markdown::language();
-    let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap();
-    Language::new(config, Some(grammar))
-        .with_highlights_query(load_query("markdown/highlights.scm").as_ref())
-        .unwrap()
-}
-
-fn load_query(path: &str) -> Cow<'static, str> {
-    match LanguageDir::get(path).unwrap().data {
-        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
-        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
+    use crate::languages::{language, LspAdapter};
     use gpui::color::Color;
-    use language::LspAdapter;
     use theme::SyntaxTheme;
 
     #[test]
@@ -651,7 +303,11 @@ mod tests {
 
     #[test]
     fn test_rust_label_for_completion() {
-        let language = rust();
+        let language = language(
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(RustLspAdapter)),
+        );
         let grammar = language.grammar().unwrap();
         let theme = SyntaxTheme::new(vec![
             ("type".into(), Color::green().into()),
@@ -726,7 +382,11 @@ mod tests {
 
     #[test]
     fn test_rust_label_for_symbol() {
-        let language = rust();
+        let language = language(
+            "rust",
+            tree_sitter_rust::language(),
+            Some(Arc::new(RustLspAdapter)),
+        );
         let grammar = language.grammar().unwrap();
         let theme = SyntaxTheme::new(vec![
             ("type".into(), Color::green().into()),

crates/zed/languages/rust/config.toml → crates/zed/src/languages/rust/config.toml 🔗

@@ -10,7 +10,3 @@ brackets = [
     { start = "\"", end = "\"", close = true, newline = false },
     { start = "/*", end = " */", close = true, newline = false },
 ]
-
-[language_server]
-disk_based_diagnostic_sources = ["rustc"]
-disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check"

crates/zed/src/languages/tsx/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "TSX"
+path_suffixes = ["tsx", "js"]
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = false, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
+]

crates/zed/src/languages/typescript.rs 🔗

@@ -0,0 +1,146 @@
+use super::installation::{npm_install_packages, npm_package_latest_version};
+use anyhow::{anyhow, Context, Result};
+use client::http::HttpClient;
+use futures::{future::BoxFuture, FutureExt, StreamExt};
+use language::{LanguageServerName, LspAdapter};
+use serde_json::json;
+use smol::fs;
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::{ResultExt, TryFutureExt};
+
+pub struct TypeScriptLspAdapter;
+
+impl TypeScriptLspAdapter {
+    const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
+}
+
+struct Versions {
+    typescript_version: String,
+    server_version: String,
+}
+
+impl LspAdapter for TypeScriptLspAdapter {
+    fn name(&self) -> LanguageServerName {
+        LanguageServerName("typescript-language-server".into())
+    }
+
+    fn server_args(&self) -> &[&str] {
+        &["--stdio", "--tsserver-path", "node_modules/typescript/lib"]
+    }
+
+    fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> BoxFuture<'static, Result<Box<dyn 'static + Send + Any>>> {
+        async move {
+            Ok(Box::new(Versions {
+                typescript_version: npm_package_latest_version("typescript").await?,
+                server_version: npm_package_latest_version("typescript-language-server").await?,
+            }) as Box<_>)
+        }
+        .boxed()
+    }
+
+    fn fetch_server_binary(
+        &self,
+        versions: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> BoxFuture<'static, Result<PathBuf>> {
+        let versions = versions.downcast::<Versions>().unwrap();
+        async move {
+            let version_dir = container_dir.join(&format!(
+                "typescript-{}:server-{}",
+                versions.typescript_version, versions.server_version
+            ));
+            fs::create_dir_all(&version_dir)
+                .await
+                .context("failed to create version directory")?;
+            let binary_path = version_dir.join(Self::BIN_PATH);
+
+            if fs::metadata(&binary_path).await.is_err() {
+                npm_install_packages(
+                    [
+                        ("typescript", versions.typescript_version.as_str()),
+                        (
+                            "typescript-language-server",
+                            &versions.server_version.as_str(),
+                        ),
+                    ],
+                    &version_dir,
+                )
+                .await?;
+
+                if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                    while let Some(entry) = entries.next().await {
+                        if let Some(entry) = entry.log_err() {
+                            let entry_path = entry.path();
+                            if entry_path.as_path() != version_dir {
+                                fs::remove_dir_all(&entry_path).await.log_err();
+                            }
+                        }
+                    }
+                }
+            }
+
+            Ok(binary_path)
+        }
+        .boxed()
+    }
+
+    fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option<PathBuf>> {
+        async move {
+            let mut last_version_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_version_dir = Some(entry.path());
+                }
+            }
+            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let bin_path = last_version_dir.join(Self::BIN_PATH);
+            if bin_path.exists() {
+                Ok(bin_path)
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
+            }
+        }
+        .log_err()
+        .boxed()
+    }
+
+    fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
+
+    fn label_for_completion(
+        &self,
+        item: &lsp::CompletionItem,
+        language: &language::Language,
+    ) -> Option<language::CodeLabel> {
+        use lsp::CompletionItemKind as Kind;
+        let len = item.label.len();
+        let grammar = language.grammar()?;
+        let highlight_id = match item.kind? {
+            Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"),
+            Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"),
+            Kind::CONSTANT => grammar.highlight_id_for_name("constant"),
+            Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"),
+            Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
+            _ => None,
+        }?;
+        Some(language::CodeLabel {
+            text: item.label.clone(),
+            runs: vec![(0..len, highlight_id)],
+            filter_range: 0..len,
+        })
+    }
+
+    fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}

crates/zed/src/languages/typescript/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "TypeScript"
+path_suffixes = ["ts"]
+line_comment = "// "
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "<", end = ">", close = false, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "/*", end = " */", close = true, newline = false },
+]

crates/zed/src/languages/typescript/highlights.scm 🔗

@@ -0,0 +1,219 @@
+; Variables
+
+(identifier) @variable
+
+; Properties
+
+(property_identifier) @property
+
+; Function and method calls
+
+(call_expression
+  function: (identifier) @function)
+
+(call_expression
+  function: (member_expression
+    property: (property_identifier) @function.method))
+
+; Function and method definitions
+
+(function
+  name: (identifier) @function)
+(function_declaration
+  name: (identifier) @function)
+(method_definition
+  name: (property_identifier) @function.method)
+
+(pair
+  key: (property_identifier) @function.method
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (member_expression
+    property: (property_identifier) @function.method)
+  right: [(function) (arrow_function)])
+
+(variable_declarator
+  name: (identifier) @function
+  value: [(function) (arrow_function)])
+
+(assignment_expression
+  left: (identifier) @function
+  right: [(function) (arrow_function)])
+
+; Special identifiers
+
+((identifier) @constructor
+ (#match? @constructor "^[A-Z]"))
+
+([
+  (identifier)
+  (shorthand_property_identifier)
+  (shorthand_property_identifier_pattern)
+ ] @constant
+ (#match? @constant "^[A-Z_][A-Z\\d_]+$"))
+
+; Literals
+
+(this) @variable.builtin
+(super) @variable.builtin
+
+[
+  (true)
+  (false)
+  (null)
+  (undefined)
+] @constant.builtin
+
+(comment) @comment
+
+[
+  (string)
+  (template_string)
+] @string
+
+(regex) @string.special
+(number) @number
+
+; Tokens
+
+(template_substitution
+  "${" @punctuation.special
+  "}" @punctuation.special) @embedded
+
+[
+  ";"
+  "?."
+  "."
+  ","
+] @punctuation.delimiter
+
+[
+  "-"
+  "--"
+  "-="
+  "+"
+  "++"
+  "+="
+  "*"
+  "*="
+  "**"
+  "**="
+  "/"
+  "/="
+  "%"
+  "%="
+  "<"
+  "<="
+  "<<"
+  "<<="
+  "="
+  "=="
+  "==="
+  "!"
+  "!="
+  "!=="
+  "=>"
+  ">"
+  ">="
+  ">>"
+  ">>="
+  ">>>"
+  ">>>="
+  "~"
+  "^"
+  "&"
+  "|"
+  "^="
+  "&="
+  "|="
+  "&&"
+  "||"
+  "??"
+  "&&="
+  "||="
+  "??="
+] @operator
+
+[
+  "("
+  ")"
+  "["
+  "]"
+  "{"
+  "}"
+]  @punctuation.bracket
+
+[
+  "as"
+  "async"
+  "await"
+  "break"
+  "case"
+  "catch"
+  "class"
+  "const"
+  "continue"
+  "debugger"
+  "default"
+  "delete"
+  "do"
+  "else"
+  "export"
+  "extends"
+  "finally"
+  "for"
+  "from"
+  "function"
+  "get"
+  "if"
+  "import"
+  "in"
+  "instanceof"
+  "let"
+  "new"
+  "of"
+  "return"
+  "set"
+  "static"
+  "switch"
+  "target"
+  "throw"
+  "try"
+  "typeof"
+  "var"
+  "void"
+  "while"
+  "with"
+  "yield"
+] @keyword
+
+; Types
+
+(type_identifier) @type
+(predefined_type) @type.builtin
+
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(type_arguments
+  "<" @punctuation.bracket
+  ">" @punctuation.bracket)
+
+; Keywords
+
+[ "abstract"
+  "declare"
+  "enum"
+  "export"
+  "implements"
+  "interface"
+  "keyof"
+  "namespace"
+  "private"
+  "protected"
+  "public"
+  "type"
+  "readonly"
+  "override"
+] @keyword

crates/zed/src/languages/typescript/indents.scm 🔗

@@ -0,0 +1,15 @@
+[
+    (call_expression)
+    (assignment_expression)
+    (member_expression)
+    (lexical_declaration)
+    (variable_declaration)
+    (assignment_expression)
+    (if_statement)
+    (for_statement)
+] @indent
+
+(_ "[" "]" @end) @indent
+(_ "<" ">" @end) @indent
+(_ "{" "}" @end) @indent
+(_ "(" ")" @end) @indent

crates/zed/src/languages/typescript/outline.scm 🔗

@@ -0,0 +1,55 @@
+(internal_module
+    "namespace" @context
+    name: (_) @name) @item
+
+(enum_declaration
+    "enum" @context
+    name: (_) @name) @item
+
+(function_declaration
+    "async"? @context
+    "function" @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(interface_declaration
+    "interface" @context
+    name: (_) @name) @item
+
+(program
+    (lexical_declaration
+        ["let" "const"] @context
+        (variable_declarator
+            name: (_) @name) @item))
+
+(class_declaration
+    "class" @context
+    name: (_) @name) @item
+
+(method_definition
+    [
+        "get"
+        "set"
+        "async"
+        "*"
+        "readonly"
+        "static"
+        (override_modifier)
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name
+    parameters: (formal_parameters
+      "(" @context
+      ")" @context)) @item
+
+(public_field_definition
+    [
+        "declare"
+        "readonly"
+        "abstract"
+        "static"
+        (accessibility_modifier)
+    ]* @context
+    name: (_) @name) @item

crates/zed/src/main.rs 🔗

@@ -19,7 +19,7 @@ use workspace::{
     AppState, OpenNew, OpenParams, OpenPaths, Settings,
 };
 use zed::{
-    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus,
+    self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus,
 };
 
 fn main() {
@@ -34,7 +34,7 @@ fn main() {
     let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme)
         .unwrap()
         .with_overrides(
-            language::PLAIN_TEXT.name(),
+            languages::PLAIN_TEXT.name(),
             settings::LanguageOverride {
                 soft_wrap: Some(settings::SoftWrap::PreferredLineLength),
                 ..Default::default()
@@ -60,7 +60,7 @@ fn main() {
     app.run(move |cx| {
         let http = http::client();
         let client = client::Client::new(http.clone());
-        let mut languages = language::build_language_registry(login_shell_env_loaded);
+        let mut languages = languages::build_language_registry(login_shell_env_loaded);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
         let channel_list =
             cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));

crates/zed/src/zed.rs 🔗

@@ -1,5 +1,5 @@
 pub mod assets;
-pub mod language;
+pub mod languages;
 pub mod menus;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
@@ -574,7 +574,7 @@ mod tests {
             assert_eq!(editor.title(cx), "untitled");
             assert!(Arc::ptr_eq(
                 editor.language(cx).unwrap(),
-                &language::PLAIN_TEXT
+                &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
             assert!(editor.is_dirty(cx));
@@ -664,7 +664,7 @@ mod tests {
         editor.update(cx, |editor, cx| {
             assert!(Arc::ptr_eq(
                 editor.language(cx).unwrap(),
-                &language::PLAIN_TEXT
+                &languages::PLAIN_TEXT
             ));
             editor.handle_input(&editor::Input("hi".into()), cx);
             assert!(editor.is_dirty(cx.as_ref()));