From 36eb1c15eaf7bff72bf120e1fccc7a73326ad8e9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 10 Sep 2024 15:51:01 -0400 Subject: [PATCH] use ssh lsp store (#17655) Release Notes: - ssh remoting: Added support for booting langauge servers (in limited circumstances) --------- Co-authored-by: Mikayla --- 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 +- .../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 +- .../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 +- .../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 +- .../src/active_buffer_language.rs | 4 +- .../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 + .../src/markdown_preview_view.rs | 2 +- .../project/src/lsp_command/signature_help.rs | 2 +- crates/project/src/lsp_store.rs | 1368 ++++++++++++----- 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 +- .../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, 1553 insertions(+), 671 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 22843d41cd..7eebc97b1d 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/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) + Ok( + ProjectLspAdapterDelegate::new(lsp_store, &worktree, http_client, fs, cx) + as Arc, + ) }) }) } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 7bd74ccabf..051db0f247 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/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) { diff --git a/crates/assistant/src/prompts.rs b/crates/assistant/src/prompts.rs index 068bf7158d..83e894f797 100644 --- a/crates/assistant/src/prompts.rs +++ b/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, ) -> Result { - 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", }; diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e012fce8c2..b6d7aca2e0 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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 diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 21e7f9dd9e..c4410fd776 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/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()] ) diff --git a/crates/editor/src/clangd_ext.rs b/crates/editor/src/clangd_ext.rs index 7fbb8f5f41..2f0f7aaee4 100644 --- a/crates/editor/src/clangd_ext.rs +++ b/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( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cb4ae63afc..3466888c94 100644 --- a/crates/editor/src/editor.rs +++ b/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 } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c8c509fd98..0b1e0385de 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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 = "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() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 23293469dd..1be2092d7d 100644 --- a/crates/editor/src/items.rs +++ b/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 }); diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index a152f3c453..db17eaab28 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/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, cx: &mut WindowContext) { diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index ec1eccb864..16735760bf 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/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() diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs index 41a35cb617..f82b6c9e0e 100644 --- a/crates/extension/src/extension_lsp_adapter.rs +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -38,7 +38,6 @@ impl LspAdapter for ExtensionLspAdapter { fn get_language_server_command<'a>( self: Arc, - _: Arc, _: Arc, delegate: Arc, _: futures::lock::MutexGuard<'a, Option>, diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs index 9d8a841686..3dfd7e0d41 100644 --- a/crates/extension/src/extension_manifest.rs +++ b/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>, + language: Option, /// The list of languages this language server should work with. #[serde(default)] - languages: Vec>, + languages: Vec, #[serde(default)] pub language_ids: HashMap, #[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> + '_ { + pub fn languages(&self) -> impl IntoIterator + '_ { let language = if self.languages.is_empty() { self.language.clone() } else { diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 2558dca93e..3ebc4f20d3 100644 --- a/crates/extension/src/extension_store.rs +++ b/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, ExtensionIndexEntry>, pub themes: BTreeMap, ExtensionIndexThemeEntry>, - pub languages: BTreeMap, ExtensionIndexLanguageEntry>, + pub languages: BTreeMap, } #[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] diff --git a/crates/extension/src/extension_store_test.rs b/crates/extension/src/extension_store_test.rs index 70ea7ac909..da530306d1 100644 --- a/crates/extension/src/extension_store_test.rs +++ b/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| { diff --git a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension/src/wasm_host/wit/since_v0_1_0.rs index 68550a44cf..337bb8afb0 100644 --- a/crates/extension/src/wasm_host/wit/since_v0_1_0.rs +++ b/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, })?) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 564b893489..ac7d5eb47b 100644 --- a/crates/gpui/src/app.rs +++ b/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>, } + +impl Drop for AppContext { + fn drop(&mut self) { + println!("Dropping the App Context"); + } +} diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 8584eee4c7..77a1079d3a 100644 --- a/crates/language/src/buffer_tests.rs +++ b/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 { @@ -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())), ..Default::default() }, diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7e8fcc655d..6424da8a54 100644 --- a/crates/language/src/language.rs +++ b/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); +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, @@ -195,9 +202,12 @@ impl CachedLspAdapter { }) } + pub fn name(&self) -> Arc { + self.adapter.name().0.clone() + } + pub async fn get_language_server_command( self: Arc, - language: Arc, container_dir: Arc, delegate: Arc, 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, - cx: &mut AsyncAppContext, - ) -> Option>> { - 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, - language: Arc, container_dir: Arc, delegate: Arc, mut cached_binary: futures::lock::MutexGuard<'a, Option>, @@ -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, - _: &mut AsyncAppContext, - ) -> Option>> { - None - } - async fn fetch_server_binary( &self, latest_version: Box, @@ -562,7 +555,7 @@ pub struct CodeLabel { #[derive(Clone, Deserialize, JsonSchema)] pub struct LanguageConfig { /// Human-readable name of the language. - pub name: Arc, + pub name: LanguageName, /// The name of this language for a Markdown code fence block pub code_fence_block_name: Option>, // The name of the grammar in a WASM bundle (experimental). @@ -699,7 +692,7 @@ impl Override { 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 { + 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> { @@ -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 { + 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, - _: Arc, _: Arc, _: Arc, _: futures::lock::MutexGuard<'a, Option>, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index a558b942d6..a65d20019f 100644 --- a/crates/language/src/language_registry.rs +++ b/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); + +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 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, language_server_download_dir: Option>, @@ -46,7 +90,7 @@ struct LanguageRegistryState { language_settings: AllLanguageSettingsContent, available_languages: Vec, grammars: HashMap, AvailableGrammar>, - lsp_adapters: HashMap, Vec>>, + lsp_adapters: HashMap>>, available_lsp_adapters: HashMap Arc + 'static + Send + Sync>>, loading_languages: HashMap>>>>, @@ -56,8 +100,10 @@ struct LanguageRegistryState { reload_count: usize, #[cfg(any(test, feature = "test-support"))] - fake_server_txs: - HashMap, Vec>>, + fake_server_txs: HashMap< + LanguageName, + Vec>, + >, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -75,9 +121,9 @@ pub struct PendingLanguageServer { } #[derive(Clone)] -struct AvailableLanguage { +pub struct AvailableLanguage { id: LanguageId, - name: Arc, + name: LanguageName, grammar: Option>, 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: &LanguageName, ordered_lsp_adapters: Vec>, ) { 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], + languages_to_remove: &[LanguageName], grammars_to_remove: &[Arc], ) { 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, adapter: Arc) { + pub fn register_lsp_adapter(&self, language_name: LanguageName, adapter: Arc) { 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, adapter: crate::FakeLspAdapter, ) -> futures::channel::mpsc::UnboundedReceiver { + 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 { 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, + name: LanguageName, grammar_name: Option>, matcher: LanguageMatcher, load: impl Fn() -> Result<( @@ -445,7 +502,7 @@ impl LanguageRegistry { ) -> impl Future>> { 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>> { 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, + name: &LanguageName, + ) -> Option { + let state = self.state.read(); + state + .available_languages + .iter() + .find(|l| &l.name == name) + .cloned() + } + pub fn language_for_file( self: &Arc, file: &Arc, content: Option<&Rope>, cx: &AppContext, - ) -> impl Future>> { + ) -> Option { 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, path: &'a Path, ) -> impl Future>> + '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, GlobSet>>, - ) -> impl Future>> { + ) -> Option { 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, - callback: impl Fn(&str, &LanguageMatcher) -> usize, - ) -> oneshot::Receiver>> { - let (tx, rx) = oneshot::channel(); - - let mut state = self.state.write(); - let Some((language, _)) = state + callback: impl Fn(&LanguageName, &LanguageMatcher) -> usize, + ) -> Option { + 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, + language: &AvailableLanguage, + ) -> oneshot::Receiver>> { + 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, + callback: impl Fn(&LanguageName, &LanguageMatcher) -> usize, + ) -> oneshot::Receiver>> { + 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, name: Arc, @@ -702,11 +803,11 @@ impl LanguageRegistry { self.state.read().languages.to_vec() } - pub fn lsp_adapters(&self, language: &Arc) -> Vec> { + pub fn lsp_adapters(&self, language_name: &LanguageName) -> Vec> { 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, stderr_capture: Arc>>, - language: Arc, + _language_name_for_tests: LanguageName, adapter: Arc, root_path: Arc, delegate: Arc, @@ -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 = 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_name: &LanguageName, ordered_lsp_adapters: Vec>, ) { - 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], + languages_to_remove: &[LanguageName], grammars_to_remove: &[Arc], ) { if languages_to_remove.is_empty() && grammars_to_remove.is_empty() { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index ac3c9eb6ca..e1fcaaba28 100644 --- a/crates/language/src/language_settings.rs +++ b/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, LanguageSettings>, + languages: HashMap, pub(crate) file_types: HashMap, GlobSet>, } @@ -204,7 +204,7 @@ pub struct AllLanguageSettingsContent { pub defaults: LanguageSettingsContent, /// The settings for individual languages. #[serde(default)] - pub languages: HashMap, LanguageSettingsContent>, + pub languages: HashMap, /// 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 } } diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 647ff93b81..6aa31d7ff8 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/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>>, + active_language: Option>, workspace: WeakView, _observe_active_editor: Option, } diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 6bdf5a67d0..489f6fd141 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/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)"); } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 5cf800d306..53def5eb2a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/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, diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 1d98c3d0b0..e2c4903e19 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/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()))) } } diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 6ed20abe17..46b6ce475d 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -451,7 +451,7 @@ impl ContextProvider for RustContextProvider { ) -> Option { 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); diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 4f0270fb26..51a9913b24 100644 --- a/crates/languages/src/yaml.rs +++ b/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}}); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 30feffad97..0612917575 100644 --- a/crates/lsp/src/lsp.rs +++ b/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 { diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index d73e205483..1aa60e2a3b 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/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 diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs index 163c6ae134..bf197a11ba 100644 --- a/crates/project/src/lsp_command/signature_help.rs +++ b/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 { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 1d9ca98c06..b218ac5804 100644 --- a/crates/project/src/lsp_store.rs +++ b/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 LspStore { - downstream_client: Option, - upstream_client: Option, - project_id: u64, +pub struct LocalLspStore { http_client: Option>, + environment: Model, fs: Arc, - nonce: u128, - buffer_store: Model, - worktree_store: Model, - buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots - environment: Option>, - supplementary_language_servers: - HashMap)>, - languages: Arc, - language_servers: HashMap, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, - language_server_statuses: BTreeMap, + yarn: Model, + pub language_servers: HashMap, last_workspace_edits_by_language_server: HashMap, language_server_watched_paths: HashMap>, language_server_watcher_registrations: HashMap>>, + supplementary_language_servers: + HashMap)>, + _subscription: gpui::Subscription, +} + +impl LocalLspStore { + fn shutdown_language_servers( + &mut self, + _cx: &mut ModelContext, + ) -> impl Future { + 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::>(); + + 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, + project_id: u64, + nonce: u128, + buffer_store: Model, + worktree_store: Model, + buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots + pub languages: Arc, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + pub language_server_statuses: BTreeMap, active_entry: Option, _maintain_workspace_config: Task>, _maintain_buffer_languages: Task<()>, @@ -122,8 +186,6 @@ pub struct LspStore { )>, >, >, - yarn: Model, - _subscription: gpui::Subscription, } pub enum LspStoreEvent { @@ -209,17 +271,53 @@ impl LspStore { client.add_model_request_handler(Self::handle_lsp_command::); } - #[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 { + 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, worktree_store: Model, - environment: Option>, + environment: Model, languages: Arc, http_client: Option>, fs: Arc, - downstream_client: Option, - upstream_client: Option, - remote_id: Option, cx: &mut ModelContext, ) -> 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(), - environment, - nonce: StdRng::from_entropy().gen(), - buffer_snapshots: Default::default(), - supplementary_language_servers: Default::default(), - language_servers: Default::default(), 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, + _maintain_workspace_config: Self::maintain_workspace_config(cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), + } + } + + fn send_lsp_proto_request( + &self, + buffer: Model, + client: AnyProtoClient, + request: R, + cx: &mut ModelContext<'_, LspStore>, + ) -> Task::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, + worktree_store: Model, + languages: Arc, + upstream_client: AnyProtoClient, + project_id: u64, + cx: &mut ModelContext, + ) -> 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(), + 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), + } + } + + pub fn new_remote( + buffer_store: Model, + worktree_store: Model, + languages: Arc, + upstream_client: AnyProtoClient, + project_id: u64, + cx: &mut ModelContext, + ) -> 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(), + 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, cx: &mut ModelContext, - ) { + ) -> Option { // 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::() { - 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, - ) -> impl Future { - 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::>(); - - 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 { ::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( - &self, - buffer: Model, - project_id: u64, - request: R, - cx: &mut ModelContext<'_, Self>, - ) -> Task::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, adapters_and_servers: &[(Arc, Arc)], @@ -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, ) -> Task> { - 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, ) -> Task> { - 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, ) -> Task>> { - 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, cx: &mut ModelContext, ) -> Task> { - 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>> { 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>>, cx: &mut ModelContext, ) -> Task> { - 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> { 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, ) -> Task> { - 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) -> Task>> { 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, - language: Arc, + language: LanguageName, worktree: WeakModel, worktree_abs_path: Arc, 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, &Arc, &Arc)> { + ) -> impl Iterator> { 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, cx: &mut ModelContext, ) { + 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, ) -> Task>> { - 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 { ::Result: Send, ::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 { ::Params: Send, ::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, mut cx: AsyncAppContext, ) -> Result { - 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, mut cx: AsyncAppContext, ) -> Result { - 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, ) { - 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::>(); + + 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::>(); - enum PathToWatch { Worktree { literal_prefix: Arc, @@ -3438,18 +3594,27 @@ 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> { - 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)) + 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 } @@ -3480,7 +3645,9 @@ impl LspStore { .log_err(); this.update(&mut cx, |this, _| { if let Some(transaction) = transaction { - this.last_workspace_edits_by_language_server + this.as_local_mut() + .unwrap() + .last_workspace_edits_by_language_server .insert(server_id, transaction); } })?; @@ -3665,14 +3832,16 @@ impl LspStore { params: DidChangeWatchedFilesRegistrationOptions, cx: &mut ModelContext, ) { - let registrations = self - .language_server_watcher_registrations - .entry(language_server_id) - .or_default(); + if let Some(local) = self.as_local_mut() { + let registrations = local + .language_server_watcher_registrations + .entry(language_server_id) + .or_default(); - registrations.insert(registration_id.to_string(), params.watchers); + registrations.insert(registration_id.to_string(), params.watchers); - self.rebuild_watched_paths(language_server_id, cx); + self.rebuild_watched_paths(language_server_id, cx); + } } fn on_lsp_unregister_did_change_watched_files( @@ -3681,26 +3850,28 @@ impl LspStore { registration_id: &str, cx: &mut ModelContext, ) { - let registrations = self - .language_server_watcher_registrations - .entry(language_server_id) - .or_default(); + if let Some(local) = self.as_local_mut() { + let registrations = local + .language_server_watcher_registrations + .entry(language_server_id) + .or_default(); - if registrations.remove(registration_id).is_some() { - log::info!( + if registrations.remove(registration_id).is_some() { + log::info!( "language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}", language_server_id, registration_id ); - } else { - log::warn!( + } else { + log::warn!( "language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.", language_server_id, registration_id ); - } + } - self.rebuild_watched_paths(language_server_id, cx); + self.rebuild_watched_paths(language_server_id, cx); + } } #[allow(clippy::type_complexity)] @@ -3915,7 +4086,7 @@ impl LspStore { envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> Result { - 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 buffer = this.update(&mut cx, |this, cx| { this.buffer_store.read(cx).get_existing(buffer_id) @@ -3991,7 +4162,7 @@ impl LspStore { envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> Result { - let peer_id = envelope.original_sender_id()?; + let peer_id = envelope.original_sender_id().unwrap_or_default(); let symbol = envelope .payload .symbol @@ -4093,6 +4264,76 @@ impl LspStore { Ok(proto::Ack {}) } + pub async fn handle_create_language_server( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + let name = LanguageServerName::from_proto(envelope.payload.name); + + let binary = envelope + .payload + .binary + .ok_or_else(|| anyhow!("missing binary"))?; + let binary = LanguageServerBinary { + path: PathBuf::from(binary.path), + env: None, + arguments: binary.arguments.into_iter().map(Into::into).collect(), + }; + let language = envelope + .payload + .language + .ok_or_else(|| anyhow!("missing language"))?; + let language_name = LanguageName::from_proto(language.name); + let matcher: LanguageMatcher = serde_json::from_str(&language.matcher)?; + this.update(&mut cx, |this, cx| { + this.languages + .register_language(language_name.clone(), None, matcher.clone(), { + let language_name = language_name.clone(); + move || { + Ok(( + LanguageConfig { + name: language_name.clone(), + matcher: matcher.clone(), + ..Default::default() + }, + Default::default(), + Default::default(), + )) + } + }); + cx.background_executor() + .spawn(this.languages.language_for_name(language_name.0.as_ref())) + .detach(); + + let adapter = Arc::new(SshLspAdapter::new( + name, + binary, + envelope.payload.initialization_options, + envelope.payload.code_action_kinds, + )); + + this.languages + .register_lsp_adapter(language_name.clone(), adapter.clone()); + let Some(worktree) = this + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return Err(anyhow!("worktree not found")); + }; + this.start_language_server( + &worktree, + CachedLspAdapter::new(adapter), + language_name, + cx, + ); + Ok(()) + })??; + Ok(proto::Ack {}) + } + async fn handle_apply_additional_edits_for_completion( this: Model, envelope: TypedEnvelope, @@ -4139,16 +4380,24 @@ impl LspStore { }) } + fn language_settings<'a>( + &'a self, + worktree: &'a Model, + language: &LanguageName, + cx: &'a mut ModelContext, + ) -> &'a LanguageSettings { + let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx)); + all_language_settings(root_file.map(|f| f as _).as_ref(), cx).language(Some(language)) + } + pub fn start_language_servers( &mut self, worktree: &Model, - language: Arc, + language: LanguageName, cx: &mut ModelContext, ) { - let (root_file, is_local) = - worktree.update(cx, |tree, cx| (tree.root_file(cx), tree.is_local())); - let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx); - if !settings.enable_language_server || !is_local { + let settings = self.language_settings(worktree, &language, cx); + if !settings.enable_language_server || self.mode.is_remote() { return; } @@ -4176,7 +4425,7 @@ impl LspStore { .load_available_lsp_adapter(&desired_language_server) { self.languages - .register_lsp_adapter(language.name(), adapter.adapter.clone()); + .register_lsp_adapter(language.clone(), adapter.adapter.clone()); enabled_lsp_adapters.push(adapter); continue; } @@ -4189,7 +4438,6 @@ impl LspStore { log::info!( "starting language servers for {language}: {adapters}", - language = language.name(), adapters = enabled_lsp_adapters .iter() .map(|adapter| adapter.name.0.as_ref()) @@ -4210,14 +4458,108 @@ impl LspStore { .reorder_language_servers(&language, enabled_lsp_adapters); } + /* + ssh client owns the lifecycle of the language servers + ssh host actually runs the binaries + + in the future: ssh client will use the local extensions to get the downloads etc. + and send them up over the ssh connection (but today) we'll just the static config + + languages::() <-- registers lsp adapters + on the ssh host we won't have adapters for the LSPs + */ + + fn start_language_server_on_ssh_host( + &mut self, + worktree: &Model, + adapter: Arc, + language: LanguageName, + cx: &mut ModelContext, + ) { + let ssh = self.as_ssh().unwrap(); + + let configured_binary = ProjectSettings::get( + Some(worktree.update(cx, |worktree, cx| worktree.settings_location(cx))), + cx, + ) + .lsp + .get(&adapter.name()) + .and_then(|c| c.binary.as_ref()) + .and_then(|config| { + if let Some(path) = &config.path { + Some((path.clone(), config.arguments.clone().unwrap_or_default())) + } else { + None + } + }); + let delegate = + ProjectLspAdapterDelegate::for_ssh(self, worktree, cx) as Arc; + let project_id = self.project_id; + let worktree_id = worktree.read(cx).id().to_proto(); + let upstream_client = ssh.upstream_client.clone(); + let name = adapter.name().to_string(); + let Some((path, arguments)) = configured_binary else { + cx.emit(LspStoreEvent::Notification(format!( + "ssh-remoting currently requires manually configuring {} in your settings", + adapter.name() + ))); + return; + }; + let Some(available_language) = self.languages.available_language_for_name(&language) else { + log::error!("failed to find available language {language}"); + return; + }; + let task = cx.spawn(|_, _| async move { + let delegate = delegate; + let name = adapter.name().to_string(); + let code_action_kinds = adapter + .adapter + .code_action_kinds() + .map(|kinds| serde_json::to_string(&kinds)) + .transpose()?; + let get_options = adapter.adapter.clone().initialization_options(&delegate); + let initialization_options = get_options + .await? + .map(|options| serde_json::to_string(&options)) + .transpose()?; + + upstream_client + .request(proto::CreateLanguageServer { + project_id, + worktree_id, + name, + binary: Some(proto::LanguageServerCommand { path, arguments }), + initialization_options, + code_action_kinds, + language: Some(proto::AvailableLanguage { + name: language.to_proto(), + matcher: serde_json::to_string(&available_language.matcher())?, + }), + }) + .await + }); + cx.spawn(|this, mut cx| async move { + if let Err(e) = task.await { + this.update(&mut cx, |_this, cx| { + cx.emit(LspStoreEvent::Notification(format!( + "failed to start {}: {}", + name, e + ))) + }) + .ok(); + } + }) + .detach(); + } + fn start_language_server( &mut self, worktree_handle: &Model, adapter: Arc, - language: Arc, + language: LanguageName, cx: &mut ModelContext, ) { - if adapter.reinstall_attempt_count.load(SeqCst) > MAX_SERVER_REINSTALL_ATTEMPT_COUNT { + if self.mode.is_remote() { return; } @@ -4229,12 +4571,24 @@ impl LspStore { return; } + if self.mode.is_ssh() { + self.start_language_server_on_ssh_host(worktree_handle, adapter, language, cx); + return; + } + + if adapter.reinstall_attempt_count.load(SeqCst) > MAX_SERVER_REINSTALL_ATTEMPT_COUNT { + return; + } + let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); - let lsp_adapter_delegate = ProjectLspAdapterDelegate::new(self, worktree_handle, cx); + let lsp_adapter_delegate = ProjectLspAdapterDelegate::for_local(self, worktree_handle, cx); let cli_environment = self + .as_local() + .unwrap() .environment - .as_ref() - .and_then(|environment| environment.read(cx).get_cli_environment()); + .read(cx) + .get_cli_environment(); + let pending_server = match self.languages.create_pending_language_server( stderr_capture.clone(), language.clone(), @@ -4255,6 +4609,8 @@ impl LspStore { }), cx, ); + + // We need some on the SSH client, and some on SSH host let lsp = project_settings.lsp.get(&adapter.name.0); let override_options = lsp.and_then(|s| s.initialization_options.clone()); @@ -4329,7 +4685,10 @@ impl LspStore { }) }); - self.language_servers.insert(server_id, state); + self.as_local_mut() + .unwrap() + .language_servers + .insert(server_id, state); self.language_server_ids.insert(key, server_id); } @@ -4340,7 +4699,7 @@ impl LspStore { override_initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, - language: Arc, + language: LanguageName, server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), cx: &mut AsyncAppContext, @@ -4377,51 +4736,63 @@ impl LspStore { fn reinstall_language_server( &mut self, - language: Arc, + language: LanguageName, adapter: Arc, server_id: LanguageServerId, cx: &mut ModelContext, ) -> Option> { log::info!("beginning to reinstall server"); - let existing_server = match self.language_servers.remove(&server_id) { - Some(LanguageServerState::Running { server, .. }) => Some(server), - _ => None, - }; - - self.worktree_store.update(cx, |store, cx| { - for worktree in store.worktrees() { - let key = (worktree.read(cx).id(), adapter.name.clone()); - self.language_server_ids.remove(&key); - } - }); - - Some(cx.spawn(move |this, mut cx| async move { - if let Some(task) = existing_server.and_then(|server| server.shutdown()) { - log::info!("shutting down existing server"); - task.await; - } - - // TODO: This is race-safe with regards to preventing new instances from - // starting while deleting, but existing instances in other projects are going - // to be very confused and messed up - let Some(task) = this - .update(&mut cx, |this, cx| { - this.languages.delete_server_container(adapter.clone(), cx) - }) - .log_err() - else { - return; + if let Some(local) = self.as_local_mut() { + let existing_server = match local.language_servers.remove(&server_id) { + Some(LanguageServerState::Running { server, .. }) => Some(server), + _ => None, }; - task.await; - this.update(&mut cx, |this, cx| { - for worktree in this.worktree_store.read(cx).worktrees().collect::>() { - this.start_language_server(&worktree, adapter.clone(), language.clone(), cx); + self.worktree_store.update(cx, |store, cx| { + for worktree in store.worktrees() { + let key = (worktree.read(cx).id(), adapter.name.clone()); + self.language_server_ids.remove(&key); } - }) - .ok(); - })) + }); + + Some(cx.spawn(move |this, mut cx| async move { + if let Some(task) = existing_server.and_then(|server| server.shutdown()) { + log::info!("shutting down existing server"); + task.await; + } + + // TODO: This is race-safe with regards to preventing new instances from + // starting while deleting, but existing instances in other projects are going + // to be very confused and messed up + let Some(task) = this + .update(&mut cx, |this, cx| { + this.languages.delete_server_container(adapter.clone(), cx) + }) + .log_err() + else { + return; + }; + task.await; + + this.update(&mut cx, |this, cx| { + for worktree in this.worktree_store.read(cx).worktrees().collect::>() { + this.start_language_server( + &worktree, + adapter.clone(), + language.clone(), + cx, + ); + } + }) + .ok(); + })) + } else if let Some(_ssh_store) = self.as_ssh() { + // TODO + None + } else { + None + } } async fn shutdown_language_server( @@ -4469,76 +4840,90 @@ impl LspStore { cx: &mut ModelContext, ) -> Task> { let key = (worktree_id, adapter_name); - if let Some(server_id) = self.language_server_ids.remove(&key) { - let name = key.1 .0; - log::info!("stopping language server {name}"); + if self.mode.is_local() { + if let Some(server_id) = self.language_server_ids.remove(&key) { + let name = key.1 .0; + log::info!("stopping language server {name}"); - // Remove other entries for this language server as well - let mut orphaned_worktrees = vec![worktree_id]; - let other_keys = self.language_server_ids.keys().cloned().collect::>(); - for other_key in other_keys { - if self.language_server_ids.get(&other_key) == Some(&server_id) { - self.language_server_ids.remove(&other_key); - orphaned_worktrees.push(other_key.0); + // Remove other entries for this language server as well + let mut orphaned_worktrees = vec![worktree_id]; + let other_keys = self.language_server_ids.keys().cloned().collect::>(); + for other_key in other_keys { + if self.language_server_ids.get(&other_key) == Some(&server_id) { + self.language_server_ids.remove(&other_key); + orphaned_worktrees.push(other_key.0); + } } - } - self.buffer_store.update(cx, |buffer_store, cx| { - for buffer in buffer_store.buffers() { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(server_id, Default::default(), cx); + self.buffer_store.update(cx, |buffer_store, cx| { + for buffer in buffer_store.buffers() { + buffer.update(cx, |buffer, cx| { + buffer.update_diagnostics(server_id, Default::default(), cx); + }); + } + }); + + let project_id = self.project_id; + for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { + summaries.retain(|path, summaries_by_server_id| { + if summaries_by_server_id.remove(&server_id).is_some() { + if let Some(downstream_client) = self.downstream_client.clone() { + downstream_client + .send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id: worktree_id.to_proto(), + summary: Some(proto::DiagnosticSummary { + path: path.to_string_lossy().to_string(), + language_server_id: server_id.0 as u64, + error_count: 0, + warning_count: 0, + }), + }) + .log_err(); + } + !summaries_by_server_id.is_empty() + } else { + true + } }); } - }); - let project_id = self.project_id; - for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { - summaries.retain(|path, summaries_by_server_id| { - if summaries_by_server_id.remove(&server_id).is_some() { - if let Some(downstream_client) = self.downstream_client.clone() { - downstream_client - .send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), - language_server_id: server_id.0 as u64, - error_count: 0, - warning_count: 0, - }), - }) - .log_err(); + for diagnostics in self.diagnostics.values_mut() { + diagnostics.retain(|_, diagnostics_by_server_id| { + if let Ok(ix) = + diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) + { + diagnostics_by_server_id.remove(ix); + !diagnostics_by_server_id.is_empty() + } else { + true } - !summaries_by_server_id.is_empty() - } else { - true - } - }); + }); + } + + self.as_local_mut() + .unwrap() + .language_server_watched_paths + .remove(&server_id); + self.language_server_statuses.remove(&server_id); + cx.notify(); + + let server_state = self + .as_local_mut() + .unwrap() + .language_servers + .remove(&server_id); + cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); + cx.spawn(move |_, cx| async move { + Self::shutdown_language_server(server_state, name, cx).await; + orphaned_worktrees + }) + } else { + Task::ready(Vec::new()) } - - for diagnostics in self.diagnostics.values_mut() { - diagnostics.retain(|_, diagnostics_by_server_id| { - if let Ok(ix) = - diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) - { - diagnostics_by_server_id.remove(ix); - !diagnostics_by_server_id.is_empty() - } else { - true - } - }); - } - - self.language_server_watched_paths.remove(&server_id); - self.language_server_statuses.remove(&server_id); - cx.notify(); - - let server_state = self.language_servers.remove(&server_id); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - cx.spawn(move |_, cx| async move { - Self::shutdown_language_server(server_state, name, cx).await; - orphaned_worktrees - }) + } else if self.mode.is_ssh() { + // TODO ssh + Task::ready(Vec::new()) } else { Task::ready(Vec::new()) } @@ -4549,7 +4934,7 @@ impl LspStore { buffers: impl IntoIterator>, cx: &mut ModelContext, ) { - if let Some(client) = self.upstream_client.clone() { + if let Some(client) = self.upstream_client() { let request = client.request(proto::RestartLanguageServers { project_id: self.project_id, buffer_ids: buffers @@ -4562,18 +4947,17 @@ impl LspStore { .detach_and_log_err(cx); } else { #[allow(clippy::mutable_key_type)] - let language_server_lookup_info: HashSet<(Model, Arc)> = buffers + let language_server_lookup_info: HashSet<(Model, LanguageName)> = buffers .into_iter() .filter_map(|buffer| { let buffer = buffer.read(cx); let file = buffer.file()?; let worktree = File::from_dyn(Some(file))?.worktree.clone(); - let language = self - .languages - .language_for_file(file, Some(buffer.as_rope()), cx) - .now_or_never()? - .ok()?; - Some((worktree, language)) + let language = + self.languages + .language_for_file(file, Some(buffer.as_rope()), cx)?; + + Some((worktree, language.name())) }) .collect(); @@ -4586,7 +4970,7 @@ impl LspStore { pub fn restart_language_servers( &mut self, worktree: Model, - language: Arc, + language: LanguageName, cx: &mut ModelContext, ) { let worktree_id = worktree.read(cx).id(); @@ -4637,7 +5021,7 @@ impl LspStore { } fn check_errored_server( - language: Arc, + language: LanguageName, adapter: Arc, server_id: LanguageServerId, installation_test_binary: Option, @@ -4719,6 +5103,7 @@ impl LspStore { .clone() .workspace_configuration(&delegate, cx) .await?; + // This has to come from the server let (language_server, mut initialization_options) = pending_server.task.await?; let name = language_server.name(); @@ -4730,6 +5115,7 @@ impl LspStore { let adapter = adapter.clone(); if let Some(this) = this.upgrade() { adapter.process_diagnostics(&mut params); + // Everything else has to be on the server, Can we make it on the client? this.update(&mut cx, |this, cx| { this.update_diagnostics( server_id, @@ -5341,7 +5727,7 @@ impl LspStore { fn insert_newly_running_language_server( &mut self, - language: Arc, + language: LanguageName, adapter: Arc, language_server: Arc, server_id: LanguageServerId, @@ -5361,15 +5747,17 @@ impl LspStore { // Update language_servers collection with Running variant of LanguageServerState // indicating that the server is up and running and ready - self.language_servers.insert( - server_id, - LanguageServerState::Running { - adapter: adapter.clone(), - language: language.clone(), - server: language_server.clone(), - simulate_disk_based_diagnostics_completion: None, - }, - ); + if let Some(local) = self.as_local_mut() { + local.language_servers.insert( + server_id, + LanguageServerState::Running { + adapter: adapter.clone(), + language: language.clone(), + server: language_server.clone(), + simulate_disk_based_diagnostics_completion: None, + }, + ); + } self.language_server_statuses.insert( server_id, @@ -5409,7 +5797,7 @@ impl LspStore { if file.worktree.read(cx).id() != key.0 || !self .languages - .lsp_adapters(language) + .lsp_adapters(&language.name()) .iter() .any(|a| a.name == key.1) { @@ -5441,7 +5829,7 @@ impl LspStore { lsp::DidOpenTextDocumentParams { text_document: lsp::TextDocumentItem::new( uri, - adapter.language_id(language), + adapter.language_id(&language.name()), version, initial_snapshot.text(), ), @@ -5521,12 +5909,14 @@ impl LspStore { ) -> impl Iterator, &'a Arc)> { self.language_server_ids_for_buffer(buffer, cx) .into_iter() - .filter_map(|server_id| match self.language_servers.get(&server_id)? { - LanguageServerState::Running { - adapter, server, .. - } => Some((adapter, server)), - _ => None, - }) + .filter_map( + |server_id| match self.as_local()?.language_servers.get(&server_id)? { + LanguageServerState::Running { + adapter, server, .. + } => Some((adapter, server)), + _ => None, + }, + ) } pub(crate) fn cancel_language_server_work_for_buffers( @@ -5564,9 +5954,12 @@ impl LspStore { server: Arc, cx: &mut ModelContext, ) { - self.supplementary_language_servers - .insert(id, (name, server)); - cx.emit(LspStoreEvent::LanguageServerAdded(id)); + if let Some(local) = self.as_local_mut() { + local + .supplementary_language_servers + .insert(id, (name, server)); + cx.emit(LspStoreEvent::LanguageServerAdded(id)); + } } pub fn unregister_supplementary_language_server( @@ -5574,27 +5967,33 @@ impl LspStore { id: LanguageServerId, cx: &mut ModelContext, ) { - self.supplementary_language_servers.remove(&id); - cx.emit(LspStoreEvent::LanguageServerRemoved(id)); + if let Some(local) = self.as_local_mut() { + local.supplementary_language_servers.remove(&id); + cx.emit(LspStoreEvent::LanguageServerRemoved(id)); + } } pub fn supplementary_language_servers( &self, - ) -> impl '_ + Iterator { - self.supplementary_language_servers - .iter() - .map(|(id, (name, _))| (id, name)) + ) -> impl '_ + Iterator { + self.as_local().into_iter().flat_map(|local| { + local + .supplementary_language_servers + .iter() + .map(|(id, (name, _))| (*id, name.clone())) + }) } pub fn language_server_adapter_for_id( &self, id: LanguageServerId, ) -> Option> { - if let Some(LanguageServerState::Running { adapter, .. }) = self.language_servers.get(&id) { - Some(adapter.clone()) - } else { - None - } + self.as_local() + .and_then(|local| local.language_servers.get(&id)) + .and_then(|language_server_state| match language_server_state { + LanguageServerState::Running { adapter, .. } => Some(adapter.clone()), + _ => None, + }) } pub(super) fn update_local_worktree_language_servers( @@ -5607,6 +6006,8 @@ impl LspStore { return; } + let Some(local) = self.as_local() else { return }; + let worktree_id = worktree_handle.read(cx).id(); let mut language_server_ids = self .language_server_ids @@ -5621,9 +6022,9 @@ impl LspStore { let abs_path = worktree_handle.read(cx).abs_path(); for server_id in &language_server_ids { if let Some(LanguageServerState::Running { server, .. }) = - self.language_servers.get(server_id) + local.language_servers.get(server_id) { - if let Some(watched_paths) = self + if let Some(watched_paths) = local .language_server_watched_paths .get(server_id) .and_then(|paths| paths.read(cx).worktree_paths.get(&worktree_id)) @@ -5665,8 +6066,11 @@ impl LspStore { token_to_cancel: Option, _cx: &mut ModelContext, ) { + let Some(local) = self.as_local() else { + return; + }; let status = self.language_server_statuses.get(&server_id); - let server = self.language_servers.get(&server_id); + let server = local.language_servers.get(&server_id); if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status) { for (token, progress) in &status.pending_work { if let Some(token_to_cancel) = token_to_cancel.as_ref() { @@ -5715,7 +6119,7 @@ impl LspStore { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); self.languages - .lsp_adapters(language) + .lsp_adapters(&language.name()) .iter() .flat_map(|adapter| { let key = (worktree_id, adapter.name.clone()); @@ -5777,7 +6181,8 @@ impl LspStore { language_server: Arc, cx: &mut AsyncAppContext, ) -> Result { - let fs = this.update(cx, |this, _| this.fs.clone())?; + let fs = this.read_with(cx, |this, _| this.as_local().unwrap().fs.clone())?; + let mut operations = Vec::new(); if let Some(document_changes) = edit.document_changes { match document_changes { @@ -6207,7 +6612,10 @@ impl LanguageServerWatchedPathsBuilder { while let Some(update) = push_updates.0.next().await { let action = lsp_store .update(&mut cx, |this, cx| { - let Some(watcher) = this + let Some(local) = this.as_local() else { + return ControlFlow::Break(()); + }; + let Some(watcher) = local .language_server_watched_paths .get(&language_server_id) else { @@ -6297,13 +6705,27 @@ pub enum LanguageServerState { Starting(Task>>), Running { - language: Arc, + language: LanguageName, adapter: Arc, server: Arc, simulate_disk_based_diagnostics_completion: Option>, }, } +impl std::fmt::Debug for LanguageServerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + LanguageServerState::Starting(_) => { + f.debug_struct("LanguageServerState::Starting").finish() + } + LanguageServerState::Running { language, .. } => f + .debug_struct("LanguageServerState::Running") + .field("language", &language) + .finish(), + } + } +} + #[derive(Clone, Debug, Serialize)] pub struct LanguageServerProgress { pub is_disk_based_diagnostics_progress: bool, @@ -6378,24 +6800,136 @@ fn glob_literal_prefix(glob: &str) -> &str { &glob[..literal_end] } +pub struct SshLspAdapter { + name: LanguageServerName, + binary: LanguageServerBinary, + initialization_options: Option, + code_action_kinds: Option>, +} + +impl SshLspAdapter { + pub fn new( + name: LanguageServerName, + binary: LanguageServerBinary, + initialization_options: Option, + code_action_kinds: Option, + ) -> Self { + Self { + name, + binary, + initialization_options, + code_action_kinds: code_action_kinds + .as_ref() + .and_then(|c| serde_json::from_str(c).ok()), + } + } +} + +#[async_trait(?Send)] +impl LspAdapter for SshLspAdapter { + fn name(&self) -> LanguageServerName { + self.name.clone() + } + + async fn initialization_options( + self: Arc, + _: &Arc, + ) -> Result> { + let Some(options) = &self.initialization_options else { + return Ok(None); + }; + let result = serde_json::from_str(options)?; + Ok(result) + } + + fn code_action_kinds(&self) -> Option> { + self.code_action_kinds.clone() + } + + async fn check_if_user_installed( + &self, + _: &dyn LspAdapterDelegate, + _: &AsyncAppContext, + ) -> Option { + Some(self.binary.clone()) + } + + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + None + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version") + } + + async fn fetch_server_binary( + &self, + _: Box, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Result { + anyhow::bail!("SshLspAdapter does not support fetch_server_binary") + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } +} + pub struct ProjectLspAdapterDelegate { lsp_store: WeakModel, worktree: worktree::Snapshot, - fs: Arc, + fs: Option>, http_client: Arc, language_registry: Arc, load_shell_env_task: Shared>>>, } impl ProjectLspAdapterDelegate { - pub fn new( + fn for_local( lsp_store: &LspStore, worktree: &Model, cx: &mut ModelContext, + ) -> Arc { + let local = lsp_store + .as_local() + .expect("ProjectLspAdapterDelegate cannot be constructed on a remote"); + + let http_client = local + .http_client + .clone() + .unwrap_or_else(|| Arc::new(BlockedHttpClient)); + + Self::new(lsp_store, worktree, http_client, Some(local.fs.clone()), cx) + } + + fn for_ssh( + lsp_store: &LspStore, + worktree: &Model, + cx: &mut ModelContext, + ) -> Arc { + Self::new(lsp_store, worktree, Arc::new(BlockedHttpClient), None, cx) + } + + pub fn new( + lsp_store: &LspStore, + worktree: &Model, + http_client: Arc, + fs: Option>, + cx: &mut ModelContext, ) -> Arc { let worktree_id = worktree.read(cx).id(); let worktree_abs_path = worktree.read(cx).abs_path(); - let load_shell_env_task = if let Some(environment) = &lsp_store.environment { + let load_shell_env_task = if let Some(environment) = + &lsp_store.as_local().map(|local| local.environment.clone()) + { environment.update(cx, |env, cx| { env.get_environment(Some(worktree_id), Some(worktree_abs_path), cx) }) @@ -6403,14 +6937,10 @@ impl ProjectLspAdapterDelegate { Task::ready(None).shared() }; - let Some(http_client) = lsp_store.http_client.clone() else { - panic!("ProjectLspAdapterDelegate cannot be constructedd on an ssh-remote yet") - }; - Arc::new(Self { lsp_store: cx.weak_model(), worktree: worktree.read(cx).snapshot(), - fs: lsp_store.fs.clone(), + fs, http_client, language_registry: lsp_store.languages.clone(), load_shell_env_task, @@ -6418,6 +6948,26 @@ impl ProjectLspAdapterDelegate { } } +struct BlockedHttpClient; + +impl HttpClient for BlockedHttpClient { + fn send( + &self, + _req: Request, + ) -> BoxFuture<'static, Result, Error>> { + Box::pin(async { + Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "ssh host blocked http connection", + ) + .into()) + }) + } + + fn proxy(&self) -> Option<&Uri> { + None + } +} #[async_trait] impl LspAdapterDelegate for ProjectLspAdapterDelegate { fn show_notification(&self, message: &str, cx: &mut AppContext) { @@ -6447,6 +6997,7 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { #[cfg(not(target_os = "windows"))] async fn which(&self, command: &OsStr) -> Option { + self.fs.as_ref()?; let worktree_abs_path = self.worktree.abs_path(); let shell_path = self.shell_env().await.get("PATH").cloned(); which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() @@ -6454,6 +7005,8 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { #[cfg(target_os = "windows")] async fn which(&self, command: &OsStr) -> Option { + self.fs.as_ref()?; + // todo(windows) Getting the shell env variables in a current directory on Windows is more complicated than other platforms // there isn't a 'default shell' necessarily. The closest would be the default profile on the windows terminal // SEE: https://learn.microsoft.com/en-us/windows/terminal/customize-settings/startup @@ -6472,17 +7025,20 @@ impl LspAdapterDelegate for ProjectLspAdapterDelegate { async fn read_text_file(&self, path: PathBuf) -> Result { if self.worktree.entry_for_path(&path).is_none() { return Err(anyhow!("no such path {path:?}")); + }; + if let Some(fs) = &self.fs { + let content = fs.load(&path).await?; + Ok(content) + } else { + return Err(anyhow!("cannot open {path:?} on ssh host (yet!)")); } - let path = self.worktree.absolutize(path.as_ref())?; - let content = self.fs.load(&path).await?; - Ok(content) } } async fn populate_labels_for_symbols( symbols: Vec, language_registry: &Arc, - default_language: Option>, + default_language: Option, lsp_adapter: Option>, output: &mut Vec, ) { @@ -6497,7 +7053,12 @@ async fn populate_labels_for_symbols( .ok() .or_else(|| { unknown_path.get_or_insert(symbol.path.path.clone()); - default_language.clone() + default_language.as_ref().and_then(|name| { + language_registry + .language_for_name(&name.0) + .now_or_never()? + .ok() + }) }); symbols_by_language .entry(language) @@ -6523,9 +7084,12 @@ async fn populate_labels_for_symbols( let mut labels = Vec::new(); if let Some(language) = language { - let lsp_adapter = lsp_adapter - .clone() - .or_else(|| language_registry.lsp_adapters(&language).first().cloned()); + let lsp_adapter = lsp_adapter.clone().or_else(|| { + language_registry + .lsp_adapters(&language.name()) + .first() + .cloned() + }); if let Some(lsp_adapter) = lsp_adapter { labels = lsp_adapter .labels_for_symbols(&label_params, &language) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ed489af687..f67423b073 100644 --- a/crates/project/src/project.rs +++ b/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,17 +709,90 @@ impl Project { fs: Arc, cx: &mut AppContext, ) -> Model { - 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| { + 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); - this.worktree_store.update(cx, |store, _cx| { - store.set_upstream_client(client.clone()); + 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 }); - this.settings_observer = cx.new_model(|cx| { - SettingsObserver::new_ssh(ssh.clone().into(), this.worktree_store.clone(), cx) + 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(); + + let settings_observer = cx.new_model(|cx| { + SettingsObserver::new_ssh(ssh.clone().into(), 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::(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); ssh.subscribe_to_entity(SSH_PROJECT_ID, &this.worktree_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 { + ) -> impl '_ + Iterator { self.lsp_store.read(cx).supplementary_language_servers() } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index c2af1c3597..70b2eccf23 100644 --- a/crates/project/src/project_settings.rs +++ b/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. /// diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ffa206684f..4662c75477 100644 --- a/crates/project/src/project_tests.rs +++ b/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::(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::(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 { fn js_lang() -> Arc { Arc::new(Language::new( LanguageConfig { - name: Arc::from("JavaScript"), + name: "JavaScript".into(), matcher: LanguageMatcher { path_suffixes: vec!["js".to_string()], ..Default::default() diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 95ae6aee13..314903ec5d 100644 --- a/crates/project/src/task_inventory.rs +++ b/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() diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 3d464904b8..b24d939965 100644 --- a/crates/proto/proto/zed.proto +++ b/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 { + +// } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d8ebf66588..44cb91db10 100644 --- a/crates/proto/src/proto.rs +++ b/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!( diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/quick_action_bar/src/repl_menu.rs index fbf2ac17e5..f4e4cd2d1a 100644 --- a/crates/quick_action_bar/src/repl_menu.rs +++ b/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, }; diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 4bee1c5a9f..8da4284b7f 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/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<()> { diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 91f7b330e4..5ff11fe099 100644 --- a/crates/remote/src/ssh_session.rs +++ b/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, spawn_process_tx: mpsc::UnboundedSender, client_socket: Option, - state: Mutex, + state: Mutex, // Lock } struct SshClientState { @@ -392,9 +392,9 @@ impl SshSession { ) -> impl 'static + Future> { 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) } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 60f29bb573..ca5fe06e13 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/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, fs: Arc, cx: &mut ModelContext) -> 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, diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 0aea585538..67a2f0b57d 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/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(); diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index 112cf591e9..868594aaf1 100644 --- a/crates/repl/src/repl_editor.rs +++ b/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, move_down: bool, cx: &mut WindowContext) -> pub enum SessionSupport { ActiveSession(View), Inactive(Box), - RequiresSetup(Arc), + RequiresSetup(LanguageName), Unsupported, } @@ -268,7 +268,7 @@ fn runnable_ranges( range: Range, ) -> (Vec>, Option) { 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) -> Vec) -> bool { - match language.name().as_ref() { + match language.name().0.as_ref() { "TypeScript" | "Python" => true, _ => false, } diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 82aad401a4..c6e64deb59 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -564,6 +564,13 @@ impl Worktree { !self.is_local() } + pub fn settings_location(&self, _: &ModelContext) -> 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(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9ec43d607a..93fee57ecd 100644 --- a/crates/zed/src/zed.rs +++ b/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() ) }); })