use ssh lsp store (#17655)

Conrad Irwin and Mikayla created

Release Notes:

- ssh remoting: Added support for booting langauge servers (in limited
circumstances)

---------

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

crates/assistant/src/assistant_panel.rs                       |  15 
crates/assistant/src/inline_assistant.rs                      |   4 
crates/assistant/src/prompts.rs                               |   6 
crates/collab/src/tests/integration_tests.rs                  |  10 
crates/collab/src/tests/remote_editing_collaboration_tests.rs |   2 
crates/editor/src/clangd_ext.rs                               |   2 
crates/editor/src/editor.rs                                   |   2 
crates/editor/src/editor_tests.rs                             |  10 
crates/editor/src/items.rs                                    |   4 
crates/editor/src/rust_analyzer_ext.rs                        |   2 
crates/editor/src/test/editor_lsp_test_context.rs             |   2 
crates/extension/src/extension_lsp_adapter.rs                 |   1 
crates/extension/src/extension_manifest.rs                    |   8 
crates/extension/src/extension_store.rs                       |   5 
crates/extension/src/extension_store_test.rs                  |   2 
crates/extension/src/wasm_host/wit/since_v0_1_0.rs            |   4 
crates/gpui/src/app.rs                                        |   6 
crates/language/src/buffer_tests.rs                           |  56 
crates/language/src/language.rs                               |  59 
crates/language/src/language_registry.rs                      | 211 +
crates/language/src/language_settings.rs                      |  12 
crates/language_selector/src/active_buffer_language.rs        |   4 
crates/language_selector/src/language_selector.rs             |   2 
crates/language_tools/src/lsp_log.rs                          |   2 
crates/language_tools/src/syntax_tree_view.rs                 |   2 
crates/languages/src/rust.rs                                  |   2 
crates/languages/src/yaml.rs                                  |   2 
crates/lsp/src/lsp.rs                                         |  10 
crates/markdown_preview/src/markdown_preview_view.rs          |   2 
crates/project/src/lsp_command/signature_help.rs              |   2 
crates/project/src/lsp_store.rs                               | 573 +++-
crates/project/src/project.rs                                 | 122 
crates/project/src/project_settings.rs                        |   2 
crates/project/src/project_tests.rs                           |  14 
crates/project/src/task_inventory.rs                          |   4 
crates/proto/proto/zed.proto                                  |  37 
crates/proto/src/proto.rs                                     |   7 
crates/quick_action_bar/src/repl_menu.rs                      |   2 
crates/recent_projects/src/ssh_connections.rs                 |  21 
crates/remote/src/ssh_session.rs                              |  10 
crates/remote_server/src/headless_project.rs                  |  29 
crates/remote_server/src/remote_editing_tests.rs              | 122 +
crates/repl/src/repl_editor.rs                                |   8 
crates/worktree/src/worktree.rs                               |   7 
crates/zed/src/zed.rs                                         |  20 
45 files changed, 956 insertions(+), 473 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -53,7 +53,8 @@ use language_model::{
 };
 use multi_buffer::MultiBufferRow;
 use picker::{Picker, PickerDelegate};
-use project::{Project, ProjectLspAdapterDelegate, Worktree};
+use project::lsp_store::ProjectLspAdapterDelegate;
+use project::{Project, Worktree};
 use search::{buffer_search::DivRegistrar, BufferSearchBar};
 use serde::{Deserialize, Serialize};
 use settings::{update_settings_file, Settings};
@@ -5340,9 +5341,17 @@ fn make_lsp_adapter_delegate(
             .worktrees(cx)
             .next()
             .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
+        let fs = if project.is_local() {
+            Some(project.fs().clone())
+        } else {
+            None
+        };
+        let http_client = project.client().http_client().clone();
         project.lsp_store().update(cx, |lsp_store, cx| {
-            Ok(ProjectLspAdapterDelegate::new(lsp_store, &worktree, cx)
-                as Arc<dyn LspAdapterDelegate>)
+            Ok(
+                ProjectLspAdapterDelegate::new(lsp_store, &worktree, http_client, fs, cx)
+                    as Arc<dyn LspAdapterDelegate>,
+            )
         })
     })
 }

crates/assistant/src/inline_assistant.rs 🔗

@@ -2377,7 +2377,7 @@ impl Codegen {
         // If Markdown or No Language is Known, increase the randomness for more creative output
         // If Code, decrease temperature to get more deterministic outputs
         let temperature = if let Some(language) = language_name.clone() {
-            if language.as_ref() == "Markdown" {
+            if language == "Markdown".into() {
                 1.0
             } else {
                 0.5
@@ -2386,7 +2386,7 @@ impl Codegen {
             1.0
         };
 
-        let language_name = language_name.as_deref();
+        let language_name = language_name.as_ref();
         let start = buffer.point_to_buffer_offset(edit_range.start);
         let end = buffer.point_to_buffer_offset(edit_range.end);
         let (buffer, range) = if let Some((start, end)) = start.zip(end) {

crates/assistant/src/prompts.rs 🔗

@@ -4,7 +4,7 @@ use fs::Fs;
 use futures::StreamExt;
 use gpui::AssetSource;
 use handlebars::{Handlebars, RenderError};
-use language::BufferSnapshot;
+use language::{BufferSnapshot, LanguageName};
 use parking_lot::Mutex;
 use serde::Serialize;
 use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
@@ -204,11 +204,11 @@ impl PromptBuilder {
     pub fn generate_content_prompt(
         &self,
         user_prompt: String,
-        language_name: Option<&str>,
+        language_name: Option<&LanguageName>,
         buffer: BufferSnapshot,
         range: Range<usize>,
     ) -> Result<String, RenderError> {
-        let content_type = match language_name {
+        let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
             None | Some("Markdown" | "Plain Text") => "text",
             Some(_) => "code",
         };

crates/collab/src/tests/integration_tests.rs 🔗

@@ -2328,11 +2328,11 @@ async fn test_propagate_saves_and_fs_changes(
         .unwrap();
 
     buffer_b.read_with(cx_b, |buffer, _| {
-        assert_eq!(&*buffer.language().unwrap().name(), "Rust");
+        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
     });
 
     buffer_c.read_with(cx_c, |buffer, _| {
-        assert_eq!(&*buffer.language().unwrap().name(), "Rust");
+        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
     });
     buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
     buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
@@ -2432,17 +2432,17 @@ async fn test_propagate_saves_and_fs_changes(
 
     buffer_a.read_with(cx_a, |buffer, _| {
         assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
-        assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
+        assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
     });
 
     buffer_b.read_with(cx_b, |buffer, _| {
         assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
-        assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
+        assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
     });
 
     buffer_c.read_with(cx_c, |buffer, _| {
         assert_eq!(buffer.file().unwrap().path().to_str(), Some("file1.js"));
-        assert_eq!(&*buffer.language().unwrap().name(), "JavaScript");
+        assert_eq!(buffer.language().unwrap().name(), "JavaScript".into());
     });
 
     let new_buffer_a = project_a

crates/collab/src/tests/remote_editing_collaboration_tests.rs 🔗

@@ -100,7 +100,7 @@ async fn test_sharing_an_ssh_remote_project(
         let file = buffer_b.read(cx).file();
         assert_eq!(
             all_language_settings(file, cx)
-                .language(Some("Rust"))
+                .language(Some(&("Rust".into())))
                 .language_servers,
             ["override-rust-analyzer".into()]
         )

crates/editor/src/clangd_ext.rs 🔗

@@ -12,7 +12,7 @@ use crate::{element::register_action, Editor, SwitchSourceHeader};
 static CLANGD_SERVER_NAME: &str = "clangd";
 
 fn is_c_language(language: &Language) -> bool {
-    return language.name().as_ref() == "C++" || language.name().as_ref() == "C";
+    return language.name() == "C++".into() || language.name() == "C".into();
 }
 
 pub fn switch_source_header(

crates/editor/src/editor.rs 🔗

@@ -12465,7 +12465,7 @@ fn inlay_hint_settings(
     let language = snapshot.language_at(location);
     let settings = all_language_settings(file, cx);
     settings
-        .language(language.map(|l| l.name()).as_deref())
+        .language(language.map(|l| l.name()).as_ref())
         .inlay_hints
 }
 

crates/editor/src/editor_tests.rs 🔗

@@ -20,8 +20,8 @@ use language::{
     },
     BracketPairConfig,
     Capability::ReadWrite,
-    FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
-    ParsedMarkdown, Point,
+    FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
+    LanguageName, Override, ParsedMarkdown, Point,
 };
 use language_settings::{Formatter, FormatterList, IndentGuideSettings};
 use multi_buffer::MultiBufferIndentGuide;
@@ -9587,12 +9587,12 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
     let server_restarts = Arc::new(AtomicUsize::new(0));
     let closure_restarts = Arc::clone(&server_restarts);
     let language_server_name = "test language server";
-    let language_name: Arc<str> = "Rust".into();
+    let language_name: LanguageName = "Rust".into();
 
     let language_registry = project.read_with(cx, |project, _| project.languages().clone());
     language_registry.add(Arc::new(Language::new(
         LanguageConfig {
-            name: Arc::clone(&language_name),
+            name: language_name.clone(),
             matcher: LanguageMatcher {
                 path_suffixes: vec!["rs".to_string()],
                 ..Default::default()
@@ -9629,7 +9629,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test
     let _fake_server = fake_servers.next().await.unwrap();
     update_test_language_settings(cx, |language_settings| {
         language_settings.languages.insert(
-            Arc::clone(&language_name),
+            language_name.clone(),
             LanguageSettingsContent {
                 tab_size: NonZeroU32::new(8),
                 ..Default::default()

crates/editor/src/items.rs 🔗

@@ -1705,8 +1705,8 @@ mod tests {
 
                 let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx);
                 assert_eq!(
-                    buffer.language().map(|lang| lang.name()).as_deref(),
-                    Some("Rust")
+                    buffer.language().map(|lang| lang.name()),
+                    Some("Rust".into())
                 ); // Language should be set to Rust
                 assert!(buffer.file().is_none()); // The buffer should not have an associated file
             });

crates/editor/src/rust_analyzer_ext.rs 🔗

@@ -13,7 +13,7 @@ use crate::{
 static RUST_ANALYZER_NAME: &str = "rust-analyzer";
 
 fn is_rust_language(language: &Language) -> bool {
-    language.name().as_ref() == "Rust"
+    language.name() == "Rust".into()
 }
 
 pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {

crates/editor/src/test/editor_lsp_test_context.rs 🔗

@@ -58,7 +58,7 @@ impl EditorLspTestContext {
 
         let language_registry = project.read_with(cx, |project, _| project.languages().clone());
         let mut fake_servers = language_registry.register_fake_lsp_adapter(
-            language.name().as_ref(),
+            language.name(),
             FakeLspAdapter {
                 capabilities,
                 ..Default::default()

crates/extension/src/extension_lsp_adapter.rs 🔗

@@ -38,7 +38,6 @@ impl LspAdapter for ExtensionLspAdapter {
 
     fn get_language_server_command<'a>(
         self: Arc<Self>,
-        _: Arc<Language>,
         _: Arc<Path>,
         delegate: Arc<dyn LspAdapterDelegate>,
         _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,

crates/extension/src/extension_manifest.rs 🔗

@@ -1,7 +1,7 @@
 use anyhow::{anyhow, Context, Result};
 use collections::{BTreeMap, HashMap};
 use fs::Fs;
-use language::LanguageServerName;
+use language::{LanguageName, LanguageServerName};
 use semantic_version::SemanticVersion;
 use serde::{Deserialize, Serialize};
 use std::{
@@ -106,10 +106,10 @@ pub struct GrammarManifestEntry {
 pub struct LanguageServerManifestEntry {
     /// Deprecated in favor of `languages`.
     #[serde(default)]
-    language: Option<Arc<str>>,
+    language: Option<LanguageName>,
     /// The list of languages this language server should work with.
     #[serde(default)]
-    languages: Vec<Arc<str>>,
+    languages: Vec<LanguageName>,
     #[serde(default)]
     pub language_ids: HashMap<String, String>,
     #[serde(default)]
@@ -124,7 +124,7 @@ impl LanguageServerManifestEntry {
     ///
     /// We can replace this with just field access for the `languages` field once
     /// we have removed `language`.
-    pub fn languages(&self) -> impl IntoIterator<Item = Arc<str>> + '_ {
+    pub fn languages(&self) -> impl IntoIterator<Item = LanguageName> + '_ {
         let language = if self.languages.is_empty() {
             self.language.clone()
         } else {

crates/extension/src/extension_store.rs 🔗

@@ -36,7 +36,8 @@ use gpui::{
 use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
 use indexed_docs::{IndexedDocsRegistry, ProviderId};
 use language::{
-    LanguageConfig, LanguageMatcher, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES,
+    LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry,
+    QUERY_FILENAME_PREFIXES,
 };
 use node_runtime::NodeRuntime;
 use project::ContextProviderWithTasks;
@@ -148,7 +149,7 @@ impl Global for GlobalExtensionStore {}
 pub struct ExtensionIndex {
     pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>,
     pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>,
-    pub languages: BTreeMap<Arc<str>, ExtensionIndexLanguageEntry>,
+    pub languages: BTreeMap<LanguageName, ExtensionIndexLanguageEntry>,
 }
 
 #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]

crates/extension/src/extension_store_test.rs 🔗

@@ -609,7 +609,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
         .await
         .unwrap();
 
-    let mut fake_servers = language_registry.fake_language_servers("Gleam");
+    let mut fake_servers = language_registry.fake_language_servers("Gleam".into());
 
     let buffer = project
         .update(cx, |project, cx| {

crates/extension/src/wasm_host/wit/since_v0_1_0.rs 🔗

@@ -9,6 +9,7 @@ use futures::{io::BufReader, FutureExt as _};
 use futures::{lock::Mutex, AsyncReadExt};
 use indexed_docs::IndexedDocsDatabase;
 use isahc::config::{Configurable, RedirectPolicy};
+use language::LanguageName;
 use language::{
     language_settings::AllLanguageSettings, LanguageServerBinaryStatus, LspAdapterDelegate,
 };
@@ -399,8 +400,9 @@ impl ExtensionImports for WasmState {
 
                 cx.update(|cx| match category.as_str() {
                     "language" => {
+                        let key = key.map(|k| LanguageName::new(&k));
                         let settings =
-                            AllLanguageSettings::get(location, cx).language(key.as_deref());
+                            AllLanguageSettings::get(location, cx).language(key.as_ref());
                         Ok(serde_json::to_string(&settings::LanguageSettings {
                             tab_size: settings.tab_size,
                         })?)

crates/gpui/src/app.rs 🔗

@@ -1504,3 +1504,9 @@ pub struct KeystrokeEvent {
     /// The action that was resolved for the keystroke, if any
     pub action: Option<Box<dyn Action>>,
 }
+
+impl Drop for AppContext {
+    fn drop(&mut self) {
+        println!("Dropping the App Context");
+    }
+}

crates/language/src/buffer_tests.rs 🔗

@@ -72,7 +72,7 @@ fn test_select_language(cx: &mut AppContext) {
     let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
     registry.add(Arc::new(Language::new(
         LanguageConfig {
-            name: "Rust".into(),
+            name: LanguageName::new("Rust"),
             matcher: LanguageMatcher {
                 path_suffixes: vec!["rs".to_string()],
                 ..Default::default()
@@ -83,7 +83,7 @@ fn test_select_language(cx: &mut AppContext) {
     )));
     registry.add(Arc::new(Language::new(
         LanguageConfig {
-            name: "Make".into(),
+            name: LanguageName::new("Make"),
             matcher: LanguageMatcher {
                 path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
                 ..Default::default()
@@ -97,15 +97,13 @@ fn test_select_language(cx: &mut AppContext) {
     assert_eq!(
         registry
             .language_for_file(&file("src/lib.rs"), None, cx)
-            .now_or_never()
-            .and_then(|l| Some(l.ok()?.name())),
+            .map(|l| l.name()),
         Some("Rust".into())
     );
     assert_eq!(
         registry
             .language_for_file(&file("src/lib.mk"), None, cx)
-            .now_or_never()
-            .and_then(|l| Some(l.ok()?.name())),
+            .map(|l| l.name()),
         Some("Make".into())
     );
 
@@ -113,8 +111,7 @@ fn test_select_language(cx: &mut AppContext) {
     assert_eq!(
         registry
             .language_for_file(&file("src/Makefile"), None, cx)
-            .now_or_never()
-            .and_then(|l| Some(l.ok()?.name())),
+            .map(|l| l.name()),
         Some("Make".into())
     );
 
@@ -122,22 +119,19 @@ fn test_select_language(cx: &mut AppContext) {
     assert_eq!(
         registry
             .language_for_file(&file("zed/cars"), None, cx)
-            .now_or_never()
-            .and_then(|l| Some(l.ok()?.name())),
+            .map(|l| l.name()),
         None
     );
     assert_eq!(
         registry
             .language_for_file(&file("zed/a.cars"), None, cx)
-            .now_or_never()
-            .and_then(|l| Some(l.ok()?.name())),
+            .map(|l| l.name()),
         None
     );
     assert_eq!(
         registry
             .language_for_file(&file("zed/sumk"), None, cx)
-            .now_or_never()
-            .and_then(|l| Some(l.ok()?.name())),
+            .map(|l| l.name()),
         None
     );
 }
@@ -158,23 +152,22 @@ async fn test_first_line_pattern(cx: &mut TestAppContext) {
         ..Default::default()
     });
 
-    cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx))
-        .await
-        .unwrap_err();
-    cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx))
-        .await
-        .unwrap_err();
+    assert!(cx
+        .read(|cx| languages.language_for_file(&file("the/script"), None, cx))
+        .is_none());
+    assert!(cx
+        .read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx))
+        .is_none());
+
     assert_eq!(
         cx.read(|cx| languages.language_for_file(
             &file("the/script"),
             Some(&"#!/bin/env node".into()),
             cx
         ))
-        .await
         .unwrap()
-        .name()
-        .as_ref(),
-        "JavaScript"
+        .name(),
+        "JavaScript".into()
     );
 }
 
@@ -242,19 +235,16 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
 
     let language = cx
         .read(|cx| languages.language_for_file(&file("foo.js"), None, cx))
-        .await
         .unwrap();
-    assert_eq!(language.name().as_ref(), "TypeScript");
+    assert_eq!(language.name(), "TypeScript".into());
     let language = cx
         .read(|cx| languages.language_for_file(&file("foo.c"), None, cx))
-        .await
         .unwrap();
-    assert_eq!(language.name().as_ref(), "C++");
+    assert_eq!(language.name(), "C++".into());
     let language = cx
         .read(|cx| languages.language_for_file(&file("Dockerfile.dev"), None, cx))
-        .await
         .unwrap();
-    assert_eq!(language.name().as_ref(), "Dockerfile");
+    assert_eq!(language.name(), "Dockerfile".into());
 }
 
 fn file(path: &str) -> Arc<dyn File> {
@@ -2245,10 +2235,10 @@ fn test_language_at_with_hidden_languages(cx: &mut AppContext) {
 
         for point in [Point::new(0, 4), Point::new(0, 16)] {
             let config = snapshot.language_scope_at(point).unwrap();
-            assert_eq!(config.language_name().as_ref(), "Markdown");
+            assert_eq!(config.language_name(), "Markdown".into());
 
             let language = snapshot.language_at(point).unwrap();
-            assert_eq!(language.name().as_ref(), "Markdown");
+            assert_eq!(language.name().0.as_ref(), "Markdown");
         }
 
         buffer
@@ -2757,7 +2747,7 @@ fn ruby_lang() -> Language {
 fn html_lang() -> Language {
     Language::new(
         LanguageConfig {
-            name: "HTML".into(),
+            name: LanguageName::new("HTML"),
             block_comment: Some(("<!--".into(), "-->".into())),
             ..Default::default()
         },

crates/language/src/language.rs 🔗

@@ -28,6 +28,7 @@ use futures::Future;
 use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task};
 pub use highlight_map::HighlightMap;
 use http_client::HttpClient;
+pub use language_registry::LanguageName;
 use lsp::{CodeActionKind, LanguageServerBinary};
 use parking_lot::Mutex;
 use regex::Regex;
@@ -67,8 +68,8 @@ pub use buffer::Operation;
 pub use buffer::*;
 pub use diagnostic_set::DiagnosticEntry;
 pub use language_registry::{
-    LanguageNotFound, LanguageQueries, LanguageRegistry, LanguageServerBinaryStatus,
-    PendingLanguageServer, QUERY_FILENAME_PREFIXES,
+    AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry,
+    LanguageServerBinaryStatus, PendingLanguageServer, QUERY_FILENAME_PREFIXES,
 };
 pub use lsp::LanguageServerId;
 pub use outline::*;
@@ -140,6 +141,12 @@ pub trait ToLspPosition {
 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
 pub struct LanguageServerName(pub Arc<str>);
 
+impl LanguageServerName {
+    pub fn from_proto(s: String) -> Self {
+        Self(Arc::from(s))
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct Location {
     pub buffer: Model<Buffer>,
@@ -195,9 +202,12 @@ impl CachedLspAdapter {
         })
     }
 
+    pub fn name(&self) -> Arc<str> {
+        self.adapter.name().0.clone()
+    }
+
     pub async fn get_language_server_command(
         self: Arc<Self>,
-        language: Arc<Language>,
         container_dir: Arc<Path>,
         delegate: Arc<dyn LspAdapterDelegate>,
         cx: &mut AsyncAppContext,
@@ -205,18 +215,10 @@ impl CachedLspAdapter {
         let cached_binary = self.cached_binary.lock().await;
         self.adapter
             .clone()
-            .get_language_server_command(language, container_dir, delegate, cached_binary, cx)
+            .get_language_server_command(container_dir, delegate, cached_binary, cx)
             .await
     }
 
-    pub fn will_start_server(
-        &self,
-        delegate: &Arc<dyn LspAdapterDelegate>,
-        cx: &mut AsyncAppContext,
-    ) -> Option<Task<Result<()>>> {
-        self.adapter.will_start_server(delegate, cx)
-    }
-
     pub fn can_be_reinstalled(&self) -> bool {
         self.adapter.can_be_reinstalled()
     }
@@ -262,11 +264,11 @@ impl CachedLspAdapter {
             .await
     }
 
-    pub fn language_id(&self, language: &Language) -> String {
+    pub fn language_id(&self, language_name: &LanguageName) -> String {
         self.language_ids
-            .get(language.name().as_ref())
+            .get(language_name.0.as_ref())
             .cloned()
-            .unwrap_or_else(|| language.lsp_id())
+            .unwrap_or_else(|| language_name.lsp_id())
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -296,7 +298,6 @@ pub trait LspAdapter: 'static + Send + Sync {
 
     fn get_language_server_command<'a>(
         self: Arc<Self>,
-        language: Arc<Language>,
         container_dir: Arc<Path>,
         delegate: Arc<dyn LspAdapterDelegate>,
         mut cached_binary: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,
@@ -317,7 +318,7 @@ pub trait LspAdapter: 'static + Send + Sync {
             if let Some(binary) = self.check_if_user_installed(delegate.as_ref(), cx).await {
                 log::info!(
                     "found user-installed language server for {}. path: {:?}, arguments: {:?}",
-                    language.name(),
+                    self.name().0,
                     binary.path,
                     binary.arguments
                 );
@@ -387,14 +388,6 @@ pub trait LspAdapter: 'static + Send + Sync {
         None
     }
 
-    fn will_start_server(
-        &self,
-        _: &Arc<dyn LspAdapterDelegate>,
-        _: &mut AsyncAppContext,
-    ) -> Option<Task<Result<()>>> {
-        None
-    }
-
     async fn fetch_server_binary(
         &self,
         latest_version: Box<dyn 'static + Send + Any>,
@@ -562,7 +555,7 @@ pub struct CodeLabel {
 #[derive(Clone, Deserialize, JsonSchema)]
 pub struct LanguageConfig {
     /// Human-readable name of the language.
-    pub name: Arc<str>,
+    pub name: LanguageName,
     /// The name of this language for a Markdown code fence block
     pub code_fence_block_name: Option<Arc<str>>,
     // The name of the grammar in a WASM bundle (experimental).
@@ -699,7 +692,7 @@ impl<T> Override<T> {
 impl Default for LanguageConfig {
     fn default() -> Self {
         Self {
-            name: Arc::default(),
+            name: LanguageName::new(""),
             code_fence_block_name: None,
             grammar: None,
             matcher: LanguageMatcher::default(),
@@ -1335,7 +1328,7 @@ impl Language {
         Arc::get_mut(self.grammar.as_mut()?)
     }
 
-    pub fn name(&self) -> Arc<str> {
+    pub fn name(&self) -> LanguageName {
         self.config.name.clone()
     }
 
@@ -1343,7 +1336,7 @@ impl Language {
         self.config
             .code_fence_block_name
             .clone()
-            .unwrap_or_else(|| self.config.name.to_lowercase().into())
+            .unwrap_or_else(|| self.config.name.0.to_lowercase().into())
     }
 
     pub fn context_provider(&self) -> Option<Arc<dyn ContextProvider>> {
@@ -1408,10 +1401,7 @@ impl Language {
     }
 
     pub fn lsp_id(&self) -> String {
-        match self.config.name.as_ref() {
-            "Plain Text" => "plaintext".to_string(),
-            language_name => language_name.to_lowercase(),
-        }
+        self.config.name.lsp_id()
     }
 
     pub fn prettier_parser_name(&self) -> Option<&str> {
@@ -1420,7 +1410,7 @@ impl Language {
 }
 
 impl LanguageScope {
-    pub fn language_name(&self) -> Arc<str> {
+    pub fn language_name(&self) -> LanguageName {
         self.language.config.name.clone()
     }
 
@@ -1663,7 +1653,6 @@ impl LspAdapter for FakeLspAdapter {
 
     fn get_language_server_command<'a>(
         self: Arc<Self>,
-        _: Arc<Language>,
         _: Arc<Path>,
         _: Arc<dyn LspAdapterDelegate>,
         _: futures::lock::MutexGuard<'a, Option<LanguageServerBinary>>,

crates/language/src/language_registry.rs 🔗

@@ -6,9 +6,9 @@ use crate::{
     with_parser, CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher,
     LanguageServerName, LspAdapter, LspAdapterDelegate, PLAIN_TEXT,
 };
-use anyhow::{anyhow, Context as _, Result};
+use anyhow::{anyhow, Context, Result};
 use collections::{hash_map, HashMap, HashSet};
-use futures::TryFutureExt;
+
 use futures::{
     channel::{mpsc, oneshot},
     future::Shared,
@@ -19,8 +19,10 @@ use gpui::{AppContext, BackgroundExecutor, Task};
 use lsp::LanguageServerId;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use std::{
-    borrow::Cow,
+    borrow::{Borrow, Cow},
     ffi::OsStr,
     ops::Not,
     path::{Path, PathBuf},
@@ -32,6 +34,48 @@ use theme::Theme;
 use unicase::UniCase;
 use util::{maybe, paths::PathExt, post_inc, ResultExt};
 
+#[derive(
+    Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
+)]
+pub struct LanguageName(pub Arc<str>);
+
+impl LanguageName {
+    pub fn new(s: &str) -> Self {
+        Self(Arc::from(s))
+    }
+
+    pub fn from_proto(s: String) -> Self {
+        Self(Arc::from(s))
+    }
+    pub fn to_proto(self) -> String {
+        self.0.to_string()
+    }
+    pub fn lsp_id(&self) -> String {
+        match self.0.as_ref() {
+            "Plain Text" => "plaintext".to_string(),
+            language_name => language_name.to_lowercase(),
+        }
+    }
+}
+
+impl Borrow<str> for LanguageName {
+    fn borrow(&self) -> &str {
+        self.0.as_ref()
+    }
+}
+
+impl std::fmt::Display for LanguageName {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl<'a> From<&'a str> for LanguageName {
+    fn from(str: &'a str) -> LanguageName {
+        LanguageName(str.into())
+    }
+}
+
 pub struct LanguageRegistry {
     state: RwLock<LanguageRegistryState>,
     language_server_download_dir: Option<Arc<Path>>,
@@ -46,7 +90,7 @@ struct LanguageRegistryState {
     language_settings: AllLanguageSettingsContent,
     available_languages: Vec<AvailableLanguage>,
     grammars: HashMap<Arc<str>, AvailableGrammar>,
-    lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
+    lsp_adapters: HashMap<LanguageName, Vec<Arc<CachedLspAdapter>>>,
     available_lsp_adapters:
         HashMap<LanguageServerName, Arc<dyn Fn() -> Arc<CachedLspAdapter> + 'static + Send + Sync>>,
     loading_languages: HashMap<LanguageId, Vec<oneshot::Sender<Result<Arc<Language>>>>>,
@@ -56,8 +100,10 @@ struct LanguageRegistryState {
     reload_count: usize,
 
     #[cfg(any(test, feature = "test-support"))]
-    fake_server_txs:
-        HashMap<Arc<str>, Vec<futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>>>,
+    fake_server_txs: HashMap<
+        LanguageName,
+        Vec<futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>>,
+    >,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -75,9 +121,9 @@ pub struct PendingLanguageServer {
 }
 
 #[derive(Clone)]
-struct AvailableLanguage {
+pub struct AvailableLanguage {
     id: LanguageId,
-    name: Arc<str>,
+    name: LanguageName,
     grammar: Option<Arc<str>>,
     matcher: LanguageMatcher,
     load: Arc<
@@ -93,6 +139,16 @@ struct AvailableLanguage {
     loaded: bool,
 }
 
+impl AvailableLanguage {
+    pub fn name(&self) -> LanguageName {
+        self.name.clone()
+    }
+
+    pub fn matcher(&self) -> &LanguageMatcher {
+        &self.matcher
+    }
+}
+
 enum AvailableGrammar {
     Native(tree_sitter::Language),
     Loaded(#[allow(unused)] PathBuf, tree_sitter::Language),
@@ -196,7 +252,7 @@ impl LanguageRegistry {
     /// appended to the end.
     pub fn reorder_language_servers(
         &self,
-        language: &Arc<Language>,
+        language: &LanguageName,
         ordered_lsp_adapters: Vec<Arc<CachedLspAdapter>>,
     ) {
         self.state
@@ -207,7 +263,7 @@ impl LanguageRegistry {
     /// Removes the specified languages and grammars from the registry.
     pub fn remove_languages(
         &self,
-        languages_to_remove: &[Arc<str>],
+        languages_to_remove: &[LanguageName],
         grammars_to_remove: &[Arc<str>],
     ) {
         self.state
@@ -215,7 +271,7 @@ impl LanguageRegistry {
             .remove_languages(languages_to_remove, grammars_to_remove)
     }
 
-    pub fn remove_lsp_adapter(&self, language_name: &str, name: &LanguageServerName) {
+    pub fn remove_lsp_adapter(&self, language_name: &LanguageName, name: &LanguageServerName) {
         let mut state = self.state.write();
         if let Some(adapters) = state.lsp_adapters.get_mut(language_name) {
             adapters.retain(|adapter| &adapter.name != name)
@@ -267,7 +323,7 @@ impl LanguageRegistry {
         Some(load_lsp_adapter())
     }
 
-    pub fn register_lsp_adapter(&self, language_name: Arc<str>, adapter: Arc<dyn LspAdapter>) {
+    pub fn register_lsp_adapter(&self, language_name: LanguageName, adapter: Arc<dyn LspAdapter>) {
         self.state
             .write()
             .lsp_adapters
@@ -279,13 +335,14 @@ impl LanguageRegistry {
     #[cfg(any(feature = "test-support", test))]
     pub fn register_fake_lsp_adapter(
         &self,
-        language_name: &str,
+        language_name: impl Into<LanguageName>,
         adapter: crate::FakeLspAdapter,
     ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
+        let language_name = language_name.into();
         self.state
             .write()
             .lsp_adapters
-            .entry(language_name.into())
+            .entry(language_name.clone())
             .or_default()
             .push(CachedLspAdapter::new(Arc::new(adapter)));
         self.fake_language_servers(language_name)
@@ -294,13 +351,13 @@ impl LanguageRegistry {
     #[cfg(any(feature = "test-support", test))]
     pub fn fake_language_servers(
         &self,
-        language_name: &str,
+        language_name: LanguageName,
     ) -> futures::channel::mpsc::UnboundedReceiver<lsp::FakeLanguageServer> {
         let (servers_tx, servers_rx) = futures::channel::mpsc::unbounded();
         self.state
             .write()
             .fake_server_txs
-            .entry(language_name.into())
+            .entry(language_name)
             .or_default()
             .push(servers_tx);
         servers_rx
@@ -309,7 +366,7 @@ impl LanguageRegistry {
     /// Adds a language to the registry, which can be loaded if needed.
     pub fn register_language(
         &self,
-        name: Arc<str>,
+        name: LanguageName,
         grammar_name: Option<Arc<str>>,
         matcher: LanguageMatcher,
         load: impl Fn() -> Result<(
@@ -445,7 +502,7 @@ impl LanguageRegistry {
     ) -> impl Future<Output = Result<Arc<Language>>> {
         let name = UniCase::new(name);
         let rx = self.get_or_load_language(|language_name, _| {
-            if UniCase::new(language_name) == name {
+            if UniCase::new(&language_name.0) == name {
                 1
             } else {
                 0
@@ -460,7 +517,7 @@ impl LanguageRegistry {
     ) -> impl Future<Output = Result<Arc<Language>>> {
         let string = UniCase::new(string);
         let rx = self.get_or_load_language(|name, config| {
-            if UniCase::new(name) == string
+            if UniCase::new(&name.0) == string
                 || config
                     .path_suffixes
                     .iter()
@@ -474,13 +531,26 @@ impl LanguageRegistry {
         async move { rx.await? }
     }
 
+    pub fn available_language_for_name(
+        self: &Arc<Self>,
+        name: &LanguageName,
+    ) -> Option<AvailableLanguage> {
+        let state = self.state.read();
+        state
+            .available_languages
+            .iter()
+            .find(|l| &l.name == name)
+            .cloned()
+    }
+
     pub fn language_for_file(
         self: &Arc<Self>,
         file: &Arc<dyn File>,
         content: Option<&Rope>,
         cx: &AppContext,
-    ) -> impl Future<Output = Result<Arc<Language>>> {
+    ) -> Option<AvailableLanguage> {
         let user_file_types = all_language_settings(Some(file), cx);
+
         self.language_for_file_internal(
             &file.full_path(cx),
             content,
@@ -492,8 +562,16 @@ impl LanguageRegistry {
         self: &Arc<Self>,
         path: &'a Path,
     ) -> impl Future<Output = Result<Arc<Language>>> + 'a {
-        self.language_for_file_internal(path, None, None)
-            .map_err(|error| error.context(format!("language for file path {}", path.display())))
+        let available_language = self.language_for_file_internal(path, None, None);
+
+        let this = self.clone();
+        async move {
+            if let Some(language) = available_language {
+                this.load_language(&language).await?
+            } else {
+                Err(anyhow!(LanguageNotFound))
+            }
+        }
     }
 
     fn language_for_file_internal(
@@ -501,19 +579,19 @@ impl LanguageRegistry {
         path: &Path,
         content: Option<&Rope>,
         user_file_types: Option<&HashMap<Arc<str>, GlobSet>>,
-    ) -> impl Future<Output = Result<Arc<Language>>> {
+    ) -> Option<AvailableLanguage> {
         let filename = path.file_name().and_then(|name| name.to_str());
         let extension = path.extension_or_hidden_file_name();
         let path_suffixes = [extension, filename, path.to_str()];
         let empty = GlobSet::empty();
 
-        let rx = self.get_or_load_language(move |language_name, config| {
+        self.find_matching_language(move |language_name, config| {
             let path_matches_default_suffix = config
                 .path_suffixes
                 .iter()
                 .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
             let custom_suffixes = user_file_types
-                .and_then(|types| types.get(language_name))
+                .and_then(|types| types.get(&language_name.0))
                 .unwrap_or(&empty);
             let path_matches_custom_suffix = path_suffixes
                 .iter()
@@ -535,18 +613,15 @@ impl LanguageRegistry {
             } else {
                 0
             }
-        });
-        async move { rx.await? }
+        })
     }
 
-    fn get_or_load_language(
+    fn find_matching_language(
         self: &Arc<Self>,
-        callback: impl Fn(&str, &LanguageMatcher) -> usize,
-    ) -> oneshot::Receiver<Result<Arc<Language>>> {
-        let (tx, rx) = oneshot::channel();
-
-        let mut state = self.state.write();
-        let Some((language, _)) = state
+        callback: impl Fn(&LanguageName, &LanguageMatcher) -> usize,
+    ) -> Option<AvailableLanguage> {
+        let state = self.state.read();
+        let available_language = state
             .available_languages
             .iter()
             .filter_map(|language| {
@@ -559,15 +634,23 @@ impl LanguageRegistry {
             })
             .max_by_key(|e| e.1)
             .clone()
-        else {
-            let _ = tx.send(Err(anyhow!(LanguageNotFound)));
-            return rx;
-        };
+            .map(|(available_language, _)| available_language);
+        drop(state);
+        available_language
+    }
+
+    pub fn load_language(
+        self: &Arc<Self>,
+        language: &AvailableLanguage,
+    ) -> oneshot::Receiver<Result<Arc<Language>>> {
+        let (tx, rx) = oneshot::channel();
+
+        let mut state = self.state.write();
 
         // If the language is already loaded, resolve with it immediately.
         for loaded_language in state.languages.iter() {
             if loaded_language.id == language.id {
-                let _ = tx.send(Ok(loaded_language.clone()));
+                tx.send(Ok(loaded_language.clone())).unwrap();
                 return rx;
             }
         }
@@ -580,12 +663,15 @@ impl LanguageRegistry {
             // Otherwise, start loading the language.
             hash_map::Entry::Vacant(entry) => {
                 let this = self.clone();
+
+                let id = language.id;
+                let name = language.name.clone();
+                let language_load = language.load.clone();
+
                 self.executor
                     .spawn(async move {
-                        let id = language.id;
-                        let name = language.name.clone();
                         let language = async {
-                            let (config, queries, provider) = (language.load)()?;
+                            let (config, queries, provider) = (language_load)()?;
 
                             if let Some(grammar) = config.grammar.clone() {
                                 let grammar = Some(this.get_or_load_grammar(grammar).await?);
@@ -629,13 +715,28 @@ impl LanguageRegistry {
                         };
                     })
                     .detach();
+
                 entry.insert(vec![tx]);
             }
         }
 
+        drop(state);
         rx
     }
 
+    fn get_or_load_language(
+        self: &Arc<Self>,
+        callback: impl Fn(&LanguageName, &LanguageMatcher) -> usize,
+    ) -> oneshot::Receiver<Result<Arc<Language>>> {
+        let Some(language) = self.find_matching_language(callback) else {
+            let (tx, rx) = oneshot::channel();
+            let _ = tx.send(Err(anyhow!(LanguageNotFound)));
+            return rx;
+        };
+
+        self.load_language(&language)
+    }
+
     fn get_or_load_grammar(
         self: &Arc<Self>,
         name: Arc<str>,
@@ -702,11 +803,11 @@ impl LanguageRegistry {
         self.state.read().languages.to_vec()
     }
 
-    pub fn lsp_adapters(&self, language: &Arc<Language>) -> Vec<Arc<CachedLspAdapter>> {
+    pub fn lsp_adapters(&self, language_name: &LanguageName) -> Vec<Arc<CachedLspAdapter>> {
         self.state
             .read()
             .lsp_adapters
-            .get(&language.config.name)
+            .get(language_name)
             .cloned()
             .unwrap_or_default()
     }
@@ -723,7 +824,7 @@ impl LanguageRegistry {
     pub fn create_pending_language_server(
         self: &Arc<Self>,
         stderr_capture: Arc<Mutex<Option<String>>>,
-        language: Arc<Language>,
+        _language_name_for_tests: LanguageName,
         adapter: Arc<CachedLspAdapter>,
         root_path: Arc<Path>,
         delegate: Arc<dyn LspAdapterDelegate>,
@@ -741,7 +842,6 @@ impl LanguageRegistry {
             .clone()
             .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
             .log_err()?;
-        let language = language.clone();
         let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
         let root_path = root_path.clone();
         let login_shell_env_loaded = self.login_shell_env_loaded.clone();
@@ -756,12 +856,7 @@ impl LanguageRegistry {
 
                 let binary_result = adapter
                     .clone()
-                    .get_language_server_command(
-                        language.clone(),
-                        container_dir,
-                        delegate.clone(),
-                        &mut cx,
-                    )
+                    .get_language_server_command(container_dir, delegate.clone(), &mut cx)
                     .await;
 
                 delegate.update_status(adapter.name.clone(), LanguageServerBinaryStatus::None);
@@ -785,10 +880,6 @@ impl LanguageRegistry {
                     .initialization_options(&delegate)
                     .await?;
 
-                if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
-                    task.await?;
-                }
-
                 #[cfg(any(test, feature = "test-support"))]
                 if true {
                     let capabilities = adapter
@@ -825,7 +916,7 @@ impl LanguageRegistry {
                                         .state
                                         .write()
                                         .fake_server_txs
-                                        .get_mut(language.name().as_ref())
+                                        .get_mut(&_language_name_for_tests)
                                     {
                                         for tx in txs {
                                             tx.unbounded_send(fake_server.clone()).ok();
@@ -935,10 +1026,10 @@ impl LanguageRegistryState {
     /// appended to the end.
     fn reorder_language_servers(
         &mut self,
-        language: &Arc<Language>,
+        language_name: &LanguageName,
         ordered_lsp_adapters: Vec<Arc<CachedLspAdapter>>,
     ) {
-        let Some(lsp_adapters) = self.lsp_adapters.get_mut(&language.config.name) else {
+        let Some(lsp_adapters) = self.lsp_adapters.get_mut(language_name) else {
             return;
         };
 
@@ -959,7 +1050,7 @@ impl LanguageRegistryState {
 
     fn remove_languages(
         &mut self,
-        languages_to_remove: &[Arc<str>],
+        languages_to_remove: &[LanguageName],
         grammars_to_remove: &[Arc<str>],
     ) {
         if languages_to_remove.is_empty() && grammars_to_remove.is_empty() {

crates/language/src/language_settings.rs 🔗

@@ -1,6 +1,6 @@
 //! Provides `language`-related settings.
 
-use crate::{File, Language, LanguageServerName};
+use crate::{File, Language, LanguageName, LanguageServerName};
 use anyhow::Result;
 use collections::{HashMap, HashSet};
 use core::slice;
@@ -32,7 +32,7 @@ pub fn language_settings<'a>(
     cx: &'a AppContext,
 ) -> &'a LanguageSettings {
     let language_name = language.map(|l| l.name());
-    all_language_settings(file, cx).language(language_name.as_deref())
+    all_language_settings(file, cx).language(language_name.as_ref())
 }
 
 /// Returns the settings for all languages from the provided file.
@@ -53,7 +53,7 @@ pub struct AllLanguageSettings {
     /// The inline completion settings.
     pub inline_completions: InlineCompletionSettings,
     defaults: LanguageSettings,
-    languages: HashMap<Arc<str>, LanguageSettings>,
+    languages: HashMap<LanguageName, LanguageSettings>,
     pub(crate) file_types: HashMap<Arc<str>, GlobSet>,
 }
 
@@ -204,7 +204,7 @@ pub struct AllLanguageSettingsContent {
     pub defaults: LanguageSettingsContent,
     /// The settings for individual languages.
     #[serde(default)]
-    pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
+    pub languages: HashMap<LanguageName, LanguageSettingsContent>,
     /// Settings for associating file extensions and filenames
     /// with languages.
     #[serde(default)]
@@ -791,7 +791,7 @@ impl InlayHintSettings {
 
 impl AllLanguageSettings {
     /// Returns the [`LanguageSettings`] for the language with the specified name.
-    pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
+    pub fn language<'a>(&'a self, language_name: Option<&LanguageName>) -> &'a LanguageSettings {
         if let Some(name) = language_name {
             if let Some(overrides) = self.languages.get(name) {
                 return overrides;
@@ -821,7 +821,7 @@ impl AllLanguageSettings {
             }
         }
 
-        self.language(language.map(|l| l.name()).as_deref())
+        self.language(language.map(|l| l.name()).as_ref())
             .show_inline_completions
     }
 }

crates/language_selector/src/active_buffer_language.rs 🔗

@@ -1,13 +1,13 @@
 use editor::Editor;
 use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
-use std::sync::Arc;
+use language::LanguageName;
 use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
 use crate::LanguageSelector;
 
 pub struct ActiveBufferLanguage {
-    active_language: Option<Option<Arc<str>>>,
+    active_language: Option<Option<LanguageName>>,
     workspace: WeakView<Workspace>,
     _observe_active_editor: Option<Subscription>,
 }

crates/language_selector/src/language_selector.rs 🔗

@@ -217,7 +217,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
         let mat = &self.matches[ix];
         let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
         let mut label = mat.string.clone();
-        if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
+        if buffer_language_name.map(|n| n.0).as_deref() == Some(mat.string.as_str()) {
             label.push_str(" (current)");
         }
 

crates/language_tools/src/lsp_log.rs 🔗

@@ -683,7 +683,7 @@ impl LspLogView {
                 self.project
                     .read(cx)
                     .supplementary_language_servers(cx)
-                    .filter_map(|(&server_id, name)| {
+                    .filter_map(|(server_id, name)| {
                         let state = log_store.language_servers.get(&server_id)?;
                         Some(LogMenuItem {
                             server_id,

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -471,7 +471,7 @@ impl SyntaxTreeToolbarItemView {
 
     fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
         ButtonLike::new("syntax tree header")
-            .child(Label::new(active_layer.language.name()))
+            .child(Label::new(active_layer.language.name().0))
             .child(Label::new(format_node_range(active_layer.node())))
     }
 }

crates/languages/src/rust.rs 🔗

@@ -451,7 +451,7 @@ impl ContextProvider for RustContextProvider {
     ) -> Option<TaskTemplates> {
         const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
         let package_to_run = all_language_settings(file.as_ref(), cx)
-            .language(Some("Rust"))
+            .language(Some(&"Rust".into()))
             .tasks
             .variables
             .get(DEFAULT_RUN_NAME_STR);

crates/languages/src/yaml.rs 🔗

@@ -141,7 +141,7 @@ impl LspAdapter for YamlLspAdapter {
 
         let tab_size = cx.update(|cx| {
             AllLanguageSettings::get(Some(location), cx)
-                .language(Some("YAML"))
+                .language(Some(&"YAML".into()))
                 .tab_size
         })?;
         let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}});

crates/lsp/src/lsp.rs 🔗

@@ -89,6 +89,16 @@ pub struct LanguageServer {
 #[repr(transparent)]
 pub struct LanguageServerId(pub usize);
 
+impl LanguageServerId {
+    pub fn from_proto(id: u64) -> Self {
+        Self(id as usize)
+    }
+
+    pub fn to_proto(self) -> u64 {
+        self.0 as u64
+    }
+}
+
 /// Handle to a language server RPC activity subscription.
 pub enum Subscription {
     Notification {

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -282,7 +282,7 @@ impl MarkdownPreviewView {
         let buffer = editor.read(cx).buffer().read(cx);
         if let Some(buffer) = buffer.as_singleton() {
             if let Some(language) = buffer.read(cx).language() {
-                return language.name().as_ref() == "Markdown";
+                return language.name() == "Markdown".into();
             }
         }
         false

crates/project/src/lsp_command/signature_help.rs 🔗

@@ -86,7 +86,7 @@ impl SignatureHelp {
         } else {
             let markdown = markdown.join(str_for_join);
             let language_name = language
-                .map(|n| n.name().to_lowercase())
+                .map(|n| n.name().0.to_lowercase())
                 .unwrap_or_default();
 
             let markdown = if function_options_count >= 2 {

crates/project/src/lsp_store.rs 🔗

@@ -15,7 +15,7 @@ use async_trait::async_trait;
 use client::{proto, TypedEnvelope};
 use collections::{btree_map, BTreeMap, HashMap, HashSet};
 use futures::{
-    future::{join_all, Shared},
+    future::{join_all, BoxFuture, Shared},
     select,
     stream::FuturesUnordered,
     Future, FutureExt, StreamExt,
@@ -25,22 +25,26 @@ use gpui::{
     AppContext, AsyncAppContext, Context, Entity, EventEmitter, Model, ModelContext, PromptLevel,
     Task, WeakModel,
 };
-use http_client::HttpClient;
+use http_client::{AsyncBody, Error, HttpClient, Request, Response, Uri};
 use itertools::Itertools;
 use language::{
-    language_settings::{language_settings, AllLanguageSettings, LanguageSettings},
+    language_settings::{
+        all_language_settings, language_settings, AllLanguageSettings, LanguageSettings,
+    },
     markdown, point_to_lsp, prepare_completion_documentation,
     proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
     range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
-    DiagnosticEntry, DiagnosticSet, Documentation, File as _, Language, LanguageRegistry,
-    LanguageServerName, LocalFile, LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16,
-    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    DiagnosticEntry, DiagnosticSet, Documentation, File as _, Language, LanguageConfig,
+    LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerName, LocalFile, LspAdapter,
+    LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset,
+    ToPointUtf16, Transaction, Unclipped,
 };
 use lsp::{
-    CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
-    Edit, FileSystemWatcher, InsertTextFormat, LanguageServer, LanguageServerBinary,
-    LanguageServerId, LspRequestFuture, MessageActionItem, MessageType, OneOf, ServerHealthStatus,
-    ServerStatus, SymbolKind, TextEdit, Url, WorkDoneProgressCancelParams, WorkspaceFolder,
+    CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag,
+    DidChangeWatchedFilesRegistrationOptions, Edit, FileSystemWatcher, InsertTextFormat,
+    LanguageServer, LanguageServerBinary, LanguageServerId, LspRequestFuture, MessageActionItem,
+    MessageType, OneOf, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url,
+    WorkDoneProgressCancelParams, WorkspaceFolder,
 };
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
@@ -54,6 +58,7 @@ use similar::{ChangeTag, TextDiff};
 use smol::channel::Sender;
 use snippet::Snippet;
 use std::{
+    any::Any,
     cmp::Ordering,
     convert::TryInto,
     ffi::OsStr,
@@ -85,27 +90,86 @@ const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
 const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
 pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100);
 
+pub struct LocalLspStore {
+    http_client: Option<Arc<dyn HttpClient>>,
+    environment: Model<ProjectEnvironment>,
+    fs: Arc<dyn Fs>,
+    yarn: Model<YarnPathStore>,
+    pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
+    last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
+    language_server_watched_paths: HashMap<LanguageServerId, Model<LanguageServerWatchedPaths>>,
+    language_server_watcher_registrations:
+        HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
+    supplementary_language_servers:
+        HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
+    _subscription: gpui::Subscription,
+}
+
+impl LocalLspStore {
+    fn shutdown_language_servers(
+        &mut self,
+        _cx: &mut ModelContext<LspStore>,
+    ) -> impl Future<Output = ()> {
+        let shutdown_futures = self
+            .language_servers
+            .drain()
+            .map(|(_, server_state)| async {
+                use LanguageServerState::*;
+                match server_state {
+                    Running { server, .. } => server.shutdown()?.await,
+                    Starting(task) => task.await?.shutdown()?.await,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        async move {
+            futures::future::join_all(shutdown_futures).await;
+        }
+    }
+}
+
+pub struct RemoteLspStore {
+    upstream_client: AnyProtoClient,
+}
+
+impl RemoteLspStore {}
+
+pub struct SshLspStore {
+    upstream_client: AnyProtoClient,
+}
+
+#[allow(clippy::large_enum_variant)]
+pub enum LspStoreMode {
+    Local(LocalLspStore),   // ssh host and collab host
+    Remote(RemoteLspStore), // collab guest
+    Ssh(SshLspStore),       // ssh client
+}
+
+impl LspStoreMode {
+    fn is_local(&self) -> bool {
+        matches!(self, LspStoreMode::Local(_))
+    }
+
+    fn is_ssh(&self) -> bool {
+        matches!(self, LspStoreMode::Ssh(_))
+    }
+
+    fn is_remote(&self) -> bool {
+        matches!(self, LspStoreMode::Remote(_))
+    }
+}
+
 pub struct LspStore {
+    mode: LspStoreMode,
     downstream_client: Option<AnyProtoClient>,
-    upstream_client: Option<AnyProtoClient>,
     project_id: u64,
-    http_client: Option<Arc<dyn HttpClient>>,
-    fs: Arc<dyn Fs>,
     nonce: u128,
     buffer_store: Model<BufferStore>,
     worktree_store: Model<WorktreeStore>,
     buffer_snapshots: HashMap<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
-    environment: Option<Model<ProjectEnvironment>>,
-    supplementary_language_servers:
-        HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
-    languages: Arc<LanguageRegistry>,
-    language_servers: HashMap<LanguageServerId, LanguageServerState>,
+    pub languages: Arc<LanguageRegistry>,
     language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
-    language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
-    last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
-    language_server_watched_paths: HashMap<LanguageServerId, Model<LanguageServerWatchedPaths>>,
-    language_server_watcher_registrations:
-        HashMap<LanguageServerId, HashMap<String, Vec<FileSystemWatcher>>>,
+    pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
     active_entry: Option<ProjectEntryId>,
     _maintain_workspace_config: Task<Result<()>>,
     _maintain_buffer_languages: Task<()>,
@@ -122,8 +186,6 @@ pub struct LspStore {
             )>,
         >,
     >,
-    yarn: Model<YarnPathStore>,
-    _subscription: gpui::Subscription,
 }
 
 pub enum LspStoreEvent {
@@ -209,17 +271,53 @@ impl LspStore {
         client.add_model_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
     }
 
-    #[allow(clippy::too_many_arguments)]
-    pub fn new(
+    pub fn as_remote(&self) -> Option<&RemoteLspStore> {
+        match &self.mode {
+            LspStoreMode::Remote(remote_lsp_store) => Some(remote_lsp_store),
+            _ => None,
+        }
+    }
+
+    pub fn as_ssh(&self) -> Option<&SshLspStore> {
+        match &self.mode {
+            LspStoreMode::Ssh(ssh_lsp_store) => Some(ssh_lsp_store),
+            _ => None,
+        }
+    }
+
+    pub fn as_local(&self) -> Option<&LocalLspStore> {
+        match &self.mode {
+            LspStoreMode::Local(local_lsp_store) => Some(local_lsp_store),
+            _ => None,
+        }
+    }
+
+    pub fn as_local_mut(&mut self) -> Option<&mut LocalLspStore> {
+        match &mut self.mode {
+            LspStoreMode::Local(local_lsp_store) => Some(local_lsp_store),
+            _ => None,
+        }
+    }
+
+    pub fn upstream_client(&self) -> Option<AnyProtoClient> {
+        match &self.mode {
+            LspStoreMode::Ssh(SshLspStore {
+                upstream_client, ..
+            })
+            | LspStoreMode::Remote(RemoteLspStore {
+                upstream_client, ..
+            }) => Some(upstream_client.clone()),
+            LspStoreMode::Local(_) => None,
+        }
+    }
+
+    pub fn new_local(
         buffer_store: Model<BufferStore>,
         worktree_store: Model<WorktreeStore>,
-        environment: Option<Model<ProjectEnvironment>>,
+        environment: Model<ProjectEnvironment>,
         languages: Arc<LanguageRegistry>,
         http_client: Option<Arc<dyn HttpClient>>,
         fs: Arc<dyn Fs>,
-        downstream_client: Option<AnyProtoClient>,
-        upstream_client: Option<AnyProtoClient>,
-        remote_id: Option<u64>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let yarn = YarnPathStore::new(fs.clone(), cx);
@@ -229,32 +327,118 @@ impl LspStore {
             .detach();
 
         Self {
-            downstream_client,
-            upstream_client,
-            http_client,
-            fs,
-            project_id: remote_id.unwrap_or(0),
+            mode: LspStoreMode::Local(LocalLspStore {
+                supplementary_language_servers: Default::default(),
+                language_servers: Default::default(),
+                last_workspace_edits_by_language_server: Default::default(),
+                language_server_watched_paths: Default::default(),
+                language_server_watcher_registrations: Default::default(),
+                environment,
+                http_client,
+                fs,
+                yarn,
+                _subscription: cx.on_app_quit(|this, cx| {
+                    this.as_local_mut().unwrap().shutdown_language_servers(cx)
+                }),
+            }),
+            downstream_client: None,
+            project_id: 0,
+            buffer_store,
+            worktree_store,
+            languages: languages.clone(),
+            language_server_ids: Default::default(),
+            language_server_statuses: Default::default(),
+            nonce: StdRng::from_entropy().gen(),
+            buffer_snapshots: Default::default(),
+            next_diagnostic_group_id: Default::default(),
+            diagnostic_summaries: Default::default(),
+            diagnostics: Default::default(),
+            active_entry: None,
+            _maintain_workspace_config: Self::maintain_workspace_config(cx),
+            _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
+        }
+    }
+
+    fn send_lsp_proto_request<R: LspCommand>(
+        &self,
+        buffer: Model<Buffer>,
+        client: AnyProtoClient,
+        request: R,
+        cx: &mut ModelContext<'_, LspStore>,
+    ) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
+        let message = request.to_proto(self.project_id, buffer.read(cx));
+        cx.spawn(move |this, cx| async move {
+            let response = client.request(message).await?;
+            let this = this.upgrade().context("project dropped")?;
+            request
+                .response_from_proto(response, this, buffer, cx)
+                .await
+        })
+    }
+
+    pub fn new_ssh(
+        buffer_store: Model<BufferStore>,
+        worktree_store: Model<WorktreeStore>,
+        languages: Arc<LanguageRegistry>,
+        upstream_client: AnyProtoClient,
+        project_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        cx.subscribe(&buffer_store, Self::on_buffer_store_event)
+            .detach();
+        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
+            .detach();
+
+        Self {
+            mode: LspStoreMode::Ssh(SshLspStore { upstream_client }),
+            downstream_client: None,
+            project_id,
             buffer_store,
             worktree_store,
             languages: languages.clone(),
-            environment,
+            language_server_ids: Default::default(),
+            language_server_statuses: Default::default(),
             nonce: StdRng::from_entropy().gen(),
             buffer_snapshots: Default::default(),
-            supplementary_language_servers: Default::default(),
-            language_servers: Default::default(),
+            next_diagnostic_group_id: Default::default(),
+            diagnostic_summaries: Default::default(),
+            diagnostics: Default::default(),
+            active_entry: None,
+            _maintain_workspace_config: Self::maintain_workspace_config(cx),
+            _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
+        }
+    }
+
+    pub fn new_remote(
+        buffer_store: Model<BufferStore>,
+        worktree_store: Model<WorktreeStore>,
+        languages: Arc<LanguageRegistry>,
+        upstream_client: AnyProtoClient,
+        project_id: u64,
+        cx: &mut ModelContext<Self>,
+    ) -> Self {
+        cx.subscribe(&buffer_store, Self::on_buffer_store_event)
+            .detach();
+        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
+            .detach();
+
+        Self {
+            mode: LspStoreMode::Remote(RemoteLspStore { upstream_client }),
+            downstream_client: None,
+            project_id,
+            buffer_store,
+            worktree_store,
+            languages: languages.clone(),
             language_server_ids: Default::default(),
             language_server_statuses: Default::default(),
-            last_workspace_edits_by_language_server: Default::default(),
-            language_server_watched_paths: Default::default(),
-            language_server_watcher_registrations: Default::default(),
+            nonce: StdRng::from_entropy().gen(),
+            buffer_snapshots: Default::default(),
             next_diagnostic_group_id: Default::default(),
             diagnostic_summaries: Default::default(),
             diagnostics: Default::default(),
             active_entry: None,
-            yarn,
             _maintain_workspace_config: Self::maintain_workspace_config(cx),
             _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
-            _subscription: cx.on_app_quit(Self::shutdown_language_servers),
         }
     }
 
@@ -273,7 +457,6 @@ impl LspStore {
                     self.unregister_buffer_from_language_servers(buffer, old_file, cx);
                 }
 
-                self.detect_language_for_buffer(buffer, cx);
                 self.register_buffer_with_language_servers(buffer, cx);
             }
             BufferStoreEvent::BufferDropped(_) => {}
@@ -338,7 +521,6 @@ impl LspStore {
         })
         .detach();
 
-        self.detect_language_for_buffer(buffer, cx);
         self.register_buffer_with_language_servers(buffer, cx);
         cx.observe_release(buffer, |this, buffer, cx| {
             if let Some(file) = File::from_dyn(buffer.file()) {
@@ -406,9 +588,7 @@ impl LspStore {
                                 buffers_with_unknown_injections.push(handle);
                             }
                         }
-
                         for buffer in plain_text_buffers {
-                            this.detect_language_for_buffer(&buffer, cx);
                             this.register_buffer_with_language_servers(&buffer, cx);
                         }
 
@@ -426,34 +606,29 @@ impl LspStore {
         &mut self,
         buffer_handle: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
-    ) {
+    ) -> Option<language::AvailableLanguage> {
         // If the buffer has a language, set it and start the language server if we haven't already.
         let buffer = buffer_handle.read(cx);
-        let Some(file) = buffer.file() else {
-            return;
-        };
-        let content = buffer.as_rope();
-        let Some(new_language_result) = self
-            .languages
-            .language_for_file(file, Some(content), cx)
-            .now_or_never()
-        else {
-            return;
-        };
+        let file = buffer.file()?;
 
-        match new_language_result {
-            Err(e) => {
-                if e.is::<language::LanguageNotFound>() {
-                    cx.emit(LspStoreEvent::LanguageDetected {
-                        buffer: buffer_handle.clone(),
-                        new_language: None,
-                    });
-                }
-            }
-            Ok(new_language) => {
+        let content = buffer.as_rope();
+        let available_language = self.languages.language_for_file(file, Some(content), cx);
+        if let Some(available_language) = &available_language {
+            if let Some(Ok(Ok(new_language))) = self
+                .languages
+                .load_language(available_language)
+                .now_or_never()
+            {
                 self.set_language_for_buffer(buffer_handle, new_language, cx);
             }
-        };
+        } else {
+            cx.emit(LspStoreEvent::LanguageDetected {
+                buffer: buffer_handle.clone(),
+                new_language: None,
+            });
+        }
+
+        available_language
     }
 
     pub fn set_language_for_buffer(
@@ -475,9 +650,7 @@ impl LspStore {
 
         if let Some(file) = buffer_file {
             let worktree = file.worktree.clone();
-            if worktree.read(cx).is_local() {
-                self.start_language_servers(&worktree, new_language.clone(), cx)
-            }
+            self.start_language_servers(&worktree, new_language.name(), cx)
         }
 
         cx.emit(LspStoreEvent::LanguageDetected {
@@ -494,27 +667,6 @@ impl LspStore {
         self.active_entry = active_entry;
     }
 
-    fn shutdown_language_servers(
-        &mut self,
-        _cx: &mut ModelContext<Self>,
-    ) -> impl Future<Output = ()> {
-        let shutdown_futures = self
-            .language_servers
-            .drain()
-            .map(|(_, server_state)| async {
-                use LanguageServerState::*;
-                match server_state {
-                    Running { server, .. } => server.shutdown()?.await,
-                    Starting(task) => task.await?.shutdown()?.await,
-                }
-            })
-            .collect::<Vec<_>>();
-
-        async move {
-            futures::future::join_all(shutdown_futures).await;
-        }
-    }
-
     pub(crate) fn send_diagnostic_summaries(
         &self,
         worktree: &mut Worktree,
@@ -547,9 +699,11 @@ impl LspStore {
         <R::LspRequest as lsp::request::Request>::Params: Send,
     {
         let buffer = buffer_handle.read(cx);
-        if self.upstream_client.is_some() {
-            return self.send_lsp_proto_request(buffer_handle, self.project_id, request, cx);
+
+        if let Some(upstream_client) = self.upstream_client() {
+            return self.send_lsp_proto_request(buffer_handle, upstream_client, request, cx);
         }
+
         let language_server = match server {
             LanguageServerToQuery::Primary => {
                 match self.primary_language_server_for_buffer(buffer, cx) {
@@ -635,26 +789,6 @@ impl LspStore {
         Task::ready(Ok(Default::default()))
     }
 
-    fn send_lsp_proto_request<R: LspCommand>(
-        &self,
-        buffer: Model<Buffer>,
-        project_id: u64,
-        request: R,
-        cx: &mut ModelContext<'_, Self>,
-    ) -> Task<anyhow::Result<<R as LspCommand>::Response>> {
-        let Some(upstream_client) = self.upstream_client.clone() else {
-            return Task::ready(Err(anyhow!("disconnected before completing request")));
-        };
-        let message = request.to_proto(project_id, buffer.read(cx));
-        cx.spawn(move |this, cx| async move {
-            let response = upstream_client.request(message).await?;
-            let this = this.upgrade().context("project dropped")?;
-            request
-                .response_from_proto(response, this, buffer, cx)
-                .await
-        })
-    }
-
     pub async fn execute_code_actions_on_servers(
         this: &WeakModel<LspStore>,
         adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
@@ -702,8 +836,10 @@ impl LspStore {
 
                 if let Some(command) = action.lsp_action.command {
                     this.update(cx, |this, _| {
-                        this.last_workspace_edits_by_language_server
-                            .remove(&language_server.server_id());
+                        if let LspStoreMode::Local(mode) = &mut this.mode {
+                            mode.last_workspace_edits_by_language_server
+                                .remove(&language_server.server_id());
+                        }
                     })?;
 
                     language_server
@@ -715,12 +851,14 @@ impl LspStore {
                         .await?;
 
                     this.update(cx, |this, _| {
-                        project_transaction.0.extend(
-                            this.last_workspace_edits_by_language_server
-                                .remove(&language_server.server_id())
-                                .unwrap_or_default()
-                                .0,
-                        )
+                        if let LspStoreMode::Local(mode) = &mut this.mode {
+                            project_transaction.0.extend(
+                                mode.last_workspace_edits_by_language_server
+                                    .remove(&language_server.server_id())
+                                    .unwrap_or_default()
+                                    .0,
+                            )
+                        }
                     })?;
                 }
             }
@@ -752,7 +890,7 @@ impl LspStore {
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ProjectTransaction>> {
-        if let Some(upstream_client) = self.upstream_client.clone() {
+        if let Some(upstream_client) = self.upstream_client() {
             let request = proto::ApplyCodeAction {
                 project_id: self.project_id,
                 buffer_id: buffer_handle.read(cx).remote_id().into(),
@@ -801,7 +939,9 @@ impl LspStore {
 
                 if let Some(command) = action.lsp_action.command {
                     this.update(&mut cx, |this, _| {
-                        this.last_workspace_edits_by_language_server
+                        this.as_local_mut()
+                            .unwrap()
+                            .last_workspace_edits_by_language_server
                             .remove(&lang_server.server_id());
                     })?;
 
@@ -816,7 +956,9 @@ impl LspStore {
                     result?;
 
                     return this.update(&mut cx, |this, _| {
-                        this.last_workspace_edits_by_language_server
+                        this.as_local_mut()
+                            .unwrap()
+                            .last_workspace_edits_by_language_server
                             .remove(&lang_server.server_id())
                             .unwrap_or_default()
                     });
@@ -834,7 +976,7 @@ impl LspStore {
         server_id: LanguageServerId,
         cx: &mut ModelContext<Self>,
     ) -> Task<anyhow::Result<InlayHint>> {
-        if let Some(upstream_client) = self.upstream_client.clone() {
+        if let Some(upstream_client) = self.upstream_client() {
             let request = proto::ResolveInlayHint {
                 project_id: self.project_id,
                 buffer_id: buffer_handle.read(cx).remote_id().into(),
@@ -912,7 +1054,7 @@ impl LspStore {
             .map(|(_, server)| LanguageServerToQuery::Other(server.server_id()))
             .next()
             .or_else(|| {
-                self.upstream_client
+                self.upstream_client()
                     .is_some()
                     .then_some(LanguageServerToQuery::Primary)
             })
@@ -945,7 +1087,7 @@ impl LspStore {
         trigger: String,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Transaction>>> {
-        if let Some(client) = self.upstream_client.clone() {
+        if let Some(client) = self.upstream_client() {
             let request = proto::OnTypeFormatting {
                 project_id: self.project_id,
                 buffer_id: buffer.read(cx).remote_id().into(),
@@ -1095,7 +1237,7 @@ impl LspStore {
         range: Range<Anchor>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Vec<CodeAction>> {
-        if let Some(upstream_client) = self.upstream_client.as_ref() {
+        if let Some(upstream_client) = self.upstream_client() {
             let request_task = upstream_client.request(proto::MultiLspQuery {
                 buffer_id: buffer_handle.read(cx).remote_id().into(),
                 version: serialize_version(&buffer_handle.read(cx).version()),
@@ -1175,10 +1317,10 @@ impl LspStore {
     ) -> Task<Result<Vec<Completion>>> {
         let language_registry = self.languages.clone();
 
-        if let Some(_) = self.upstream_client.clone() {
+        if let Some(upstream_client) = self.upstream_client() {
             let task = self.send_lsp_proto_request(
                 buffer.clone(),
-                self.project_id,
+                upstream_client,
                 GetCompletions { position, context },
                 cx,
             );
@@ -1187,9 +1329,12 @@ impl LspStore {
             // In the future, we should provide project guests with the names of LSP adapters,
             // so that they can use the correct LSP adapter when computing labels. For now,
             // guests just use the first LSP adapter associated with the buffer's language.
-            let lsp_adapter = language
-                .as_ref()
-                .and_then(|language| language_registry.lsp_adapters(language).first().cloned());
+            let lsp_adapter = language.as_ref().and_then(|language| {
+                language_registry
+                    .lsp_adapters(&language.name())
+                    .first()
+                    .cloned()
+            });
 
             cx.foreground_executor().spawn(async move {
                 let completions = task.await?;
@@ -1269,7 +1414,7 @@ impl LspStore {
         completions: Arc<RwLock<Box<[Completion]>>>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<bool>> {
-        let client = self.upstream_client.clone();
+        let client = self.upstream_client();
         let language_registry = self.languages.clone();
         let project_id = self.project_id;
 
@@ -1478,7 +1623,7 @@ impl LspStore {
         let buffer = buffer_handle.read(cx);
         let buffer_id = buffer.remote_id();
 
-        if let Some(client) = self.upstream_client.clone() {
+        if let Some(client) = self.upstream_client() {
             let project_id = self.project_id;
             cx.spawn(move |_, mut cx| async move {
                 let response = client
@@ -1594,7 +1739,7 @@ impl LspStore {
         let buffer_id = buffer.remote_id().into();
         let lsp_request = InlayHints { range };
 
-        if let Some(client) = self.upstream_client.clone() {
+        if let Some(client) = self.upstream_client() {
             let request = proto::InlayHints {
                 project_id: self.project_id,
                 buffer_id,
@@ -1644,7 +1789,7 @@ impl LspStore {
     ) -> Task<Vec<SignatureHelp>> {
         let position = position.to_point_utf16(buffer.read(cx));
 
-        if let Some(client) = self.upstream_client.clone() {
+        if let Some(client) = self.upstream_client() {
             let request_task = client.request(proto::MultiLspQuery {
                 buffer_id: buffer.read(cx).remote_id().into(),
                 version: serialize_version(&buffer.read(cx).version()),
@@ -1716,7 +1861,7 @@ impl LspStore {
         position: PointUtf16,
         cx: &mut ModelContext<Self>,
     ) -> Task<Vec<Hover>> {
-        if let Some(client) = self.upstream_client.clone() {
+        if let Some(client) = self.upstream_client() {
             let request_task = client.request(proto::MultiLspQuery {
                 buffer_id: buffer.read(cx).remote_id().into(),
                 version: serialize_version(&buffer.read(cx).version()),
@@ -1790,7 +1935,7 @@ impl LspStore {
     pub fn symbols(&self, query: &str, cx: &mut ModelContext<Self>) -> Task<Result<Vec<Symbol>>> {
         let language_registry = self.languages.clone();
 
-        if let Some(upstream_client) = self.upstream_client.as_ref() {
+        if let Some(upstream_client) = self.upstream_client().as_ref() {
             let request = upstream_client.request(proto::GetProjectSymbols {
                 project_id: self.project_id,
                 query: query.to_string(),
@@ -1816,7 +1961,7 @@ impl LspStore {
         } else {
             struct WorkspaceSymbolsResult {
                 lsp_adapter: Arc<CachedLspAdapter>,
-                language: Arc<Language>,
+                language: LanguageName,
                 worktree: WeakModel<Worktree>,
                 worktree_abs_path: Arc<Path>,
                 lsp_symbols: Vec<(String, SymbolKind, lsp::Location)>,
@@ -1837,16 +1982,17 @@ impl LspStore {
                 }
                 let worktree_abs_path = worktree.abs_path().clone();
 
-                let (lsp_adapter, language, server) = match self.language_servers.get(server_id) {
-                    Some(LanguageServerState::Running {
-                        adapter,
-                        language,
-                        server,
-                        ..
-                    }) => (adapter.clone(), language.clone(), server),
+                let (lsp_adapter, language, server) =
+                    match self.as_local().unwrap().language_servers.get(server_id) {
+                        Some(LanguageServerState::Running {
+                            adapter,
+                            language,
+                            server,
+                            ..
+                        }) => (adapter.clone(), language.clone(), server),
 
-                    _ => continue,
-                };
+                        _ => continue,
+                    };
 
                 requests.push(
                     server
@@ -2105,7 +2251,7 @@ impl LspStore {
             uri: lsp::Url::from_file_path(abs_path).log_err()?,
         };
 
-        for (_, _, server) in self.language_servers_for_worktree(worktree_id) {
+        for server in self.language_servers_for_worktree(worktree_id) {
             if let Some(include_text) = include_text(server.as_ref()) {
                 let text = if include_text {
                     Some(buffer.read(cx).text())
@@ -2148,8 +2294,9 @@ impl LspStore {
                                 .worktree_store
                                 .read(cx)
                                 .worktree_for_id(*worktree_id, cx)?;
-                            let state = this.language_servers.get(server_id)?;
-                            let delegate = ProjectLspAdapterDelegate::new(this, &worktree, cx);
+                            let state = this.as_local()?.language_servers.get(server_id)?;
+                            let delegate =
+                                ProjectLspAdapterDelegate::for_local(this, &worktree, cx);
                             match state {
                                 LanguageServerState::Starting(_) => None,
                                 LanguageServerState::Running {
@@ -2204,19 +2351,15 @@ impl LspStore {
     fn language_servers_for_worktree(
         &self,
         worktree_id: WorktreeId,
-    ) -> impl Iterator<Item = (&Arc<CachedLspAdapter>, &Arc<Language>, &Arc<LanguageServer>)> {
+    ) -> impl Iterator<Item = &Arc<LanguageServer>> {
         self.language_server_ids
             .iter()
             .filter_map(move |((language_server_worktree_id, _), id)| {
                 if *language_server_worktree_id == worktree_id {
-                    if let Some(LanguageServerState::Running {
-                        adapter,
-                        language,
-                        server,
-                        ..
-                    }) = self.language_servers.get(id)
+                    if let Some(LanguageServerState::Running { server, .. }) =
+                        self.as_local()?.language_servers.get(id)
                     {
-                        return Some((adapter, language, server));
+                        return Some(server);
                     }
                 }
                 None
@@ -2241,11 +2384,17 @@ impl LspStore {
             self.language_server_ids
                 .remove(&(id_to_remove, server_name));
             self.language_server_statuses.remove(&server_id_to_remove);
-            self.language_server_watched_paths
-                .remove(&server_id_to_remove);
-            self.last_workspace_edits_by_language_server
-                .remove(&server_id_to_remove);
-            self.language_servers.remove(&server_id_to_remove);
+            if let Some(local_lsp_store) = self.as_local_mut() {
+                local_lsp_store
+                    .language_server_watched_paths
+                    .remove(&server_id_to_remove);
+                local_lsp_store
+                    .last_workspace_edits_by_language_server
+                    .remove(&server_id_to_remove);
+                local_lsp_store
+                    .language_servers
+                    .remove(&server_id_to_remove);
+            }
             cx.emit(LspStoreEvent::LanguageServerRemoved(server_id_to_remove));
         }
     }
@@ -2306,11 +2455,14 @@ impl LspStore {
             .insert((worktree_id, language_server_name), language_server_id);
     }
 
+    #[track_caller]
     pub(crate) fn register_buffer_with_language_servers(
         &mut self,
         buffer_handle: &Model<Buffer>,
         cx: &mut ModelContext<Self>,
     ) {
+        let available_language = self.detect_language_for_buffer(buffer_handle, cx);
+
         let buffer = buffer_handle.read(cx);
         let buffer_id = buffer.remote_id();
 
@@ -2324,7 +2476,6 @@ impl LspStore {
                 return;
             };
             let initial_snapshot = buffer.text_snapshot();
-            let language = buffer.language().cloned();
             let worktree_id = file.worktree_id(cx);
 
             if let Some(diagnostics) = self.diagnostics.get(&worktree_id) {
@@ -2336,12 +2487,12 @@ impl LspStore {
                 }
             }
 
-            if let Some(language) = language {
-                for adapter in self.languages.lsp_adapters(&language) {
+            if let Some(language) = available_language {
+                for adapter in self.languages.lsp_adapters(&language.name()) {
                     let server = self
                         .language_server_ids
                         .get(&(worktree_id, adapter.name.clone()))
-                        .and_then(|id| self.language_servers.get(id))
+                        .and_then(|id| self.as_local()?.language_servers.get(id))
                         .and_then(|server_state| {
                             if let LanguageServerState::Running { server, .. } = server_state {
                                 Some(server.clone())
@@ -2359,7 +2510,7 @@ impl LspStore {
                             lsp::DidOpenTextDocumentParams {
                                 text_document: lsp::TextDocumentItem::new(
                                     uri.clone(),
-                                    adapter.language_id(&language),
+                                    adapter.language_id(&language.name()),
                                     0,
                                     initial_snapshot.text(),
                                 ),
@@ -2409,7 +2560,7 @@ impl LspStore {
             let ids = &self.language_server_ids;
 
             if let Some(language) = buffer.language().cloned() {
-                for adapter in self.languages.lsp_adapters(&language) {
+                for adapter in self.languages.lsp_adapters(&language.name()) {
                     if let Some(server_id) = ids.get(&(worktree_id, adapter.name.clone())) {
                         buffer.update_diagnostics(*server_id, Default::default(), cx);
                     }
@@ -2537,7 +2688,7 @@ impl LspStore {
         symbol: &Symbol,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Model<Buffer>>> {
-        if let Some(client) = self.upstream_client.clone() {
+        if let Some(client) = self.upstream_client() {
             let request = client.request(proto::OpenBufferForSymbol {
                 project_id: self.project_id,
                 symbol: Some(Self::serialize_symbol(symbol)),
@@ -2605,7 +2756,7 @@ impl LspStore {
             let p = abs_path.clone();
             let yarn_worktree = this
                 .update(&mut cx, move |this, cx| {
-                    this.yarn.update(cx, |_, cx| {
+                    this.as_local().unwrap().yarn.update(cx, |_, cx| {
                         cx.spawn(|this, mut cx| async move {
                             let t = this
                                 .update(&mut cx, |this, cx| {
@@ -2755,7 +2906,7 @@ impl LspStore {
         <R::LspRequest as lsp::request::Request>::Result: Send,
         <R::LspRequest as lsp::request::Request>::Params: Send,
     {
-        debug_assert!(self.upstream_client.is_none());
+        debug_assert!(self.upstream_client().is_none());
 
         let snapshot = buffer.read(cx).snapshot();
         let scope = position.and_then(|position| snapshot.language_scope_at(position));
@@ -2801,7 +2952,7 @@ impl LspStore {
         <T::LspRequest as lsp::request::Request>::Params: Send,
         <T::LspRequest as lsp::request::Request>::Result: Send,
     {
-        let sender_id = envelope.original_sender_id()?;
+        let sender_id = envelope.original_sender_id().unwrap_or_default();
         let buffer_id = T::buffer_id_from_proto(&envelope.payload)?;
         let buffer_handle = this.update(&mut cx, |this, cx| {
             this.buffer_store.read(cx).get_existing(buffer_id)
@@ -2839,7 +2990,7 @@ impl LspStore {
         envelope: TypedEnvelope<proto::MultiLspQuery>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::MultiLspQueryResponse> {
-        let sender_id = envelope.original_sender_id()?;
+        let sender_id = envelope.original_sender_id().unwrap_or_default();
         let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
         let version = deserialize_version(&envelope.payload.version);
         let buffer = this.update(&mut cx, |this, cx| {
@@ -2979,7 +3130,7 @@ impl LspStore {
         envelope: TypedEnvelope<proto::ApplyCodeAction>,
         mut cx: AsyncAppContext,
     ) -> Result<proto::ApplyCodeActionResponse> {
-        let sender_id = envelope.original_sender_id()?;
+        let sender_id = envelope.original_sender_id().unwrap_or_default();
         let action = Self::deserialize_code_action(
             envelope
                 .payload
@@ -3184,7 +3335,9 @@ impl LspStore {
             simulate_disk_based_diagnostics_completion,
             adapter,
             ..
-        }) = self.language_servers.get_mut(&language_server_id)
+        }) = self
+            .as_local_mut()
+            .and_then(|local_store| local_store.language_servers.get_mut(&language_server_id))
         else {
             return;
         };
@@ -3205,8 +3358,9 @@ impl LspStore {
                     if let Some(LanguageServerState::Running {
                         simulate_disk_based_diagnostics_completion,
                         ..
-                    }) = this.language_servers.get_mut(&language_server_id)
-                    {
+                    }) = this.as_local_mut().and_then(|local_store| {
+                        local_store.language_servers.get_mut(&language_server_id)
+                    }) {
                         *simulate_disk_based_diagnostics_completion = None;
                     }
                 })
@@ -3264,7 +3418,20 @@ impl LspStore {
         language_server_id: LanguageServerId,
         cx: &mut ModelContext<Self>,
     ) {
-        let Some(watchers) = self
+        let worktrees = self
+            .worktree_store
+            .read(cx)
+            .worktrees()
+            .filter_map(|worktree| {
+                self.language_servers_for_worktree(worktree.read(cx).id())
+                    .find(|server| server.server_id() == language_server_id)
+                    .map(|_| worktree)
+            })
+            .collect::<Vec<_>>();
+
+        let local_lsp_store = self.as_local_mut().unwrap();
+
+        let Some(watchers) = local_lsp_store
             .language_server_watcher_registrations
             .get(&language_server_id)
         else {
@@ -3278,17 +3445,6 @@ impl LspStore {
             language_server_id
         );
 
-        let worktrees = self
-            .worktree_store
-            .read(cx)
-            .worktrees()
-            .filter_map(|worktree| {
-                self.language_servers_for_worktree(worktree.read(cx).id())
-                    .find(|(_, _, server)| server.server_id() == language_server_id)
-                    .map(|_| worktree)
-            })
-            .collect::<Vec<_>>();
-
         enum PathToWatch {
             Worktree {
                 literal_prefix: Arc<Path>,
@@ -3438,20 +3594,29 @@ impl LspStore {
                 watch_builder.watch_abs_path(abs_path, globset);
             }
         }
-        let watcher = watch_builder.build(self.fs.clone(), language_server_id, cx);
-        self.language_server_watched_paths
+        let watcher = watch_builder.build(local_lsp_store.fs.clone(), language_server_id, cx);
+        local_lsp_store
+            .language_server_watched_paths
             .insert(language_server_id, watcher);
 
         cx.notify();
     }
 
     pub fn language_server_for_id(&self, id: LanguageServerId) -> Option<Arc<LanguageServer>> {
-        if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) {
-            Some(server.clone())
-        } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) {
-            Some(Arc::clone(server))
-        } else {
-            None
+        if let Some(local_lsp_store) = self.as_local() {
+            if let Some(LanguageServerState::Running { server, .. }) =
+                local_lsp_store.language_servers.get(&id)
+            {
+                Some(server.clone())
+            } else if let Some((_, server)) =
+                local_lsp_store.supplementary_language_servers.get(&id)
+            {
+                Some(Arc::clone(server))
+            } else {
+                None
+            }
+        } else {
+            None
         }
     }
 

crates/project/src/project.rs 🔗

@@ -107,7 +107,7 @@ pub use buffer_store::ProjectTransaction;
 pub use lsp_store::{
     DiagnosticSummary, LanguageServerLogType, LanguageServerProgress, LanguageServerPromptRequest,
     LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
-    ProjectLspAdapterDelegate, SERVER_PROGRESS_THROTTLE_TIMEOUT,
+    SERVER_PROGRESS_THROTTLE_TIMEOUT,
 };
 
 const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
@@ -643,16 +643,13 @@ impl Project {
 
             let environment = ProjectEnvironment::new(&worktree_store, env, cx);
             let lsp_store = cx.new_model(|cx| {
-                LspStore::new(
+                LspStore::new_local(
                     buffer_store.clone(),
                     worktree_store.clone(),
-                    Some(environment.clone()),
+                    environment.clone(),
                     languages.clone(),
                     Some(client.http_client()),
                     fs.clone(),
-                    None,
-                    None,
-                    None,
                     cx,
                 )
             });
@@ -712,16 +709,89 @@ impl Project {
         fs: Arc<dyn Fs>,
         cx: &mut AppContext,
     ) -> Model<Self> {
-        let this = Self::local(client, node, user_store, languages, fs, None, cx);
-        this.update(cx, |this, cx| {
-            let client: AnyProtoClient = ssh.clone().into();
+        cx.new_model(|cx: &mut ModelContext<Self>| {
+            let (tx, rx) = mpsc::unbounded();
+            cx.spawn(move |this, cx| Self::send_buffer_ordered_messages(this, rx, cx))
+                .detach();
+            let tasks = Inventory::new(cx);
+            let global_snippets_dir = paths::config_dir().join("snippets");
+            let snippets =
+                SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
+
+            let worktree_store = cx.new_model(|_| {
+                let mut worktree_store = WorktreeStore::new(false, fs.clone());
+                worktree_store.set_upstream_client(ssh.clone().into());
+                worktree_store
+            });
+            cx.subscribe(&worktree_store, Self::on_worktree_store_event)
+                .detach();
+
+            let buffer_store =
+                cx.new_model(|cx| BufferStore::new(worktree_store.clone(), None, cx));
+            cx.subscribe(&buffer_store, Self::on_buffer_store_event)
+                .detach();
 
-            this.worktree_store.update(cx, |store, _cx| {
-                store.set_upstream_client(client.clone());
+            let settings_observer = cx.new_model(|cx| {
+                SettingsObserver::new_ssh(ssh.clone().into(), worktree_store.clone(), cx)
             });
-            this.settings_observer = cx.new_model(|cx| {
-                SettingsObserver::new_ssh(ssh.clone().into(), this.worktree_store.clone(), cx)
+
+            let environment = ProjectEnvironment::new(&worktree_store, None, cx);
+            let lsp_store = cx.new_model(|cx| {
+                LspStore::new_ssh(
+                    buffer_store.clone(),
+                    worktree_store.clone(),
+                    languages.clone(),
+                    ssh.clone().into(),
+                    0,
+                    cx,
+                )
             });
+            cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
+
+            let this = Self {
+                buffer_ordered_messages_tx: tx,
+                collaborators: Default::default(),
+                worktree_store,
+                buffer_store,
+                lsp_store,
+                current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
+                join_project_response_message_id: 0,
+                client_state: ProjectClientState::Local,
+                client_subscriptions: Vec::new(),
+                _subscriptions: vec![
+                    cx.observe_global::<SettingsStore>(Self::on_settings_changed),
+                    cx.on_release(Self::release),
+                ],
+                active_entry: None,
+                snippets,
+                languages,
+                client,
+                user_store,
+                settings_observer,
+                fs,
+                ssh_session: Some(ssh.clone()),
+                buffers_needing_diff: Default::default(),
+                git_diff_debouncer: DebouncedDelay::new(),
+                terminals: Terminals {
+                    local_handles: Vec::new(),
+                },
+                node: Some(node),
+                default_prettier: DefaultPrettier::default(),
+                prettiers_per_worktree: HashMap::default(),
+                prettier_instances: HashMap::default(),
+                tasks,
+                hosted_project_id: None,
+                dev_server_project_id: None,
+                search_history: Self::new_search_history(),
+                environment,
+                remotely_created_buffers: Default::default(),
+                last_formatting_failure: None,
+                buffers_being_formatted: Default::default(),
+                search_included_history: Self::new_search_history(),
+                search_excluded_history: Self::new_search_history(),
+            };
+
+            let client: AnyProtoClient = ssh.clone().into();
 
             ssh.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle());
             ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.buffer_store);
@@ -735,9 +805,8 @@ impl Project {
             LspStore::init(&client);
             SettingsObserver::init(&client);
 
-            this.ssh_session = Some(ssh);
-        });
-        this
+            this
+        })
     }
 
     pub async fn remote(
@@ -820,16 +889,12 @@ impl Project {
             cx.new_model(|cx| BufferStore::new(worktree_store.clone(), Some(remote_id), cx))?;
 
         let lsp_store = cx.new_model(|cx| {
-            let mut lsp_store = LspStore::new(
+            let mut lsp_store = LspStore::new_remote(
                 buffer_store.clone(),
                 worktree_store.clone(),
-                None,
                 languages.clone(),
-                Some(client.http_client()),
-                fs.clone(),
-                None,
-                Some(client.clone().into()),
-                Some(remote_id),
+                client.clone().into(),
+                remote_id,
                 cx,
             );
             lsp_store.set_language_server_statuses_from_proto(response.payload.language_servers);
@@ -1125,8 +1190,7 @@ impl Project {
             if let Some(language) = buffer_language {
                 if settings.enable_language_server {
                     if let Some(file) = buffer_file {
-                        language_servers_to_start
-                            .push((file.worktree.clone(), Arc::clone(language)));
+                        language_servers_to_start.push((file.worktree.clone(), language.name()));
                     }
                 }
                 language_formatters_to_check
@@ -1144,7 +1208,7 @@ impl Project {
             let language = languages.iter().find_map(|l| {
                 let adapter = self
                     .languages
-                    .lsp_adapters(l)
+                    .lsp_adapters(&l.name())
                     .iter()
                     .find(|adapter| adapter.name == started_lsp_name)?
                     .clone();
@@ -1165,11 +1229,11 @@ impl Project {
                     ) {
                         (None, None) => {}
                         (Some(_), None) | (None, Some(_)) => {
-                            language_servers_to_restart.push((worktree, Arc::clone(language)));
+                            language_servers_to_restart.push((worktree, language.name()));
                         }
                         (Some(current_lsp_settings), Some(new_lsp_settings)) => {
                             if current_lsp_settings != new_lsp_settings {
-                                language_servers_to_restart.push((worktree, Arc::clone(language)));
+                                language_servers_to_restart.push((worktree, language.name()));
                             }
                         }
                     }
@@ -4777,7 +4841,7 @@ impl Project {
     pub fn supplementary_language_servers<'a>(
         &'a self,
         cx: &'a AppContext,
-    ) -> impl '_ + Iterator<Item = (&'a LanguageServerId, &'a LanguageServerName)> {
+    ) -> impl '_ + Iterator<Item = (LanguageServerId, LanguageServerName)> {
         self.lsp_store.read(cx).supplementary_language_servers()
     }
 

crates/project/src/project_settings.rs 🔗

@@ -19,7 +19,7 @@ use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
 
 use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
 
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct ProjectSettings {
     /// Configuration for language servers.
     ///

crates/project/src/project_tests.rs 🔗

@@ -6,7 +6,7 @@ use http_client::Url;
 use language::{
     language_settings::{AllLanguageSettings, LanguageSettingsContent},
     tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter,
-    LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint,
+    LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
 };
 use lsp::{DiagnosticSeverity, NumberOrString};
 use parking_lot::Mutex;
@@ -1559,7 +1559,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
         SettingsStore::update_global(cx, |settings, cx| {
             settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
                 settings.languages.insert(
-                    Arc::from("Rust"),
+                    "Rust".into(),
                     LanguageSettingsContent {
                         enable_language_server: Some(false),
                         ..Default::default()
@@ -1578,14 +1578,14 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
         SettingsStore::update_global(cx, |settings, cx| {
             settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
                 settings.languages.insert(
-                    Arc::from("Rust"),
+                    LanguageName::new("Rust"),
                     LanguageSettingsContent {
                         enable_language_server: Some(true),
                         ..Default::default()
                     },
                 );
                 settings.languages.insert(
-                    Arc::from("JavaScript"),
+                    LanguageName::new("JavaScript"),
                     LanguageSettingsContent {
                         enable_language_server: Some(false),
                         ..Default::default()
@@ -2983,7 +2983,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
         buffer.edit([(0..0, "abc")], None, cx);
         assert!(buffer.is_dirty());
         assert!(!buffer.has_conflict());
-        assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
+        assert_eq!(buffer.language().unwrap().name(), "Plain Text".into());
     });
     project
         .update(cx, |project, cx| {
@@ -3006,7 +3006,7 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
         );
         assert!(!buffer.is_dirty());
         assert!(!buffer.has_conflict());
-        assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
+        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
     });
 
     let opened_buffer = project
@@ -5308,7 +5308,7 @@ fn json_lang() -> Arc<Language> {
 fn js_lang() -> Arc<Language> {
     Arc::new(Language::new(
         LanguageConfig {
-            name: Arc::from("JavaScript"),
+            name: "JavaScript".into(),
             matcher: LanguageMatcher {
                 path_suffixes: vec!["js".to_string()],
                 ..Default::default()

crates/project/src/task_inventory.rs 🔗

@@ -161,7 +161,7 @@ impl Inventory {
         cx: &AppContext,
     ) -> Vec<(TaskSourceKind, TaskTemplate)> {
         let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
-            name: language.name(),
+            name: language.name().0,
         });
         let language_tasks = language
             .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
@@ -207,7 +207,7 @@ impl Inventory {
             .as_ref()
             .and_then(|location| location.buffer.read(cx).language_at(location.range.start));
         let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
-            name: language.name(),
+            name: language.name().0,
         });
         let file = location
             .as_ref()

crates/proto/proto/zed.proto 🔗

@@ -281,7 +281,9 @@ message Envelope {
         FindSearchCandidatesResponse find_search_candidates_response = 244;
 
         CloseBuffer close_buffer = 245;
-        UpdateUserSettings update_user_settings = 246; // current max
+        UpdateUserSettings update_user_settings = 246;
+
+        CreateLanguageServer create_language_server = 247; // current max
     }
 
     reserved 158 to 161;
@@ -2497,3 +2499,36 @@ message UpdateUserSettings {
     uint64 project_id = 1;
     string content = 2;
 }
+
+message LanguageServerCommand {
+   string path = 1;
+   repeated string arguments = 2;
+}
+
+message AvailableLanguage {
+    string name = 7;
+    string matcher = 8;
+}
+
+message CreateLanguageServer {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string name = 3;
+
+    LanguageServerCommand binary = 4;
+    optional string initialization_options = 5;
+    optional string code_action_kinds = 6;
+
+    AvailableLanguage language = 7;
+}
+
+// message RestartLanguageServer {
+
+// }
+// message DestroyLanguageServer {
+
+// }
+
+// message LspWorkspaceConfiguration {
+
+// }

crates/proto/src/proto.rs 🔗

@@ -366,7 +366,8 @@ messages!(
     (FindSearchCandidates, Background),
     (FindSearchCandidatesResponse, Background),
     (CloseBuffer, Foreground),
-    (UpdateUserSettings, Foreground)
+    (UpdateUserSettings, Foreground),
+    (CreateLanguageServer, Foreground)
 );
 
 request_messages!(
@@ -490,6 +491,7 @@ request_messages!(
     (SynchronizeContexts, SynchronizeContextsResponse),
     (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse),
     (AddWorktree, AddWorktreeResponse),
+    (CreateLanguageServer, Ack)
 );
 
 entity_messages!(
@@ -562,7 +564,8 @@ entity_messages!(
     UpdateContext,
     SynchronizeContexts,
     LspExtSwitchSourceHeader,
-    UpdateUserSettings
+    UpdateUserSettings,
+    CreateLanguageServer
 );
 
 entity_messages!(

crates/quick_action_bar/src/repl_menu.rs 🔗

@@ -62,7 +62,7 @@ impl QuickActionBar {
                 return self.render_repl_launch_menu(spec, cx);
             }
             SessionSupport::RequiresSetup(language) => {
-                return self.render_repl_setup(&language, cx);
+                return self.render_repl_setup(&language.0, cx);
             }
             SessionSupport::Unsupported => return None,
         };

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -291,11 +291,24 @@ impl SshClientDelegate {
 
             self.update_status(Some("building remote server binary from source"), cx);
             log::info!("building remote server binary from source");
-            run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?;
-            run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?;
-            run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?;
+            run_cmd(Command::new("cargo").args([
+                "build",
+                "--package",
+                "remote_server",
+                "--target-dir",
+                "target/remote_server",
+            ]))
+            .await?;
+            // run_cmd(Command::new("strip").args(["target/remote_server/debug/remote_server"]))
+            // .await?;
+            run_cmd(Command::new("gzip").args([
+                "-9",
+                "-f",
+                "target/remote_server/debug/remote_server",
+            ]))
+            .await?;
 
-            let path = std::env::current_dir()?.join("target/debug/remote_server.gz");
+            let path = std::env::current_dir()?.join("target/remote_server/debug/remote_server.gz");
             return Ok((path, version));
 
             async fn run_cmd(command: &mut Command) -> Result<()> {

crates/remote/src/ssh_session.rs 🔗

@@ -41,11 +41,11 @@ pub struct SshSocket {
 
 pub struct SshSession {
     next_message_id: AtomicU32,
-    response_channels: ResponseChannels,
+    response_channels: ResponseChannels, // Lock
     outgoing_tx: mpsc::UnboundedSender<Envelope>,
     spawn_process_tx: mpsc::UnboundedSender<SpawnRequest>,
     client_socket: Option<SshSocket>,
-    state: Mutex<ProtoMessageHandlerSet>,
+    state: Mutex<ProtoMessageHandlerSet>, // Lock
 }
 
 struct SshClientState {
@@ -392,9 +392,9 @@ impl SshSession {
     ) -> impl 'static + Future<Output = Result<proto::Envelope>> {
         envelope.id = self.next_message_id.fetch_add(1, SeqCst);
         let (tx, rx) = oneshot::channel();
-        self.response_channels
-            .lock()
-            .insert(MessageId(envelope.id), tx);
+        let mut response_channels_lock = self.response_channels.lock();
+        response_channels_lock.insert(MessageId(envelope.id), tx);
+        drop(response_channels_lock);
         self.outgoing_tx.unbounded_send(envelope).ok();
         async move { Ok(rx.await.context("connection lost")?.0) }
     }

crates/remote_server/src/headless_project.rs 🔗

@@ -4,14 +4,13 @@ use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task};
 use language::LanguageRegistry;
 use project::{
     buffer_store::BufferStore, project_settings::SettingsObserver, search::SearchQuery,
-    worktree_store::WorktreeStore, LspStore, ProjectPath, WorktreeId, WorktreeSettings,
+    worktree_store::WorktreeStore, LspStore, ProjectPath, WorktreeId,
 };
 use remote::SshSession;
 use rpc::{
     proto::{self, AnyProtoClient, SSH_PEER_ID, SSH_PROJECT_ID},
     TypedEnvelope,
 };
-use settings::Settings as _;
 use smol::stream::StreamExt;
 use std::{
     path::{Path, PathBuf},
@@ -33,15 +32,17 @@ impl HeadlessProject {
     pub fn init(cx: &mut AppContext) {
         settings::init(cx);
         language::init(cx);
-        WorktreeSettings::register(cx);
+        project::Project::init_settings(cx);
     }
 
     pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
         // TODO: we should load the env correctly (as we do in login_shell_env_loaded when stdout is not a pty). Can we re-use the ProjectEnvironment for that?
-        let languages = Arc::new(LanguageRegistry::new(
-            Task::ready(()),
-            cx.background_executor().clone(),
-        ));
+        let mut languages =
+            LanguageRegistry::new(Task::ready(()), cx.background_executor().clone());
+        languages
+            .set_language_server_download_dir(PathBuf::from("/Users/conrad/what-could-go-wrong"));
+
+        let languages = Arc::new(languages);
 
         let worktree_store = cx.new_model(|_| WorktreeStore::new(true, fs.clone()));
         let buffer_store = cx.new_model(|cx| {
@@ -57,18 +58,17 @@ impl HeadlessProject {
         });
         let environment = project::ProjectEnvironment::new(&worktree_store, None, cx);
         let lsp_store = cx.new_model(|cx| {
-            LspStore::new(
+            let mut lsp_store = LspStore::new_local(
                 buffer_store.clone(),
                 worktree_store.clone(),
-                Some(environment),
+                environment,
                 languages,
                 None,
                 fs.clone(),
-                Some(session.clone().into()),
-                None,
-                Some(0),
                 cx,
-            )
+            );
+            lsp_store.shared(SSH_PROJECT_ID, session.clone().into(), cx);
+            lsp_store
         });
 
         let client: AnyProtoClient = session.clone().into();
@@ -88,9 +88,12 @@ impl HeadlessProject {
         client.add_model_request_handler(BufferStore::handle_update_buffer);
         client.add_model_message_handler(BufferStore::handle_close_buffer);
 
+        client.add_model_request_handler(LspStore::handle_create_language_server);
+
         BufferStore::init(&client);
         WorktreeStore::init(&client);
         SettingsObserver::init(&client);
+        LspStore::init(&client);
 
         HeadlessProject {
             session: client,

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -6,7 +6,7 @@ use gpui::{Context, Model, TestAppContext};
 use http_client::FakeHttpClient;
 use language::{
     language_settings::{all_language_settings, AllLanguageSettings},
-    Buffer, LanguageRegistry,
+    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry,
 };
 use node_runtime::FakeNodeRuntime;
 use project::{
@@ -202,15 +202,29 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
     server_cx.read(|cx| {
         assert_eq!(
             AllLanguageSettings::get_global(cx)
-                .language(Some("Rust"))
+                .language(Some(&"Rust".into()))
                 .language_servers,
             ["custom-rust-analyzer".into()]
         )
     });
 
-    fs.insert_tree("/code/project1/.zed", json!({
-        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
-    })).await;
+    fs.insert_tree(
+        "/code/project1/.zed",
+        json!({
+            "settings.json": r#"
+                  {
+                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
+                    "lsp": {
+                      "override-rust-analyzer": {
+                        "binary": {
+                          "path": "~/.cargo/bin/rust-analyzer"
+                        }
+                      }
+                    }
+                  }"#
+        }),
+    )
+    .await;
 
     let worktree_id = project
         .update(cx, |project, cx| {
@@ -247,7 +261,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
                 }),
                 cx
             )
-            .language(Some("Rust"))
+            .language(Some(&"Rust".into()))
             .language_servers,
             ["override-rust-analyzer".into()]
         )
@@ -257,13 +271,107 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
         let file = buffer.read(cx).file();
         assert_eq!(
             all_language_settings(file, cx)
-                .language(Some("Rust"))
+                .language(Some(&"Rust".into()))
                 .language_servers,
             ["override-rust-analyzer".into()]
         )
     });
 }
 
+#[gpui::test]
+async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
+    let (project, headless, fs) = init_test(cx, server_cx).await;
+
+    fs.insert_tree(
+        "/code/project1/.zed",
+        json!({
+            "settings.json": r#"
+          {
+            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
+            "lsp": {
+              "rust-analyzer": {
+                "binary": {
+                  "path": "~/.cargo/bin/rust-analyzer"
+                }
+              }
+            }
+          }"#
+        }),
+    )
+    .await;
+
+    cx.update_model(&project, |project, _| {
+        project.languages().register_test_language(LanguageConfig {
+            name: "Rust".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["rs".into()],
+                ..Default::default()
+            },
+            ..Default::default()
+        });
+        project.languages().register_fake_lsp_adapter(
+            "Rust",
+            FakeLspAdapter {
+                name: "rust-analyzer",
+                ..Default::default()
+            },
+        )
+    });
+    cx.run_until_parked();
+
+    let worktree_id = project
+        .update(cx, |project, cx| {
+            project.find_or_create_worktree("/code/project1", true, cx)
+        })
+        .await
+        .unwrap()
+        .0
+        .read_with(cx, |worktree, _| worktree.id());
+
+    // Wait for the settings to synchronize
+    cx.run_until_parked();
+
+    let buffer = project
+        .update(cx, |project, cx| {
+            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
+        })
+        .await
+        .unwrap();
+    cx.run_until_parked();
+
+    cx.read(|cx| {
+        let file = buffer.read(cx).file();
+        assert_eq!(
+            all_language_settings(file, cx)
+                .language(Some(&"Rust".into()))
+                .language_servers,
+            ["rust-analyzer".into()]
+        )
+    });
+
+    let buffer_id = cx.read(|cx| {
+        let buffer = buffer.read(cx);
+        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
+        buffer.remote_id()
+    });
+
+    server_cx.read(|cx| {
+        let buffer = headless
+            .read(cx)
+            .buffer_store
+            .read(cx)
+            .get(buffer_id)
+            .unwrap();
+
+        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
+    });
+
+    server_cx.read(|cx| {
+        let lsp_store = headless.read(cx).lsp_store.read(cx);
+        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
+    });
+}
+
 fn init_logger() {
     if std::env::var("RUST_LOG").is_ok() {
         env_logger::try_init().ok();

crates/repl/src/repl_editor.rs 🔗

@@ -6,7 +6,7 @@ use std::sync::Arc;
 use anyhow::{Context, Result};
 use editor::Editor;
 use gpui::{prelude::*, AppContext, Entity, View, WeakView, WindowContext};
-use language::{BufferSnapshot, Language, Point};
+use language::{BufferSnapshot, Language, LanguageName, Point};
 
 use crate::repl_store::ReplStore;
 use crate::session::SessionEvent;
@@ -99,7 +99,7 @@ pub fn run(editor: WeakView<Editor>, move_down: bool, cx: &mut WindowContext) ->
 pub enum SessionSupport {
     ActiveSession(View<Session>),
     Inactive(Box<KernelSpecification>),
-    RequiresSetup(Arc<str>),
+    RequiresSetup(LanguageName),
     Unsupported,
 }
 
@@ -268,7 +268,7 @@ fn runnable_ranges(
     range: Range<Point>,
 ) -> (Vec<Range<Point>>, Option<Point>) {
     if let Some(language) = buffer.language() {
-        if language.name().as_ref() == "Markdown" {
+        if language.name() == "Markdown".into() {
             return (markdown_code_blocks(buffer, range.clone()), None);
         }
     }
@@ -305,7 +305,7 @@ fn markdown_code_blocks(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Ran
 }
 
 fn language_supported(language: &Arc<Language>) -> bool {
-    match language.name().as_ref() {
+    match language.name().0.as_ref() {
         "TypeScript" | "Python" => true,
         _ => false,
     }

crates/worktree/src/worktree.rs 🔗

@@ -564,6 +564,13 @@ impl Worktree {
         !self.is_local()
     }
 
+    pub fn settings_location(&self, _: &ModelContext<Self>) -> SettingsLocation<'static> {
+        SettingsLocation {
+            worktree_id: self.id(),
+            path: Path::new(EMPTY_PATH),
+        }
+    }
+
     pub fn snapshot(&self) -> Snapshot {
         match self {
             Worktree::Local(worktree) => worktree.snapshot.snapshot.clone(),

crates/zed/src/zed.rs 🔗

@@ -2251,14 +2251,8 @@ mod tests {
                     assert!(!editor.is_dirty(cx));
                     assert_eq!(editor.title(cx), "the-new-name.rs");
                     assert_eq!(
-                        editor
-                            .buffer()
-                            .read(cx)
-                            .language_at(0, cx)
-                            .unwrap()
-                            .name()
-                            .as_ref(),
-                        "Rust"
+                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
+                        "Rust".into()
                     );
                 });
             })
@@ -2374,14 +2368,8 @@ mod tests {
                 editor.update(cx, |editor, cx| {
                     assert!(!editor.is_dirty(cx));
                     assert_eq!(
-                        editor
-                            .buffer()
-                            .read(cx)
-                            .language_at(0, cx)
-                            .unwrap()
-                            .name()
-                            .as_ref(),
-                        "Rust"
+                        editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
+                        "Rust".into()
                     )
                 });
             })