diff --git a/Cargo.lock b/Cargo.lock index 3db783c120f3e..da82ffa84a60f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7396,6 +7396,7 @@ dependencies = [ "serde", "serde_json", "smol", + "text", "util", ] @@ -9832,6 +9833,7 @@ dependencies = [ "log", "lsp", "node_runtime", + "once_cell", "parking_lot", "pathdiff", "paths", diff --git a/Cargo.toml b/Cargo.toml index e7481808e22e9..b958452a02ba9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -372,7 +372,7 @@ async-tungstenite = "0.28" async-watch = "0.3.1" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } base64 = "0.22" -bitflags = "2.6.0" +bitflags = "2.8.0" blade-graphics = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } blade-util = { git = "https://github.com/kvark/blade", rev = "091a8401033847bb9b6ace3fcf70448d069621c5" } @@ -420,12 +420,13 @@ libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev="060964da10574cd9bf06463a53bf6e0769c5c45e", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false } -log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } +log = { version = "0.4.25", features = ["kv_unstable_serde", "serde"] } markup5ever_rcdom = "0.3.0" nanoid = "0.4" nbformat = { version = "0.10.0" } nix = "0.29" num-format = "0.4.4" +once_cell = "1.20" ordered-float = "2.1.1" palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" @@ -508,7 +509,7 @@ tree-sitter = { version = "0.23", features = ["wasm"] } tree-sitter-bash = "0.23" tree-sitter-c = "0.23" tree-sitter-cpp = "0.23" -tree-sitter-css = "0.23" +tree-sitter-css = "0.23.2" tree-sitter-elixir = "0.3" tree-sitter-embedded-template = "0.23.0" tree-sitter-go = "0.23" diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 020a86c9f8052..7437546cba424 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -53,7 +53,7 @@ reqwest_client.workspace = true rpc.workspace = true rustc-demangle.workspace = true scrypt = "0.11" -sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } +sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } semantic_version.workspace = true semver.workspace = true serde.workspace = true @@ -116,7 +116,7 @@ release_channel.workspace = true remote = { workspace = true, features = ["test-support"] } remote_server.workspace = true rpc = { workspace = true, features = ["test-support"] } -sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] } +sea-orm = { version = "1.1.4", features = ["sqlx-sqlite"] } serde_json.workspace = true session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 67280765f6643..9ffb5f3fb9691 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -461,12 +461,14 @@ impl Copilot { .on_notification::(|_, _| { /* Silence the notification */ }) .detach(); - let initialize_params = None; let configuration = lsp::DidChangeConfigurationParams { settings: Default::default(), }; let server = cx - .update(|cx| server.initialize(initialize_params, configuration.into(), cx))? + .update(|cx| { + let params = server.default_initialize_params(cx); + server.initialize(params, configuration.into(), cx) + })? .await?; let status = server diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9efc74c787cfd..ad13de90b65c0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -12476,28 +12476,27 @@ impl Editor { cx.emit(SearchEvent::MatchesInvalidated); if *singleton_buffer_edited { if let Some(project) = &self.project { - let project = project.read(cx); #[allow(clippy::mutable_key_type)] - let languages_affected = multibuffer - .read(cx) - .all_buffers() - .into_iter() - .filter_map(|buffer| { - let buffer = buffer.read(cx); - let language = buffer.language()?; - if project.is_local() - && project - .language_servers_for_local_buffer(buffer, cx) - .count() - == 0 - { - None - } else { - Some(language) - } - }) - .cloned() - .collect::>(); + let languages_affected = multibuffer.update(cx, |multibuffer, cx| { + multibuffer + .all_buffers() + .into_iter() + .filter_map(|buffer| { + buffer.update(cx, |buffer, cx| { + let language = buffer.language()?; + let should_discard = project.update(cx, |project, cx| { + project.is_local() + && project.for_language_servers_for_local_buffer( + buffer, + |it| it.count() == 0, + cx, + ) + }); + should_discard.not().then_some(language.clone()) + }) + }) + .collect::>() + }); if !languages_affected.is_empty() { self.refresh_inlay_hints( InlayHintRefreshReason::BufferEdited(languages_affected), @@ -13051,15 +13050,18 @@ impl Editor { self.handle_input(text, cx); } - pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool { + pub fn supports_inlay_hints(&self, cx: &mut AppContext) -> bool { let Some(provider) = self.semantics_provider.as_ref() else { return false; }; let mut supports = false; - self.buffer().read(cx).for_each_buffer(|buffer| { - supports |= provider.supports_inlay_hints(buffer, cx); + self.buffer().update(cx, |this, cx| { + this.for_each_buffer(|buffer| { + supports |= provider.supports_inlay_hints(buffer, cx); + }) }); + supports } @@ -13671,7 +13673,7 @@ pub trait SemanticsProvider { cx: &mut AppContext, ) -> Option>>; - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool; + fn supports_inlay_hints(&self, buffer: &Model, cx: &mut AppContext) -> bool; fn document_highlights( &self, @@ -14056,17 +14058,25 @@ impl SemanticsProvider for Model { })) } - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool { + fn supports_inlay_hints(&self, buffer: &Model, cx: &mut AppContext) -> bool { // TODO: make this work for remote projects - self.read(cx) - .language_servers_for_local_buffer(buffer.read(cx), cx) - .any( - |(_, server)| match server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Left(enabled)) => enabled, - Some(lsp::OneOf::Right(_)) => true, - None => false, - }, - ) + buffer.update(cx, |buffer, cx| { + self.update(cx, |this, cx| { + this.for_language_servers_for_local_buffer( + buffer, + |mut it| { + it.any( + |(_, server)| match server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Left(enabled)) => enabled, + Some(lsp::OneOf::Right(_)) => true, + None => false, + }, + ) + }, + cx, + ) + }) + }) } fn inlay_hints( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6f789acce766e..f0a9fd90af512 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6839,7 +6839,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs, ["/".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -7193,7 +7193,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs, ["/".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); @@ -7327,7 +7327,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.executor()); fs.insert_file("/file.rs", Default::default()).await; - let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; + let project = Project::test(fs, ["/".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(Arc::new(Language::new( @@ -10742,7 +10742,6 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test 0, "Should not restart LSP server on an unrelated LSP settings change" ); - update_test_project_settings(cx, |project_settings| { project_settings.lsp.insert( language_server_name.into(), diff --git a/crates/editor/src/lsp_ext.rs b/crates/editor/src/lsp_ext.rs index 2937e75943970..e4c28243a1a03 100644 --- a/crates/editor/src/lsp_ext.rs +++ b/crates/editor/src/lsp_ext.rs @@ -11,7 +11,7 @@ use multi_buffer::Anchor; pub(crate) fn find_specific_language_server_in_selection( editor: &Editor, - cx: &WindowContext, + cx: &mut WindowContext, filter_language: F, language_server_name: &str, ) -> Option<(Anchor, Arc, LanguageServerId, Model)> @@ -21,7 +21,6 @@ where let Some(project) = &editor.project else { return None; }; - let multibuffer = editor.buffer().read(cx); let mut language_servers_for = HashMap::default(); editor .selections @@ -29,29 +28,33 @@ where .iter() .filter(|selection| selection.start == selection.end) .filter_map(|selection| Some((selection.start.buffer_id?, selection.start))) - .filter_map(|(buffer_id, trigger_anchor)| { - let buffer = multibuffer.buffer(buffer_id)?; + .find_map(|(buffer_id, trigger_anchor)| { + let buffer = editor.buffer().read(cx).buffer(buffer_id)?; let server_id = *match language_servers_for.entry(buffer_id) { Entry::Occupied(occupied_entry) => occupied_entry.into_mut(), Entry::Vacant(vacant_entry) => { - let language_server_id = project - .read(cx) - .language_servers_for_local_buffer(buffer.read(cx), cx) - .find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == language_server_name { - Some(server.server_id()) - } else { - None - } - }); + let language_server_id = buffer.update(cx, |buffer, cx| { + project.update(cx, |project, cx| { + project.for_language_servers_for_local_buffer( + buffer, + |mut it| { + it.find_map(|(adapter, server)| { + if adapter.name.0.as_ref() == language_server_name { + Some(server.server_id()) + } else { + None + } + }) + }, + cx, + ) + }) + }); vacant_entry.insert(language_server_id) } } .as_ref()?; - Some((buffer, trigger_anchor, server_id)) - }) - .find_map(|(buffer, trigger_anchor, server_id)| { let language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?; if !filter_language(&language) { return None; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 923dcc24b9f04..abfda04e7b49a 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -455,7 +455,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { self.0.resolve_inlay_hint(hint, buffer, server_id, cx) } - fn supports_inlay_hints(&self, buffer: &Model, cx: &AppContext) -> bool { + fn supports_inlay_hints(&self, buffer: &Model, cx: &mut AppContext) -> bool { if let Some(buffer) = self.to_base(&buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) } else { diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index 693606619275a..e605bbb2f2e69 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -14,6 +14,6 @@ proc-macro = true doctest = false [dependencies] -proc-macro2 = "1.0.66" +proc-macro2 = "1.0.93" quote = "1.0.9" syn = { version = "1.0.72", features = ["full", "extra-traits"] } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8061c7efa9879..2769e0fe0bc86 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -45,7 +45,6 @@ use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use settings::WorktreeId; use smol::future::FutureExt as _; -use std::num::NonZeroU32; use std::{ any::Any, ffi::OsStr, @@ -61,6 +60,7 @@ use std::{ Arc, LazyLock, }, }; +use std::{num::NonZeroU32, sync::OnceLock}; use syntax_map::{QueryCursorHandle, SyntaxSnapshot}; use task::RunnableTag; pub use task_context::{ContextProvider, RunnableRange}; @@ -163,6 +163,7 @@ pub struct CachedLspAdapter { pub adapter: Arc, pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, + attach_kind: OnceLock, } impl Debug for CachedLspAdapter { @@ -198,6 +199,7 @@ impl CachedLspAdapter { adapter, cached_binary: Default::default(), reinstall_attempt_count: AtomicU64::new(0), + attach_kind: Default::default(), }) } @@ -259,6 +261,38 @@ impl CachedLspAdapter { .cloned() .unwrap_or_else(|| language_name.lsp_id()) } + pub fn find_project_root( + &self, + path: &Path, + ancestor_depth: usize, + delegate: &Arc, + ) -> Option> { + self.adapter + .find_project_root(path, ancestor_depth, delegate) + } + pub fn attach_kind(&self) -> Attach { + *self.attach_kind.get_or_init(|| self.adapter.attach_kind()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Attach { + /// Create a single language server instance per subproject root. + InstancePerRoot, + /// Use one shared language server instance for all subprojects within a project. + Shared, +} + +impl Attach { + pub fn root_path( + &self, + root_subproject_path: (WorktreeId, Arc), + ) -> (WorktreeId, Arc) { + match self { + Attach::InstancePerRoot => root_subproject_path, + Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))), + } + } } /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application @@ -505,6 +539,19 @@ pub trait LspAdapter: 'static + Send + Sync { fn prepare_initialize_params(&self, original: InitializeParams) -> Result { Ok(original) } + fn attach_kind(&self) -> Attach { + Attach::Shared + } + fn find_project_root( + &self, + + _path: &Path, + _ancestor_depth: usize, + _: &Arc, + ) -> Option> { + // By default all language servers are rooted at the root of the worktree. + Some(Arc::from("".as_ref())) + } } async fn try_fetch_server_binary( diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 794ab0784ea3c..51d464d409752 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -96,6 +96,7 @@ struct LanguageRegistryState { available_languages: Vec, grammars: HashMap, AvailableGrammar>, lsp_adapters: HashMap>>, + all_lsp_adapters: HashMap>, available_lsp_adapters: HashMap Arc + 'static + Send + Sync>>, loading_languages: HashMap>>>>, @@ -222,6 +223,7 @@ impl LanguageRegistry { language_settings: Default::default(), loading_languages: Default::default(), lsp_adapters: Default::default(), + all_lsp_adapters: Default::default(), available_lsp_adapters: HashMap::default(), subscription: watch::channel(), theme: Default::default(), @@ -344,12 +346,16 @@ impl LanguageRegistry { adapter: Arc, ) -> Arc { let cached = CachedLspAdapter::new(adapter); - self.state - .write() + let mut state = self.state.write(); + state .lsp_adapters .entry(language_name) .or_default() .push(cached.clone()); + state + .all_lsp_adapters + .insert(cached.name.clone(), cached.clone()); + cached } @@ -389,12 +395,17 @@ impl LanguageRegistry { let adapter_name = LanguageServerName(adapter.name.into()); let capabilities = adapter.capabilities.clone(); let initializer = adapter.initializer.take(); - self.state - .write() - .lsp_adapters - .entry(language_name.clone()) - .or_default() - .push(CachedLspAdapter::new(Arc::new(adapter))); + let adapter = CachedLspAdapter::new(Arc::new(adapter)); + { + let mut state = self.state.write(); + state + .lsp_adapters + .entry(language_name.clone()) + .or_default() + .push(adapter.clone()); + state.all_lsp_adapters.insert(adapter.name(), adapter); + } + self.register_fake_language_server(adapter_name, capabilities, initializer) } @@ -407,12 +418,16 @@ impl LanguageRegistry { adapter: crate::FakeLspAdapter, ) { let language_name = language_name.into(); - self.state - .write() + let mut state = self.state.write(); + let cached_adapter = CachedLspAdapter::new(Arc::new(adapter)); + state .lsp_adapters .entry(language_name.clone()) .or_default() - .push(CachedLspAdapter::new(Arc::new(adapter))); + .push(cached_adapter.clone()); + state + .all_lsp_adapters + .insert(cached_adapter.name(), cached_adapter); } /// Register a fake language server (without the adapter) @@ -880,6 +895,10 @@ impl LanguageRegistry { .unwrap_or_default() } + pub fn adapter_for_name(&self, name: &LanguageServerName) -> Option> { + self.state.read().all_lsp_adapters.get(name).cloned() + } + pub fn update_lsp_status( &self, server_name: LanguageServerName, diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 184a06f21589a..62cfdeedcbce7 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -730,7 +730,8 @@ impl LspLogView { * Binary: {BINARY:#?} -* Running in project: {PATH:?} +* Registered workspace folders: +{WORKSPACE_FOLDERS} * Capabilities: {CAPABILITIES} @@ -738,7 +739,15 @@ impl LspLogView { NAME = server.name(), ID = server.server_id(), BINARY = server.binary(), - PATH = server.root_path(), + WORKSPACE_FOLDERS = server + .workspace_folders() + .iter() + .filter_map(|path| path + .to_file_path() + .ok() + .map(|path| path.to_string_lossy().into_owned())) + .collect::>() + .join(", "), CAPABILITIES = serde_json::to_string_pretty(&server.capabilities()) .unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")), CONFIGURATION = serde_json::to_string_pretty(server.configuration()) diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3ef27476427e4..cdd094a129c69 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -74,6 +74,22 @@ impl LspAdapter for RustLspAdapter { Self::SERVER_NAME.clone() } + fn find_project_root( + &self, + path: &Path, + ancestor_depth: usize, + delegate: &Arc, + ) -> Option> { + let mut outermost_cargo_toml = None; + for path in path.ancestors().take(ancestor_depth) { + let p = path.join("Cargo.toml").to_path_buf(); + if smol::block_on(delegate.read_text_file(p)).is_ok() { + outermost_cargo_toml = Some(Arc::from(path)); + } + } + + outermost_cargo_toml + } async fn check_if_user_installed( &self, delegate: &dyn LspAdapterDelegate, diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 0937b47217c09..eba4d0e8707cf 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -29,6 +29,7 @@ serde.workspace = true serde_json.workspace = true schemars.workspace = true smol.workspace = true +text.workspace = true util.workspace = true release_channel.workspace = true diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e9fa1caac2398..51de3bc9ce0fc 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -7,6 +7,7 @@ use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt}; use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, SharedString, Task}; +use notification::DidChangeWorkspaceFolders; use parking_lot::{Mutex, RwLock}; use postage::{barrier, prelude::Stream}; use schemars::{ @@ -21,12 +22,14 @@ use smol::{ io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Child, }; +use text::BufferId; use std::{ + collections::BTreeSet, ffi::{OsStr, OsString}, fmt, io::Write, - ops::DerefMut, + ops::{Deref, DerefMut}, path::PathBuf, pin::Pin, sync::{ @@ -96,9 +99,9 @@ pub struct LanguageServer { #[allow(clippy::type_complexity)] io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, - root_path: PathBuf, - working_dir: PathBuf, server: Arc>>, + workspace_folders: Arc>>, + registered_buffers: Arc>>, } /// Identifies a running language server. @@ -376,8 +379,6 @@ impl LanguageServer { Some(stderr), stderr_capture, Some(server), - root_path, - working_dir, code_action_kinds, binary, cx, @@ -403,8 +404,6 @@ impl LanguageServer { stderr: Option, stderr_capture: Arc>>, server: Option, - root_path: &Path, - working_dir: &Path, code_action_kinds: Option>, binary: LanguageServerBinary, cx: AsyncAppContext, @@ -488,9 +487,9 @@ impl LanguageServer { executor: cx.background_executor().clone(), io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), - root_path: root_path.to_path_buf(), - working_dir: working_dir.to_path_buf(), server: Arc::new(Mutex::new(server)), + workspace_folders: Default::default(), + registered_buffers: Default::default(), } } @@ -615,12 +614,11 @@ impl LanguageServer { } pub fn default_initialize_params(&self, cx: &AppContext) -> InitializeParams { - let root_uri = Url::from_file_path(&self.working_dir).unwrap(); #[allow(deprecated)] InitializeParams { process_id: None, root_path: None, - root_uri: Some(root_uri.clone()), + root_uri: None, initialization_options: None, capabilities: ClientCapabilities { general: Some(GeneralClientCapabilities { @@ -787,10 +785,7 @@ impl LanguageServer { }), }, trace: None, - workspace_folders: Some(vec![WorkspaceFolder { - uri: root_uri, - name: Default::default(), - }]), + workspace_folders: None, client_info: release_channel::ReleaseChannel::try_global(cx).map(|release_channel| { ClientInfo { name: release_channel.display_name().to_string(), @@ -809,16 +804,10 @@ impl LanguageServer { /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) pub fn initialize( mut self, - initialize_params: Option, + params: InitializeParams, configuration: Arc, cx: &AppContext, ) -> Task>> { - let params = if let Some(params) = initialize_params { - params - } else { - self.default_initialize_params(cx) - }; - cx.spawn(|_| async move { let response = self.request::(params).await?; if let Some(info) = response.server_info { @@ -1070,16 +1059,10 @@ impl LanguageServer { self.server_id } - /// Get the root path of the project the language server is running against. - pub fn root_path(&self) -> &PathBuf { - &self.root_path - } - /// Language server's binary information. pub fn binary(&self) -> &LanguageServerBinary { &self.binary } - /// Sends a RPC request to the language server. /// /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) @@ -1207,6 +1190,129 @@ impl LanguageServer { outbound_tx.try_send(message)?; Ok(()) } + + /// Add new workspace folder to the list. + pub fn add_workspace_folder(&self, uri: Url) { + if self + .capabilities() + .workspace + .and_then(|ws| { + ws.workspace_folders.and_then(|folders| { + folders + .change_notifications + .map(|caps| matches!(caps, OneOf::Left(false))) + }) + }) + .unwrap_or(true) + { + return; + } + + let is_new_folder = self.workspace_folders.lock().insert(uri.clone()); + if is_new_folder { + let params = DidChangeWorkspaceFoldersParams { + event: WorkspaceFoldersChangeEvent { + added: vec![WorkspaceFolder { + uri, + name: String::default(), + }], + removed: vec![], + }, + }; + self.notify::(¶ms).log_err(); + } + } + /// Add new workspace folder to the list. + pub fn remove_workspace_folder(&self, uri: Url) { + if self + .capabilities() + .workspace + .and_then(|ws| { + ws.workspace_folders.and_then(|folders| { + folders + .change_notifications + .map(|caps| !matches!(caps, OneOf::Left(false))) + }) + }) + .unwrap_or(true) + { + return; + } + let was_removed = self.workspace_folders.lock().remove(&uri); + if was_removed { + let params = DidChangeWorkspaceFoldersParams { + event: WorkspaceFoldersChangeEvent { + added: vec![], + removed: vec![WorkspaceFolder { + uri, + name: String::default(), + }], + }, + }; + self.notify::(¶ms).log_err(); + } + } + pub fn set_workspace_folders(&self, folders: BTreeSet) { + let mut workspace_folders = self.workspace_folders.lock(); + let added: Vec<_> = folders + .iter() + .map(|uri| WorkspaceFolder { + uri: uri.clone(), + name: String::default(), + }) + .collect(); + + let removed: Vec<_> = std::mem::replace(&mut *workspace_folders, folders) + .into_iter() + .map(|uri| WorkspaceFolder { + uri: uri.clone(), + name: String::default(), + }) + .collect(); + let should_notify = !added.is_empty() || !removed.is_empty(); + + if should_notify { + let params = DidChangeWorkspaceFoldersParams { + event: WorkspaceFoldersChangeEvent { added, removed }, + }; + self.notify::(¶ms).log_err(); + } + } + + pub fn workspace_folders(&self) -> impl Deref> + '_ { + self.workspace_folders.lock() + } + + pub fn register_buffer( + &self, + buffer_id: BufferId, + uri: Url, + language_id: String, + version: i32, + initial_text: String, + ) { + let previous_value = self + .registered_buffers + .lock() + .insert(buffer_id, uri.clone()); + if previous_value.is_none() { + self.notify::(&DidOpenTextDocumentParams { + text_document: TextDocumentItem::new(uri, language_id, version, initial_text), + }) + .log_err(); + } else { + debug_assert_eq!(previous_value, Some(uri)); + } + } + + pub fn unregister_buffer(&self, buffer_id: BufferId) { + if let Some(path) = self.registered_buffers.lock().remove(&buffer_id) { + self.notify::(&DidCloseTextDocumentParams { + text_document: TextDocumentIdentifier::new(path), + }) + .log_err(); + } + } } impl Drop for LanguageServer { @@ -1288,8 +1394,6 @@ impl FakeLanguageServer { let (stdout_writer, stdout_reader) = async_pipe::pipe(); let (notifications_tx, notifications_rx) = channel::unbounded(); - let root = Self::root_path(); - let server_name = LanguageServerName(name.clone().into()); let process_name = Arc::from(name.as_str()); let mut server = LanguageServer::new_internal( @@ -1300,8 +1404,6 @@ impl FakeLanguageServer { None::, Arc::new(Mutex::new(None)), None, - root, - root, None, binary.clone(), cx.clone(), @@ -1319,8 +1421,6 @@ impl FakeLanguageServer { None::, Arc::new(Mutex::new(None)), None, - root, - root, None, binary, cx.clone(), @@ -1357,16 +1457,6 @@ impl FakeLanguageServer { (server, fake) } - - #[cfg(target_os = "windows")] - fn root_path() -> &'static Path { - Path::new("C:\\") - } - - #[cfg(not(target_os = "windows"))] - fn root_path() -> &'static Path { - Path::new("/") - } } #[cfg(any(test, feature = "test-support"))] @@ -1554,12 +1644,14 @@ mod tests { }) .detach(); - let initialize_params = None; - let configuration = DidChangeConfigurationParams { - settings: Default::default(), - }; let server = cx - .update(|cx| server.initialize(initialize_params, configuration.into(), cx)) + .update(|cx| { + let params = server.default_initialize_params(cx); + let configuration = DidChangeConfigurationParams { + settings: Default::default(), + }; + server.initialize(params, configuration.into(), cx) + }) .await .unwrap(); server diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index b9fcd8df0e245..c0d770c5d5760 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -283,13 +283,13 @@ impl Prettier { ) .context("prettier server creation")?; - let initialize_params = None; - let configuration = lsp::DidChangeConfigurationParams { - settings: Default::default(), - }; let server = cx .update(|cx| { - executor.spawn(server.initialize(initialize_params, configuration.into(), cx)) + let params = server.default_initialize_params(cx); + let configuration = lsp::DidChangeConfigurationParams { + settings: Default::default(), + }; + executor.spawn(server.initialize(params, configuration.into(), cx)) })? .await .context("prettier server initialization")?; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 5149a818cf557..7c2c546e702e3 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -43,6 +43,7 @@ log.workspace = true lsp.workspace = true node_runtime.workspace = true image.workspace = true +once_cell.workspace = true parking_lot.workspace = true pathdiff.workspace = true paths.workspace = true diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c58fcb1351e1f..e69b7b95b0420 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -942,9 +942,11 @@ fn language_server_for_buffer( ) -> Result<(Arc, Arc)> { lsp_store .update(cx, |lsp_store, cx| { - lsp_store - .language_server_for_local_buffer(buffer.read(cx), server_id, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) + buffer.update(cx, |buffer, cx| { + lsp_store + .language_server_for_local_buffer(buffer, server_id, cx) + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) })? .ok_or_else(|| anyhow!("no language server found for buffer")) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index ba88b5e485032..9e3960c9254d1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6,6 +6,7 @@ use crate::{ lsp_ext_command, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, + project_tree::{LanguageServerTree, LaunchDisposition, ProjectTree}, relativize_path, resolve_path, toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, @@ -38,9 +39,9 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language, - LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, - LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, - Transaction, Unclipped, + LanguageRegistry, LanguageServerBinaryStatus, LanguageToolchainStore, LocalFile, LspAdapter, + LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{ notification::DidRenameFiles, CodeActionKind, CompletionContext, DiagnosticSeverity, @@ -48,8 +49,8 @@ use lsp::{ FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf, - RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, Url, - WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, + RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, WillRenameFiles, + WorkDoneProgressCancelParams, WorkspaceFolder, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -78,6 +79,7 @@ use std::{ time::{Duration, Instant}, }; use text::{Anchor, BufferId, LineEnding, OffsetRangeExt}; +use url::Url; use util::{ debug_panic, defer, maybe, merge_json_value_into, paths::SanitizedPath, post_inc, ResultExt, TryFutureExt as _, @@ -130,13 +132,14 @@ impl FormatTrigger { } pub struct LocalLspStore { + weak: WeakModel, worktree_store: Model, toolchain_store: Model, http_client: Arc, environment: Model, fs: Arc, languages: Arc, - language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, + language_server_ids: HashMap<(WorktreeId, LanguageServerName), BTreeSet>, yarn: Model, pub language_servers: HashMap, buffers_being_formatted: HashSet, @@ -149,7 +152,6 @@ pub struct LocalLspStore { supplementary_language_servers: HashMap)>, prettier_store: Model, - current_lsp_settings: HashMap, next_diagnostic_group_id: usize, diagnostics: HashMap< WorktreeId, @@ -163,7 +165,7 @@ pub struct LocalLspStore { >, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots _subscription: gpui::Subscription, - registered_buffers: HashMap, + lsp_tree: Model, } impl LocalLspStore { @@ -172,26 +174,15 @@ impl LocalLspStore { worktree_handle: &Model, delegate: Arc, adapter: Arc, - cx: &mut ModelContext, - ) { + settings: Arc, + cx: &mut AppContext, + ) -> LanguageServerId { let worktree = worktree_handle.read(cx); let worktree_id = worktree.id(); let root_path = worktree.abs_path(); let key = (worktree_id, adapter.name.clone()); - if self.language_server_ids.contains_key(&key) { - return; - } - - let project_settings = ProjectSettings::get( - Some(SettingsLocation { - worktree_id, - path: Path::new(""), - }), - cx, - ); - let lsp = project_settings.lsp.get(&adapter.name); - let override_options = lsp.and_then(|s| s.initialization_options.clone()); + let override_options = settings.initialization_options.clone(); let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); @@ -207,12 +198,13 @@ impl LocalLspStore { let adapter = adapter.clone(); let server_name = adapter.name.clone(); let stderr_capture = stderr_capture.clone(); + #[cfg(any(test, feature = "test-support"))] + let lsp_store = self.weak.clone(); - move |_lsp_store, cx| async move { + move |cx| async move { let binary = binary.await?; - #[cfg(any(test, feature = "test-support"))] - if let Some(server) = _lsp_store + if let Some(server) = lsp_store .update(&mut cx.clone(), |this, cx| { this.languages.create_fake_language_server( server_id, @@ -239,13 +231,15 @@ impl LocalLspStore { } }); - let state = LanguageServerState::Starting({ + let pending_workspace_folders: Arc>> = Default::default(); + let startup = { let server_name = adapter.name.0.clone(); let delegate = delegate as Arc; let key = key.clone(); let adapter = adapter.clone(); - - cx.spawn(move |this, mut cx| async move { + let this = self.weak.clone(); + let pending_workspace_folders = pending_workspace_folders.clone(); + cx.spawn(move |mut cx| async move { let result = { let delegate = delegate.clone(); let adapter = adapter.clone(); @@ -292,7 +286,7 @@ impl LocalLspStore { let language_server = cx .update(|cx| { language_server.initialize( - Some(initialization_params), + initialization_params, did_change_configuration_params.clone(), cx, ) @@ -326,6 +320,7 @@ impl LocalLspStore { server.clone(), server_id, key, + pending_workspace_folders, &mut cx, ); }) @@ -348,82 +343,18 @@ impl LocalLspStore { } } }) - }); + }; + let state = LanguageServerState::Starting { + startup, + pending_workspace_folders, + }; self.language_servers.insert(server_id, state); - self.language_server_ids.insert(key, server_id); - } - - pub fn start_language_servers( - &mut self, - worktree: &Model, - language: LanguageName, - cx: &mut ModelContext, - ) { - let root_file = worktree - .update(cx, |tree, cx| tree.root_file(cx)) - .map(|f| f as _); - let settings = language_settings(Some(language.clone()), root_file.as_ref(), cx); - if !settings.enable_language_server { - return; - } - - let available_lsp_adapters = self.languages.clone().lsp_adapters(&language); - let available_language_servers = available_lsp_adapters - .iter() - .map(|lsp_adapter| lsp_adapter.name.clone()) - .collect::>(); - - let desired_language_servers = - settings.customized_language_servers(&available_language_servers); - - let mut enabled_lsp_adapters: Vec> = Vec::new(); - for desired_language_server in desired_language_servers { - if let Some(adapter) = available_lsp_adapters - .iter() - .find(|adapter| adapter.name == desired_language_server) - { - enabled_lsp_adapters.push(adapter.clone()); - continue; - } - - if let Some(adapter) = self - .languages - .load_available_lsp_adapter(&desired_language_server) - { - self.languages - .register_lsp_adapter(language.clone(), adapter.adapter.clone()); - enabled_lsp_adapters.push(adapter); - continue; - } - - log::warn!( - "no language server found matching '{}'", - desired_language_server.0 - ); - } - - for adapter in &enabled_lsp_adapters { - let delegate = LocalLspAdapterDelegate::new( - self.languages.clone(), - &self.environment, - cx.weak_model(), - &worktree, - self.http_client.clone(), - self.fs.clone(), - cx, - ); - self.start_language_server(worktree, delegate, adapter.clone(), cx); - } - - // After starting all the language servers, reorder them to reflect the desired order - // based on the settings. - // - // This is done, in part, to ensure that language servers loaded at different points - // (e.g., native vs extension) still end up in the right order at the end, rather than - // it being based on which language server happened to be loaded in first. - self.languages - .reorder_language_servers(&language, enabled_lsp_adapters); + self.language_server_ids + .entry(key) + .or_default() + .insert(server_id); + server_id } fn get_language_server_binary( @@ -431,7 +362,7 @@ impl LocalLspStore { adapter: Arc, delegate: Arc, allow_binary_download: bool, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Task> { let settings = ProjectSettings::get( Some(SettingsLocation { @@ -446,7 +377,7 @@ impl LocalLspStore { if settings.as_ref().is_some_and(|b| b.path.is_some()) { let settings = settings.unwrap(); - return cx.spawn(|_, _| async move { + return cx.spawn(|_| async move { Ok(LanguageServerBinary { path: PathBuf::from(&settings.path.unwrap()), env: Some(delegate.shell_env().await), @@ -467,7 +398,7 @@ impl LocalLspStore { allow_binary_download, }; let toolchains = self.toolchain_store.read(cx).as_language_toolchain_store(); - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { let binary_result = adapter .clone() .get_language_server_command( @@ -567,14 +498,16 @@ impl LocalLspStore { else { return Ok(None); }; - let root = server.root_path(); - let Ok(uri) = Url::from_file_path(&root) else { - return Ok(None); - }; - Ok(Some(vec![WorkspaceFolder { - uri, - name: Default::default(), - }])) + let root = server.workspace_folders(); + Ok(Some( + root.iter() + .cloned() + .map(|uri| WorkspaceFolder { + uri, + name: Default::default(), + }) + .collect(), + )) } } }) @@ -996,7 +929,7 @@ impl LocalLspStore { use LanguageServerState::*; match server_state { Running { server, .. } => server.shutdown()?.await, - Starting(task) => task.await?.shutdown()?.await, + Starting { startup, .. } => startup.await?.shutdown()?.await, } }) .collect::>(); @@ -1012,42 +945,58 @@ impl LocalLspStore { ) -> impl Iterator> { self.language_server_ids .iter() - .filter_map(move |((language_server_worktree_id, _), id)| { - if *language_server_worktree_id == worktree_id { + .flat_map(move |((language_server_path, _), ids)| { + ids.iter().filter_map(move |id| { + if *language_server_path != worktree_id { + return None; + } if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(id) { return Some(server); + } else { + None } - } - None + }) }) } - pub(crate) fn language_server_ids_for_buffer( + fn language_server_ids_for_buffer( &self, buffer: &Buffer, - cx: &AppContext, + cx: &mut AppContext, ) -> Vec { 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.name()) - .iter() - .flat_map(|adapter| { - let key = (worktree_id, adapter.name.clone()); - self.language_server_ids.get(&key).copied() - }) - .collect() + + let Some(path): Option> = file.path().parent().map(Arc::from) else { + return vec![]; + }; + let worktree_path = ProjectPath { worktree_id, path }; + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return vec![]; + }; + let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); + let root = self.lsp_tree.update(cx, |this, cx| { + this.get(worktree_path, &language.name(), delegate, cx) + .filter_map(|node| node.server_id()) + .collect::>() + }); + + root } else { Vec::new() } } - pub(crate) fn language_servers_for_buffer<'a>( + fn language_servers_for_buffer<'a>( &'a self, buffer: &'a Buffer, - cx: &'a AppContext, + cx: &'a mut AppContext, ) -> impl Iterator, &'a Arc)> { self.language_server_ids_for_buffer(buffer, cx) .into_iter() @@ -1062,7 +1011,7 @@ impl LocalLspStore { fn primary_language_server_for_buffer<'a>( &'a self, buffer: &'a Buffer, - cx: &'a AppContext, + cx: &'a mut AppContext, ) -> Option<(&'a Arc, &'a Arc)> { // The list of language servers is ordered based on the `language_servers` setting // for each language, thus we can consider the first one in the list to be the @@ -1108,20 +1057,22 @@ impl LocalLspStore { for buffer in &buffers { let (primary_adapter_and_server, adapters_and_servers) = lsp_store.update(&mut cx, |lsp_store, cx| { - let buffer = buffer.handle.read(cx); - - let adapters_and_servers = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); + let adapters_and_servers = buffer.handle.update(cx, |buffer, cx| { + lsp_store + .as_local() + .unwrap() + .language_servers_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + .collect::>() + }); - let primary_adapter = lsp_store - .as_local() - .unwrap() - .primary_language_server_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())); + let primary_adapter = buffer.handle.update(cx, |buffer, cx| { + lsp_store + .as_local() + .unwrap() + .primary_language_server_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + }); (primary_adapter, adapters_and_servers) })?; @@ -1730,7 +1681,8 @@ impl LocalLspStore { ) { let buffer = buffer_handle.read(cx); - let Some(file) = File::from_dyn(buffer.file()) else { + let file = buffer.file().cloned(); + let Some(file) = File::from_dyn(file.as_ref()) else { return; }; if !file.is_local() { @@ -1738,7 +1690,6 @@ impl LocalLspStore { } let worktree_id = file.worktree_id(cx); - let language = buffer.language().cloned(); if let Some(diagnostics) = self.diagnostics.get(&worktree_id) { for (server_id, diagnostics) in @@ -1748,45 +1699,6 @@ impl LocalLspStore { .log_err(); } } - - let Some(language) = language else { - return; - }; - 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(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; - - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - } } pub(crate) fn reset_buffer( @@ -1798,14 +1710,35 @@ impl LocalLspStore { buffer.update(cx, |buffer, cx| { let worktree_id = old_file.worktree_id(cx); - let ids = &self.language_server_ids; - if let Some(language) = buffer.language().cloned() { - 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, DiagnosticSet::new([], buffer), cx); - buffer.set_completion_triggers(*server_id, Default::default(), cx); - } + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return; + }; + let Some(path): Option> = old_file.path().parent().map(Arc::from) else { + return; + }; + + let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); + let nodes = self.lsp_tree.update(cx, |this, cx| { + this.get( + ProjectPath { worktree_id, path }, + &language.name(), + delegate, + cx, + ) + .collect::>() + }); + for node in nodes { + let Some(server_id) = node.server_id() else { + continue; + }; + + buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); + buffer.set_completion_triggers(server_id, Default::default(), cx); } } }); @@ -1909,81 +1842,186 @@ impl LocalLspStore { }; let initial_snapshot = buffer.text_snapshot(); let worktree_id = file.worktree_id(cx); - let worktree = file.worktree.clone(); let Some(language) = buffer.language().cloned() else { return; }; - self.start_language_servers(&worktree, language.name(), cx); + let Some(path): Option> = file.path().parent().map(Arc::from) else { + return; + }; + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return; + }; + let delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); + let servers = self.lsp_tree.clone().update(cx, |this, cx| { + this.get( + ProjectPath { worktree_id, path }, + &language.name(), + delegate.clone(), + cx, + ) + .collect::>() + }); + let servers = servers + .into_iter() + .filter_map(|server_node| { + let server_id = server_node.server_id_or_init( + |LaunchDisposition { + server_name, + attach, + path, + settings, + }| match attach { + language::Attach::InstancePerRoot => { + // todo: handle instance per root proper. + if let Some(server_ids) = self + .language_server_ids + .get(&(worktree_id, server_name.clone())) + { + server_ids.iter().cloned().next().unwrap() + } else { + let language_name = language.name(); + + self.start_language_server( + &worktree, + delegate.clone(), + self.languages + .lsp_adapters(&language_name) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"), + settings, + cx, + ) + } + } + language::Attach::Shared => { + let uri = Url::from_directory_path( + worktree.read(cx).abs_path().join(&path.path), + ); + let key = (worktree_id, server_name.clone()); + if !self.language_server_ids.contains_key(&key) { + let language_name = language.name(); + self.start_language_server( + &worktree, + delegate.clone(), + self.languages + .lsp_adapters(&language_name) + .into_iter() + .find(|adapter| &adapter.name() == server_name) + .expect("To find LSP adapter"), + settings, + cx, + ); + } + if let Some(server_ids) = self + .language_server_ids + .get(&key) + { + debug_assert_eq!(server_ids.len(), 1); + let server_id = server_ids.iter().cloned().next().unwrap(); + + if let Some(state) = self.language_servers.get(&server_id) { + if let Ok(uri) = uri { + state.add_workspace_folder(uri); + }; + } + server_id + } else { + unreachable!("Language server ID should be available, as it's registered on demand") + } + } + }, + )?; + let server_state = self.language_servers.get(&server_id)?; + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }) + .collect::>(); + for server in servers { + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + server.server_id(), + server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| { + provider + .trigger_characters + .as_ref() + .map(|characters| characters.iter().cloned().collect()) + }) + .unwrap_or_default(), + cx, + ); + }); + } for adapter in self.languages.lsp_adapters(&language.name()) { - let server = self + let servers = self .language_server_ids .get(&(worktree_id, adapter.name.clone())) - .and_then(|id| self.language_servers.get(id)) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } + .map(|ids| { + ids.iter().flat_map(|id| { + self.language_servers.get(id).and_then(|server_state| { + if let LanguageServerState::Running { server, .. } = server_state { + Some(server.clone()) + } else { + None + } + }) + }) }); - let server = match server { + let servers = match servers { Some(server) => server, None => continue, }; - server - .notify::(&lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri.clone(), - adapter.language_id(&language.name()), - 0, - initial_snapshot.text(), - ), - }) - .log_err(); + for server in servers { + server.register_buffer( + buffer_id, + uri.clone(), + adapter.language_id(&language.name()), + 0, + initial_snapshot.text(), + ); - let snapshot = LspBufferSnapshot { - version: 0, - snapshot: initial_snapshot.clone(), - }; - self.buffer_snapshots - .entry(buffer_id) - .or_default() - .insert(server.server_id(), vec![snapshot]); + let snapshot = LspBufferSnapshot { + version: 0, + snapshot: initial_snapshot.clone(), + }; + self.buffer_snapshots + .entry(buffer_id) + .or_default() + .insert(server.server_id(), vec![snapshot]); + } } } + pub(crate) fn unregister_old_buffer_from_language_servers( &mut self, buffer: &Model, - old_file: &File, - cx: &mut AppContext, ) { - let old_path = match old_file.as_local() { - Some(local) => local.abs_path(cx), - None => return, - }; - let file_url = lsp::Url::from_file_path(old_path).unwrap(); - self.unregister_buffer_from_language_servers(buffer, file_url, cx); + self.unregister_buffer_from_language_servers(buffer, cx); } pub(crate) fn unregister_buffer_from_language_servers( &mut self, buffer: &Model, - file_url: lsp::Url, cx: &mut AppContext, ) { buffer.update(cx, |buffer, cx| { self.buffer_snapshots.remove(&buffer.remote_id()); for (_, language_server) in self.language_servers_for_buffer(buffer, cx) { - language_server - .notify::( - &lsp::DidCloseTextDocumentParams { - text_document: lsp::TextDocumentIdentifier::new(file_url.clone()), - }, - ) - .log_err(); + language_server.unregister_buffer(buffer.remote_id()); } }); } @@ -2509,6 +2547,46 @@ impl LocalLspStore { failure_reason: None, }) } + + fn remove_worktree( + &mut self, + id_to_remove: WorktreeId, + cx: &mut ModelContext<'_, LspStore>, + ) -> Vec { + self.diagnostics.remove(&id_to_remove); + self.prettier_store.update(cx, |prettier_store, cx| { + prettier_store.remove_worktree(id_to_remove, cx); + }); + + let mut servers_to_remove = BTreeMap::default(); + let mut servers_to_preserve = HashSet::default(); + for ((path, server_name), ref server_ids) in &self.language_server_ids { + if *path == id_to_remove { + servers_to_remove.extend(server_ids.iter().map(|id| (*id, server_name.clone()))); + } else { + servers_to_preserve.extend(server_ids.iter().cloned()); + } + } + servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); + + for (server_id_to_remove, _) in &servers_to_remove { + self.language_server_ids + .values_mut() + .for_each(|server_ids| { + server_ids.remove(server_id_to_remove); + }); + self.language_server_watched_paths + .remove(&server_id_to_remove); + self.language_server_paths_watched_for_rename + .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); + cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); + } + servers_to_remove.into_keys().collect() + } + fn rebuild_watched_paths_inner<'a>( &'a self, language_server_id: LanguageServerId, @@ -2845,6 +2923,7 @@ pub struct LanguageServerStatus { struct CoreSymbol { pub language_server_name: LanguageServerName, pub source_worktree_id: WorktreeId, + pub source_language_server_id: LanguageServerId, pub path: ProjectPath, pub name: String, pub kind: lsp::SymbolKind, @@ -2924,23 +3003,6 @@ impl LspStore { } } - pub fn swap_current_lsp_settings( - &mut self, - new_settings: HashMap, - ) -> Option> { - match &mut self.mode { - LspStoreMode::Local(LocalLspStore { - current_lsp_settings, - .. - }) => { - let ret = mem::take(current_lsp_settings); - *current_lsp_settings = new_settings; - Some(ret) - } - LspStoreMode::Remote(_) => None, - } - } - #[allow(clippy::too_many_arguments)] pub fn new_local( buffer_store: Model, @@ -2969,8 +3031,10 @@ impl LspStore { let (sender, receiver) = watch::channel(); (Self::maintain_workspace_config(receiver, cx), sender) }; + let project_tree = ProjectTree::new(worktree_store.clone(), cx); Self { mode: LspStoreMode::Local(LocalLspStore { + weak: cx.weak_model(), worktree_store: worktree_store.clone(), toolchain_store: toolchain_store.clone(), supplementary_language_servers: Default::default(), @@ -2981,7 +3045,6 @@ impl LspStore { language_server_watched_paths: Default::default(), language_server_paths_watched_for_rename: Default::default(), language_server_watcher_registrations: Default::default(), - current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), buffers_being_formatted: Default::default(), buffer_snapshots: Default::default(), prettier_store, @@ -2994,7 +3057,7 @@ impl LspStore { _subscription: cx.on_app_quit(|this, cx| { this.as_local_mut().unwrap().shutdown_language_servers(cx) }), - registered_buffers: HashMap::default(), + lsp_tree: LanguageServerTree::new(project_tree, languages.clone(), cx), }), last_formatting_failure: None, downstream_client: None, @@ -3008,7 +3071,7 @@ impl LspStore { active_entry: None, _maintain_workspace_config, - _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), + _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), } } @@ -3067,17 +3130,6 @@ impl LspStore { } } - fn worktree_for_id( - &self, - worktree_id: WorktreeId, - cx: &ModelContext, - ) -> Result> { - self.worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .ok_or_else(|| anyhow!("worktree not found")) - } - fn on_buffer_store_event( &mut self, _: Model, @@ -3089,22 +3141,19 @@ impl LspStore { self.on_buffer_added(buffer, cx).log_err(); } BufferStoreEvent::BufferChangedFilePath { buffer, old_file } => { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(old_file) = File::from_dyn(old_file.as_ref()) { - if let Some(local) = self.as_local_mut() { + if let Some(local) = self.as_local_mut() { + if let Some(old_file) = File::from_dyn(old_file.as_ref()) { local.reset_buffer(buffer, old_file, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); - } + + local.unregister_old_buffer_from_language_servers(buffer, cx); } } self.detect_language_for_buffer(buffer, cx); if let Some(local) = self.as_local_mut() { local.initialize_buffer(buffer, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer, cx); - } + + local.register_buffer_with_language_servers(buffer, cx); } } BufferStoreEvent::BufferDropped(_) => {} @@ -3235,8 +3284,6 @@ impl LspStore { buffer: &Model, cx: &mut ModelContext, ) -> OpenLspBufferHandle { - let buffer_id = buffer.read(cx).remote_id(); - let handle = cx.new_model(|_| buffer.clone()); if let Some(local) = self.as_local_mut() { @@ -3246,25 +3293,12 @@ impl LspStore { if !file.is_local() { return handle; } - let refcount = local.registered_buffers.entry(buffer_id).or_insert(0); - *refcount += 1; - if *refcount == 1 { - local.register_buffer_with_language_servers(buffer, cx); - } + + local.register_buffer_with_language_servers(buffer, cx); cx.observe_release(&handle, move |this, buffer, cx| { let local = this.as_local_mut().unwrap(); - let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else { - debug_panic!("bad refcounting"); - return; - }; - *refcount -= 1; - if *refcount == 0 { - local.registered_buffers.remove(&buffer_id); - if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() { - local.unregister_old_buffer_from_language_servers(&buffer, &file, cx); - } - } + local.unregister_old_buffer_from_language_servers(&buffer, cx); }) .detach(); } else if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { @@ -3308,14 +3342,10 @@ impl LspStore { .update(cx, |buffer, cx| buffer.set_language(None, cx)); if let Some(local) = this.as_local_mut() { local.reset_buffer(&buffer, &f, cx); - if local - .registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - { - local.unregister_old_buffer_from_language_servers( - &buffer, &f, cx, - ); - } + + local.unregister_old_buffer_from_language_servers( + &buffer, cx, + ); } } } @@ -3341,12 +3371,7 @@ impl LspStore { this.detect_language_for_buffer(&buffer, cx); if let Some(local) = this.as_local_mut() { local.initialize_buffer(&buffer, cx); - if local - .registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - { - local.register_buffer_with_language_servers(&buffer, cx); - } + local.register_buffer_with_language_servers(&buffer, cx); } } @@ -3396,17 +3421,8 @@ impl LspStore { cx: &mut ModelContext, ) { let buffer_file = buffer.read(cx).file().cloned(); - let buffer_id = buffer.read(cx).remote_id(); if let Some(local_store) = self.as_local_mut() { - if local_store.registered_buffers.contains_key(&buffer_id) { - if let Some(abs_path) = - File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) - { - if let Some(file_url) = lsp::Url::from_file_path(&abs_path).log_err() { - local_store.unregister_buffer_from_language_servers(buffer, file_url, cx); - } - } - } + local_store.unregister_buffer_from_language_servers(buffer, cx); } buffer.update(cx, |buffer, cx| { if buffer.language().map_or(true, |old_language| { @@ -3424,9 +3440,7 @@ impl LspStore { let worktree = file.worktree.clone(); if let Some(local) = self.as_local_mut() { - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer, cx); - } + local.register_buffer_with_language_servers(buffer, cx); } Some(worktree.read(cx).id()) } else { @@ -3501,23 +3515,23 @@ impl LspStore { cx, ); } - let buffer = buffer_handle.read(cx); - let language_server = match server { - LanguageServerToQuery::Primary => { - match self - .as_local() - .and_then(|local| local.primary_language_server_for_buffer(buffer, cx)) - { - Some((_, server)) => Some(Arc::clone(server)), - None => return Task::ready(Ok(Default::default())), - } - } + + let Some(language_server) = buffer_handle.update(cx, |buffer, cx| match server { + LanguageServerToQuery::Primary => self + .as_local() + .and_then(|local| local.primary_language_server_for_buffer(buffer, cx)) + .map(|(_, server)| server.clone()), LanguageServerToQuery::Other(id) => self .language_server_for_local_buffer(buffer, id, cx) .map(|(_, server)| Arc::clone(server)), + }) else { + return Task::ready(Ok(Default::default())); }; + + let buffer = buffer_handle.read(cx); let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let (Some(file), Some(language_server)) = (file, language_server) { + + if let Some(file) = file { let lsp_params = match request.to_lsp_params_or_response( &file.abs_path(cx), buffer, @@ -3526,6 +3540,7 @@ impl LspStore { ) { Ok(LspParamsOrResponse::Params(lsp_params)) => lsp_params, Ok(LspParamsOrResponse::Response(response)) => return Task::ready(Ok(response)), + Err(err) => { let message = format!( "{} via {} failed: {}", @@ -3537,6 +3552,7 @@ impl LspStore { return Task::ready(Err(anyhow!(message))); } }; + let status = request.status(); if !request.check_capabilities(language_server.adapter_server_capabilities()) { return Task::ready(Ok(Default::default())); @@ -3611,25 +3627,13 @@ impl LspStore { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let mut language_servers_to_start = Vec::new(); let mut language_formatters_to_check = Vec::new(); for buffer in self.buffer_store.read(cx).buffers() { let buffer = buffer.read(cx); let buffer_file = File::from_dyn(buffer.file()); let buffer_language = buffer.language(); let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - if let Some(language) = buffer_language { - if settings.enable_language_server - && self - .as_local() - .unwrap() - .registered_buffers - .contains_key(&buffer.remote_id()) - { - if let Some(file) = buffer_file { - language_servers_to_start.push((file.worktree.clone(), language.name())); - } - } + if buffer_language.is_some() { language_formatters_to_check.push(( buffer_file.map(|f| f.worktree_id(cx)), settings.into_owned(), @@ -3637,80 +3641,66 @@ impl LspStore { } } - let mut language_servers_to_stop = Vec::new(); - let mut language_servers_to_restart = Vec::new(); - let languages = self.languages.to_vec(); + let mut to_stop = Vec::new(); + if let Some(local) = self.as_local_mut() { + local.lsp_tree.clone().update(cx, |this, cx| { + let mut get_adapter = { + let languages = local.languages.clone(); + let environment = local.environment.clone(); + let weak = local.weak.clone(); + let worktree_store = local.worktree_store.clone(); + let http_client = local.http_client.clone(); + let fs = local.fs.clone(); + move |worktree_id, cx: &mut AppContext| -> Option> { + let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; + Some(LocalLspAdapterDelegate::new( + languages.clone(), + &environment, + weak.clone(), + &worktree, + http_client.clone(), + fs.clone(), + cx, + )) + } + }; - let new_lsp_settings = ProjectSettings::get_global(cx).lsp.clone(); - let Some(current_lsp_settings) = self.swap_current_lsp_settings(new_lsp_settings.clone()) - else { - return; - }; - for (worktree_id, started_lsp_name) in - self.as_local().unwrap().language_server_ids.keys().cloned() - { - let language = languages.iter().find_map(|l| { - let adapter = self - .languages - .lsp_adapters(&l.name()) - .iter() - .find(|adapter| adapter.name == started_lsp_name)? - .clone(); - Some((l, adapter)) + this.on_settings_changed( + &mut get_adapter, + &mut |disposition, cx| { + let worktree = local + .worktree_store + .read(cx) + .worktree_for_id(disposition.path.worktree_id, cx) + .expect("Worktree ID to be valid"); + let delegate = + LocalLspAdapterDelegate::from_local_lsp(local, &worktree, cx); + let adapter = local + .languages + .adapter_for_name(disposition.server_name) + .expect("Adapter to be available"); + local.start_language_server( + &worktree, + delegate, + adapter, + disposition.settings, + cx, + ) + }, + &mut |id| to_stop.push(id), + cx, + ); }); - if let Some((language, adapter)) = language { - let worktree = self.worktree_for_id(worktree_id, cx).ok(); - let root_file = worktree.as_ref().and_then(|worktree| { - worktree - .update(cx, |tree, cx| tree.root_file(cx)) - .map(|f| f as _) - }); - let settings = language_settings(Some(language.name()), root_file.as_ref(), cx); - if !settings.enable_language_server { - language_servers_to_stop.push((worktree_id, started_lsp_name.clone())); - } else if let Some(worktree) = worktree { - let server_name = &adapter.name; - match ( - current_lsp_settings.get(server_name), - new_lsp_settings.get(server_name), - ) { - (None, None) => {} - (Some(_), None) | (None, Some(_)) => { - 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, language.name())); - } - } - } - } - } } - - for (worktree_id, adapter_name) in language_servers_to_stop { - self.stop_local_language_server(worktree_id, adapter_name, cx) - .detach(); + for id in to_stop { + self.stop_local_language_server(id, cx).detach(); } - if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { prettier_store.update(cx, |prettier_store, cx| { prettier_store.on_settings_changed(language_formatters_to_check, cx) }) } - // Start all the newly-enabled language servers. - for (worktree, language) in language_servers_to_start { - self.as_local_mut() - .unwrap() - .start_language_servers(&worktree, language, cx); - } - - // Restart all language servers with changed initialization options. - for (worktree, language) in language_servers_to_restart { - self.restart_local_language_servers(worktree, language, cx); - } - cx.notify(); } @@ -3742,12 +3732,10 @@ impl LspStore { .await }) } else if self.mode.is_local() { - let buffer = buffer_handle.read(cx); - let (lsp_adapter, lang_server) = if let Some((adapter, server)) = + let Some((lsp_adapter, lang_server)) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, action.server_id, cx) - { - (adapter.clone(), server.clone()) - } else { + .map(|(adapter, server)| (adapter.clone(), server.clone())) + }) else { return Task::ready(Ok(Default::default())); }; cx.spawn(move |this, mut cx| async move { @@ -3828,19 +3816,16 @@ impl LspStore { } }) } else { - let buffer = buffer_handle.read(cx); - let (_, lang_server) = if let Some((adapter, server)) = + let Some(lang_server) = buffer_handle.update(cx, |buffer, cx| { self.language_server_for_local_buffer(buffer, server_id, cx) - { - (adapter.clone(), server.clone()) - } else { + .map(|(_, server)| server.clone()) + }) else { return Task::ready(Ok(hint)); }; if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { return Task::ready(Ok(hint)); } - - let buffer_snapshot = buffer.snapshot(); + let buffer_snapshot = buffer_handle.read(cx).snapshot(); cx.spawn(move |_, mut cx| async move { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), @@ -3871,24 +3856,26 @@ impl LspStore { let snapshot = buffer.read(cx).snapshot(); let scope = snapshot.language_scope_at(position); let Some(server_id) = self - .as_local() - .and_then(|local| { - local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| { - server - .capabilities() - .linked_editing_range_provider - .is_some() - }) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) - .next() + .as_local() + .and_then(|local| { + buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(_, server)| { + server + .capabilities() + .linked_editing_range_provider + .is_some() + }) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) + .next() + }) }) .or_else(|| { self.upstream_client() @@ -4142,17 +4129,19 @@ impl LspStore { let scope = snapshot.language_scope_at(offset); let language = snapshot.language().cloned(); - let server_ids: Vec<_> = local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .collect(); + let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(_, server)| server.capabilities().completion_provider.is_some()) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect() + }); let buffer = buffer.clone(); cx.spawn(move |this, mut cx| async move { @@ -4472,10 +4461,9 @@ impl LspStore { push_to_history: bool, cx: &mut ModelContext, ) -> Task>> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - if let Some((client, project_id)) = self.upstream_client() { + let buffer = buffer_handle.read(cx); + let buffer_id = buffer.remote_id(); cx.spawn(move |_, mut cx| async move { let request = { let completion = completions.borrow()[completion_index].clone(); @@ -4514,9 +4502,14 @@ impl LspStore { }) } else { let server_id = completions.borrow()[completion_index].server_id; - let server = match self.language_server_for_local_buffer(buffer, server_id, cx) { - Some((_, server)) => server.clone(), - _ => return Task::ready(Ok(None)), + let Some(server) = buffer_handle.update(cx, |buffer, cx| { + Some( + self.language_server_for_local_buffer(buffer, server_id, cx)? + .1 + .clone(), + ) + }) else { + return Task::ready(Ok(None)); }; let snapshot = buffer_handle.read(&cx).snapshot(); @@ -4807,6 +4800,7 @@ impl LspStore { }) } else if let Some(local) = self.as_local() { struct WorkspaceSymbolsResult { + server_id: LanguageServerId, lsp_adapter: Arc, worktree: WeakModel, worktree_abs_path: Arc, @@ -4814,7 +4808,8 @@ impl LspStore { } let mut requests = Vec::new(); - for ((worktree_id, _), server_id) in local.language_server_ids.iter() { + let mut requested_servers = BTreeSet::new(); + 'next_server: for ((worktree_id, _), server_ids) in local.language_server_ids.iter() { let Some(worktree_handle) = self .worktree_store .read(cx) @@ -4826,55 +4821,63 @@ impl LspStore { if !worktree.is_visible() { continue; } - let worktree_abs_path = worktree.abs_path().clone(); - let (lsp_adapter, server) = match local.language_servers.get(server_id) { - Some(LanguageServerState::Running { - adapter, server, .. - }) => (adapter.clone(), server), - - _ => continue, - }; - - requests.push( - server - .request::( - lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - ) - .log_err() - .map(move |response| { - let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { - lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { - flat_responses.into_iter().map(|lsp_symbol| { + let mut servers_to_query = server_ids + .difference(&requested_servers) + .cloned() + .collect::>(); + for server_id in &servers_to_query { + let (lsp_adapter, server) = match local.language_servers.get(server_id) { + Some(LanguageServerState::Running { + adapter, server, .. + }) => (adapter.clone(), server), + + _ => continue 'next_server, + }; + let worktree_abs_path = worktree.abs_path().clone(); + let worktree_handle = worktree_handle.clone(); + let server_id = server.server_id(); + requests.push( + server + .request::( + lsp::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, + ) + .log_err() + .map(move |response| { + let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { + lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { + flat_responses.into_iter().map(|lsp_symbol| { (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) - }).collect::>() - } - lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { - nested_responses.into_iter().filter_map(|lsp_symbol| { - let location = match lsp_symbol.location { - OneOf::Left(location) => location, - OneOf::Right(_) => { - log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); - return None - } - }; - Some((lsp_symbol.name, lsp_symbol.kind, location)) - }).collect::>() + }).collect::>() + } + lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { + nested_responses.into_iter().filter_map(|lsp_symbol| { + let location = match lsp_symbol.location { + OneOf::Left(location) => location, + OneOf::Right(_) => { + log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); + return None + } + }; + Some((lsp_symbol.name, lsp_symbol.kind, location)) + }).collect::>() + } + }).unwrap_or_default(); + + WorkspaceSymbolsResult { + server_id, + lsp_adapter, + worktree: worktree_handle.downgrade(), + worktree_abs_path, + lsp_symbols, } - }).unwrap_or_default(); - - WorkspaceSymbolsResult { - lsp_adapter, - - worktree: worktree_handle.downgrade(), - worktree_abs_path, - lsp_symbols, - } - }), - ); + }), + ); + } + requested_servers.append(&mut servers_to_query); } cx.spawn(move |this, mut cx| async move { @@ -4914,6 +4917,7 @@ impl LspStore { }; let signature = this.symbol_signature(&project_path); Some(CoreSymbol { + source_language_server_id: result.server_id, language_server_name: result.lsp_adapter.name.clone(), source_worktree_id, path: project_path, @@ -4993,19 +4997,19 @@ impl LspStore { buffer: Model, cx: &mut ModelContext, ) -> Option<()> { + let language_servers: Vec<_> = buffer.update(cx, |buffer, cx| { + Some( + self.as_local()? + .language_servers_for_buffer(buffer, cx) + .map(|i| i.1.clone()) + .collect(), + ) + })?; let buffer = buffer.read(cx); let file = File::from_dyn(buffer.file())?; let abs_path = file.as_local()?.abs_path(cx); let uri = lsp::Url::from_file_path(abs_path).unwrap(); let next_snapshot = buffer.text_snapshot(); - - let language_servers: Vec<_> = self - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|i| i.1.clone()) - .collect(); - for language_server in language_servers { let language_server = language_server.clone(); @@ -5122,7 +5126,10 @@ impl LspStore { } } - for language_server_id in local.language_server_ids_for_buffer(buffer.read(cx), cx) { + let language_servers = buffer.update(cx, |buffer, cx| { + local.language_server_ids_for_buffer(buffer, cx) + }); + for language_server_id in language_servers { self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx); } @@ -5142,31 +5149,37 @@ impl LspStore { local .language_server_ids .iter() - .filter_map(|((worktree_id, _), server_id)| { + .flat_map(|((worktree_id, _), server_ids)| { let worktree = this .worktree_store .read(cx) - .worktree_for_id(*worktree_id, cx)?; - let state = local.language_servers.get(server_id)?; - let delegate = LocalLspAdapterDelegate::new( - local.languages.clone(), - &local.environment, - cx.weak_model(), - &worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ); - match state { - LanguageServerState::Starting(_) => None, - LanguageServerState::Running { - adapter, server, .. - } => Some(( - adapter.adapter.clone(), - server.clone(), - delegate as Arc, - )), - } + .worktree_for_id(*worktree_id, cx); + let delegate = worktree.map(|worktree| { + LocalLspAdapterDelegate::new( + local.languages.clone(), + &local.environment, + cx.weak_model(), + &worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + }); + + server_ids.iter().filter_map(move |server_id| { + let states = local.language_servers.get(server_id)?; + + match states { + LanguageServerState::Starting { .. } => None, + LanguageServerState::Running { + adapter, server, .. + } => Some(( + adapter.adapter.clone(), + server.clone(), + delegate.clone()? as Arc, + )), + } + }) }) .collect::>() }) @@ -5224,27 +5237,31 @@ impl LspStore { pub(crate) fn language_servers_for_local_buffer<'a>( &'a self, - buffer: &'a Buffer, - cx: &'a AppContext, + buffer: &Buffer, + cx: &mut AppContext, ) -> impl Iterator, &'a Arc)> { - self.as_local().into_iter().flat_map(|local| { - local - .language_server_ids_for_buffer(buffer, cx) - .into_iter() - .filter_map(|server_id| match local.language_servers.get(&server_id)? { + let local = self.as_local(); + let language_server_ids = local + .map(|local| local.language_server_ids_for_buffer(buffer, cx)) + .unwrap_or_default(); + + language_server_ids + .into_iter() + .filter_map( + move |server_id| match local?.language_servers.get(&server_id)? { LanguageServerState::Running { adapter, server, .. } => Some((adapter, server)), _ => None, - }) - }) + }, + ) } pub fn language_server_for_local_buffer<'a>( &'a self, buffer: &'a Buffer, server_id: LanguageServerId, - cx: &'a AppContext, + cx: &'a mut AppContext, ) -> Option<(&'a Arc, &'a Arc)> { self.as_local()? .language_servers_for_buffer(buffer, cx) @@ -5253,40 +5270,12 @@ impl LspStore { fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext) { self.diagnostic_summaries.remove(&id_to_remove); - let to_remove = Vec::new(); if let Some(local) = self.as_local_mut() { - local.diagnostics.remove(&id_to_remove); - local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.remove_worktree(id_to_remove, cx); - }); - - let mut servers_to_remove = HashMap::default(); - let mut servers_to_preserve = HashSet::default(); - for ((worktree_id, server_name), &server_id) in &local.language_server_ids { - if worktree_id == &id_to_remove { - servers_to_remove.insert(server_id, server_name.clone()); - } else { - servers_to_preserve.insert(server_id); - } - } - servers_to_remove.retain(|server_id, _| !servers_to_preserve.contains(server_id)); - for (server_id_to_remove, server_name) in servers_to_remove { - local - .language_server_ids - .remove(&(id_to_remove, server_name)); - local - .language_server_watched_paths - .remove(&server_id_to_remove); - local - .last_workspace_edits_by_language_server - .remove(&server_id_to_remove); - local.language_servers.remove(&server_id_to_remove); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id_to_remove)); + let to_remove = local.remove_worktree(id_to_remove, cx); + for server in to_remove { + self.language_server_statuses.remove(&server); } } - for server in to_remove { - self.language_server_statuses.remove(&server); - } } pub fn shared( @@ -5353,7 +5342,9 @@ impl LspStore { self.as_local_mut() .unwrap() .language_server_ids - .insert((worktree_id, language_server_name), language_server_id); + .entry((worktree_id, language_server_name)) + .or_default() + .insert(language_server_id); } pub fn update_diagnostic_entries( @@ -5489,10 +5480,17 @@ impl LspStore { .await }) } else if let Some(local) = self.as_local() { - let Some(&language_server_id) = local.language_server_ids.get(&( - symbol.source_worktree_id, - symbol.language_server_name.clone(), - )) else { + let Some(language_server_id) = local + .language_server_ids + .get(&( + symbol.source_worktree_id, + symbol.language_server_name.clone(), + )) + .and_then(|ids| { + ids.contains(&symbol.source_language_server_id) + .then_some(symbol.source_language_server_id) + }) + else { return Task::ready(Err(anyhow!( "language server for worktree and language not found" ))); @@ -5633,16 +5631,19 @@ impl LspStore { let snapshot = buffer.read(cx).snapshot(); let scope = position.and_then(|position| snapshot.language_scope_at(position)); - let server_ids = local - .language_servers_for_buffer(buffer.read(cx), cx) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .collect::>(); + + let server_ids = buffer.update(cx, |buffer, cx| { + local + .language_servers_for_buffer(buffer, cx) + .filter(|(adapter, _)| { + scope + .as_ref() + .map(|scope| scope.language_allowed(&adapter.name)) + .unwrap_or(true) + }) + .map(|(_, server)| server.server_id()) + .collect::>() + }); let mut response_results = server_ids .into_iter() @@ -6386,18 +6387,13 @@ impl LspStore { } pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - 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 - } + let 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 } @@ -6794,6 +6790,7 @@ impl LspStore { &Symbol { language_server_name: symbol.language_server_name, source_worktree_id: symbol.source_worktree_id, + source_language_server_id: symbol.source_language_server_id, path: symbol.path, name: symbol.name, kind: symbol.kind, @@ -7119,14 +7116,14 @@ impl LspStore { cx: AsyncAppContext, ) { let server = match server_state { - Some(LanguageServerState::Starting(task)) => { + Some(LanguageServerState::Starting { startup, .. }) => { let mut timer = cx .background_executor() .timer(SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT) .fuse(); select! { - server = task.fuse() => server, + server = startup.fuse() => server, _ = timer => { log::info!( "timeout waiting for language server {} to finish launching before stopping", @@ -7153,36 +7150,43 @@ impl LspStore { // for the stopped server fn stop_local_language_server( &mut self, - worktree_id: WorktreeId, - adapter_name: LanguageServerName, + server_id: LanguageServerId, cx: &mut ModelContext, ) -> Task> { - let key = (worktree_id, adapter_name); let local = match &mut self.mode { LspStoreMode::Local(local) => local, _ => { return Task::ready(Vec::new()); } }; - let Some(server_id) = local.language_server_ids.remove(&key) else { + + let mut orphaned_worktrees = vec![]; + // Did any of the worktrees reference this server ID at least once? + let mut was_referenced = false; + // Remove this server ID from all entries in the given worktree. + local.language_server_ids.retain(|(worktree, _), ids| { + if !ids.remove(&server_id) { + return true; + } + + was_referenced = true; + if ids.is_empty() { + orphaned_worktrees.push(*worktree); + false + } else { + true + } + }); + let Some(status) = self + .language_server_statuses + .remove(&server_id) + .filter(|_| was_referenced) + else { return Task::ready(Vec::new()); }; - let name = key.1; - 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 = local - .language_server_ids - .keys() - .cloned() - .collect::>(); - for other_key in other_keys { - if local.language_server_ids.get(&other_key) == Some(&server_id) { - local.language_server_ids.remove(&other_key); - orphaned_worktrees.push(other_key.0); - } - } + let name = LanguageServerName(status.name.into()); + log::info!("stopping language server {name}"); self.buffer_store.update(cx, |buffer_store, cx| { for buffer in buffer_store.buffers() { @@ -7217,7 +7221,6 @@ impl LspStore { }); } - self.language_server_statuses.remove(&server_id); let local = self.as_local_mut().unwrap(); for diagnostics in local.diagnostics.values_mut() { diagnostics.retain(|_, diagnostics_by_server_id| { @@ -7256,81 +7259,89 @@ impl LspStore { .spawn(request) .detach_and_log_err(cx); } else { - let language_server_lookup_info: HashSet<(Model, LanguageName)> = buffers + let Some(local) = self.as_local_mut() else { + return; + }; + let language_servers_for_worktrees = 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)?; - - Some((worktree, language.name())) + buffer.update(cx, |buffer, cx| { + let worktree_id = buffer.file()?.worktree_id(cx); + let language_server_ids = local.language_servers_for_buffer(buffer, cx); + Some(( + language_server_ids + .filter_map(|(adapter, server)| { + let new_adapter = + local.languages.adapter_for_name(&adapter.name()); + new_adapter.map(|adapter| (server.server_id(), adapter)) + }) + .collect::>(), + worktree_id, + )) + }) }) - .collect(); - - for (worktree, language) in language_server_lookup_info { - self.restart_local_language_servers(worktree, language, cx); - } - } - } - - fn restart_local_language_servers( - &mut self, - worktree: Model, - language: LanguageName, - cx: &mut ModelContext, - ) { - let worktree_id = worktree.read(cx).id(); - - let stop_tasks = self - .languages - .clone() - .lsp_adapters(&language) - .iter() - .map(|adapter| { - let stop_task = - self.stop_local_language_server(worktree_id, adapter.name.clone(), cx); - (stop_task, adapter.name.clone()) - }) - .collect::>(); - if stop_tasks.is_empty() { - return; - } + .fold( + HashMap::default(), + |mut worktree_to_ids, (server_ids, worktree_id)| { + worktree_to_ids + .entry(worktree_id) + .or_insert_with(BTreeMap::new) + .extend(server_ids); + worktree_to_ids + }, + ); - cx.spawn(move |this, mut cx| async move { - // For each stopped language server, record all of the worktrees with which - // it was associated. - let mut affected_worktrees = Vec::new(); - for (stop_task, language_server_name) in stop_tasks { - for affected_worktree_id in stop_task.await { - affected_worktrees.push((affected_worktree_id, language_server_name.clone())); + // Multiple worktrees might refer to the same language server; + // we don't want to restart them multiple times. + let mut restarted_language_servers = BTreeMap::new(); + let mut servers_to_stop = BTreeSet::new(); + local.lsp_tree.clone().update(cx, |tree, cx| { + for (worktree_id, adapters_to_restart) in language_servers_for_worktrees { + let Some(worktree_handle) = local + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + continue; + }; + let delegate = + LocalLspAdapterDelegate::from_local_lsp(local, &worktree_handle, cx); + let servers_to_restart = adapters_to_restart.keys().copied().collect(); + tree.restart_language_servers( + worktree_id, + servers_to_restart, + &mut |old_server_id, disposition| match restarted_language_servers + .entry(old_server_id) + { + btree_map::Entry::Vacant(unfilled) => { + servers_to_stop.insert(old_server_id); + + let adapter = adapters_to_restart + .get(&old_server_id) + .map(Clone::clone) + .expect("Language server adapter to be found"); + let new_id = local.start_language_server( + &worktree_handle, + delegate.clone(), + adapter, + disposition.settings, + cx, + ); + unfilled.insert(new_id); + new_id + } + btree_map::Entry::Occupied(server_id) => *server_id.get(), + }, + ); } + }); + for server_id in servers_to_stop { + self.stop_local_language_server(server_id, cx).detach(); } - - this.update(&mut cx, |this, cx| { - let local = this.as_local_mut().unwrap(); - // Restart the language server for the given worktree. - // - local.start_language_servers(&worktree, language.clone(), cx); - - // Lookup new server ids and set them for each of the orphaned worktrees - for (affected_worktree_id, language_server_name) in affected_worktrees { - if let Some(new_server_id) = local - .language_server_ids - .get(&(worktree_id, language_server_name.clone())) - .cloned() - { - local - .language_server_ids - .insert((affected_worktree_id, language_server_name), new_server_id); - } - } - }) - .ok(); - }) - .detach(); + // for (worktree, language) in language_server_lookup_info { + // self.restart_local_language_servers(worktree, language, cx); + // } + } } pub fn update_diagnostics( @@ -7458,12 +7469,14 @@ impl LspStore { Ok(()) } + #[allow(clippy::too_many_arguments)] fn insert_newly_running_language_server( &mut self, adapter: Arc, language_server: Arc, server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), + workspace_folders: Arc>>, cx: &mut ModelContext, ) { let Some(local) = self.as_local_mut() else { @@ -7474,7 +7487,7 @@ impl LspStore { if local .language_server_ids .get(&key) - .map(|id| id != &server_id) + .map(|ids| !ids.contains(&server_id)) .unwrap_or(false) { return; @@ -7484,11 +7497,12 @@ impl LspStore { // indicating that the server is up and running and ready local.language_servers.insert( server_id, - LanguageServerState::Running { - adapter: adapter.clone(), - server: language_server.clone(), - simulate_disk_based_diagnostics_completion: None, - }, + LanguageServerState::running( + workspace_folders.lock().clone(), + adapter.clone(), + language_server.clone(), + None, + ), ); if let Some(file_ops_caps) = language_server .capabilities() @@ -7568,36 +7582,29 @@ impl LspStore { let local = self.as_local_mut().unwrap(); - if local.registered_buffers.contains_key(&buffer.remote_id()) { - let versions = local - .buffer_snapshots - .entry(buffer.remote_id()) - .or_default() - .entry(server_id) - .or_insert_with(|| { - vec![LspBufferSnapshot { - version: 0, - snapshot: buffer.text_snapshot(), - }] - }); + let versions = local + .buffer_snapshots + .entry(buffer.remote_id()) + .or_default() + .entry(server_id) + .or_insert_with(|| { + vec![LspBufferSnapshot { + version: 0, + snapshot: buffer.text_snapshot(), + }] + }); - let snapshot = versions.last().unwrap(); - let version = snapshot.version; - let initial_snapshot = &snapshot.snapshot; - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - language_server - .notify::( - &lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - adapter.language_id(&language.name()), - version, - initial_snapshot.text(), - ), - }, - ) - .log_err(); - } + let snapshot = versions.last().unwrap(); + let version = snapshot.version; + let initial_snapshot = &snapshot.snapshot; + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + language_server.register_buffer( + buffer.remote_id(), + uri, + adapter.language_id(&language.name()), + version, + initial_snapshot.text(), + ); buffer_handle.update(cx, |buffer, cx| { buffer.set_completion_triggers( @@ -7660,12 +7667,11 @@ impl LspStore { let servers = buffers .into_iter() .flat_map(|buffer| { - local - .language_server_ids_for_buffer(buffer.read(cx), cx) - .into_iter() + buffer.update(cx, |buffer, cx| { + local.language_server_ids_for_buffer(buffer, cx).into_iter() + }) }) .collect::>(); - for server_id in servers { self.cancel_language_server_work(server_id, None, cx); } @@ -7698,16 +7704,6 @@ impl LspStore { ) .ok(); } - - if progress.is_cancellable { - server - .notify::( - &WorkDoneProgressCancelParams { - token: lsp::NumberOrString::String(token.clone()), - }, - ) - .ok(); - } } } } else if let Some((client, project_id)) = self.upstream_client() { @@ -7754,7 +7750,7 @@ impl LspStore { } } - pub fn supplementary_language_servers( + pub(crate) fn supplementary_language_servers( &self, ) -> impl '_ + Iterator { self.as_local().into_iter().flat_map(|local| { @@ -7797,8 +7793,10 @@ impl LspStore { let mut language_server_ids = local .language_server_ids .iter() - .filter_map(|((server_worktree_id, _), server_id)| { - (*server_worktree_id == worktree_id).then_some(*server_id) + .flat_map(|((server_worktree, _), server_ids)| { + server_ids + .iter() + .filter_map(|server_id| server_worktree.eq(&worktree_id).then(|| *server_id)) }) .collect::>(); language_server_ids.sort(); @@ -7859,6 +7857,7 @@ impl LspStore { proto::Symbol { language_server_name: symbol.language_server_name.0.to_string(), source_worktree_id: symbol.source_worktree_id.to_proto(), + language_server_id: symbol.source_language_server_id.to_proto(), worktree_id: symbol.path.worktree_id.to_proto(), path: symbol.path.path.to_string_lossy().to_string(), name: symbol.name.clone(), @@ -7893,6 +7892,9 @@ impl LspStore { Ok(CoreSymbol { language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), source_worktree_id, + source_language_server_id: LanguageServerId::from_proto( + serialized_symbol.language_server_id, + ), path, name: serialized_symbol.name, range: Unclipped(PointUtf16::new(start.row, start.column)) @@ -8289,7 +8291,11 @@ impl LanguageServerLogType { } pub enum LanguageServerState { - Starting(Task>>), + Starting { + startup: Task>>, + /// List of language servers that will be added to the workspace once it's initialization completes. + pending_workspace_folders: Arc>>, + }, Running { adapter: Arc, @@ -8298,10 +8304,50 @@ pub enum LanguageServerState { }, } +impl LanguageServerState { + fn add_workspace_folder(&self, uri: Url) { + match self { + LanguageServerState::Starting { + pending_workspace_folders, + .. + } => { + pending_workspace_folders.lock().insert(uri); + } + LanguageServerState::Running { server, .. } => { + server.add_workspace_folder(uri); + } + } + } + fn _remove_workspace_folder(&self, uri: Url) { + match self { + LanguageServerState::Starting { + pending_workspace_folders, + .. + } => { + pending_workspace_folders.lock().remove(&uri); + } + LanguageServerState::Running { server, .. } => server.remove_workspace_folder(uri), + } + } + fn running( + workspace_folders: BTreeSet, + adapter: Arc, + server: Arc, + simulate_disk_based_diagnostics_completion: Option>, + ) -> Self { + server.set_workspace_folders(workspace_folders); + Self::Running { + adapter, + server, + simulate_disk_based_diagnostics_completion, + } + } +} + impl std::fmt::Debug for LanguageServerState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - LanguageServerState::Starting(_) => { + LanguageServerState::Starting { .. } => { f.debug_struct("LanguageServerState::Starting").finish() } LanguageServerState::Running { .. } => { @@ -8455,20 +8501,27 @@ impl LspAdapter for SshLspAdapter { } } -pub fn language_server_settings<'a, 'b: 'a>( +pub fn language_server_settings<'a>( delegate: &'a dyn LspAdapterDelegate, language: &LanguageServerName, - cx: &'b AppContext, + cx: &'a AppContext, ) -> Option<&'a LspSettings> { - ProjectSettings::get( - Some(SettingsLocation { + language_server_settings_for( + SettingsLocation { worktree_id: delegate.worktree_id(), path: delegate.worktree_root_path(), - }), + }, + language, cx, ) - .lsp - .get(language) +} + +pub(crate) fn language_server_settings_for<'a>( + location: SettingsLocation<'a>, + language: &LanguageServerName, + cx: &'a AppContext, +) -> Option<&'a LspSettings> { + ProjectSettings::get(Some(location), cx).lsp.get(language) } pub struct LocalLspAdapterDelegate { @@ -8488,10 +8541,13 @@ impl LocalLspAdapterDelegate { worktree: &Model, http_client: Arc, fs: Arc, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Arc { - let worktree_id = worktree.read(cx).id(); - let worktree_abs_path = worktree.read(cx).abs_path(); + let (worktree_id, worktree_abs_path) = { + let worktree = worktree.read(cx); + (worktree.id(), worktree.abs_path()) + }; + let load_shell_env_task = environment.update(cx, |env, cx| { env.get_environment(Some(worktree_id), Some(worktree_abs_path), cx) }); @@ -8505,6 +8561,22 @@ impl LocalLspAdapterDelegate { load_shell_env_task, }) } + + fn from_local_lsp( + local: &LocalLspStore, + worktree: &Model, + cx: &mut AppContext, + ) -> Arc { + Self::new( + local.languages.clone(), + &local.environment, + local.weak.clone(), + worktree, + local.http_client.clone(), + local.fs.clone(), + cx, + ) + } } #[async_trait] @@ -8709,6 +8781,7 @@ async fn populate_labels_for_symbols( output.push(Symbol { language_server_name: symbol.language_server_name, source_worktree_id: symbol.source_worktree_id, + source_language_server_id: symbol.source_language_server_id, path: symbol.path, label: label.unwrap_or_else(|| CodeLabel::plain(name.clone(), None)), name, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index e707f9e9bc2d2..36da95422def8 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -40,7 +40,7 @@ pub struct PrettierStore { prettier_instances: HashMap, } -pub enum PrettierStoreEvent { +pub(crate) enum PrettierStoreEvent { LanguageServerRemoved(LanguageServerId), LanguageServerAdded { new_server_id: LanguageServerId, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 78965f64b58ae..d4f78757d80da 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -9,6 +9,7 @@ pub mod lsp_ext_command; pub mod lsp_store; pub mod prettier_store; pub mod project_settings; +mod project_tree; pub mod search; mod task_inventory; pub mod task_store; @@ -474,6 +475,7 @@ pub struct DocumentHighlight { pub struct Symbol { pub language_server_name: LanguageServerName, pub source_worktree_id: WorktreeId, + pub source_language_server_id: LanguageServerId, pub path: ProjectPath, pub label: CodeLabel, pub name: String, @@ -1890,7 +1892,7 @@ impl Project { pub fn open_buffer( &mut self, path: impl Into, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Task>> { if self.is_disconnected(cx) { return Task::ready(Err(anyhow!(ErrorCode::Disconnected))); @@ -1905,11 +1907,11 @@ impl Project { pub fn open_buffer_with_lsp( &mut self, path: impl Into, - cx: &mut ModelContext, + cx: &mut AppContext, ) -> Task, lsp_store::OpenLspBufferHandle)>> { let buffer = self.open_buffer(path, cx); let lsp_store = self.lsp_store().clone(); - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { let buffer = buffer.await?; let handle = lsp_store.update(&mut cx, |lsp_store, cx| { lsp_store.register_buffer_with_language_servers(&buffer, cx) @@ -4145,14 +4147,25 @@ impl Project { self.lsp_store.read(cx).supplementary_language_servers() } - pub fn language_servers_for_local_buffer<'a>( - &'a self, - buffer: &'a Buffer, - cx: &'a AppContext, - ) -> impl Iterator, &'a Arc)> { - self.lsp_store - .read(cx) - .language_servers_for_local_buffer(buffer, cx) + pub fn language_server_for_id( + &self, + id: LanguageServerId, + cx: &AppContext, + ) -> Option> { + self.lsp_store.read(cx).language_server_for_id(id) + } + + pub fn for_language_servers_for_local_buffer( + &self, + buffer: &Buffer, + callback: impl FnOnce( + Box, &Arc)> + '_>, + ) -> R, + cx: &mut AppContext, + ) -> R { + self.lsp_store.update(cx, |this, cx| { + callback(Box::new(this.language_servers_for_local_buffer(buffer, cx))) + }) } pub fn buffer_store(&self) -> &Model { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index fe0bf8e0fa629..1ed99b50cfce7 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1749,6 +1749,12 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { }); }) }); + let _rs_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp("/dir/a.rs", cx) + }) + .await + .unwrap(); let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); assert_eq!( fake_rust_server_2 @@ -2573,25 +2579,28 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { fs.insert_tree( "/dir", json!({ - "a.rs": "const fn a() { A }", "b.rs": "const y: i32 = crate::a()", }), ) .await; + fs.insert_tree( + "/another_dir", + json!({ + "a.rs": "const fn a() { A }"}), + ) + .await; - let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await; + let project = Project::test(fs, ["/dir".as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default()); - let (buffer, _handle) = project .update(cx, |project, cx| { project.open_local_buffer_with_lsp("/dir/b.rs", cx) }) .await .unwrap(); - let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::(|params, _| async move { let params = params.text_document_position_params; @@ -2603,12 +2612,11 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { Ok(Some(lsp::GotoDefinitionResponse::Scalar( lsp::Location::new( - lsp::Url::from_file_path("/dir/a.rs").unwrap(), + lsp::Url::from_file_path("/another_dir/a.rs").unwrap(), lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), ), ))) }); - let mut definitions = project .update(cx, |project, cx| project.definition(&buffer, 22, cx)) .await @@ -2629,18 +2637,21 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { .as_local() .unwrap() .abs_path(cx), - Path::new("/dir/a.rs"), + Path::new("/another_dir/a.rs"), ); assert_eq!(definition.target.range.to_offset(target_buffer), 9..10); assert_eq!( list_worktrees(&project, cx), - [("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)], + [ + ("/another_dir/a.rs".as_ref(), false), + ("/dir".as_ref(), true) + ], ); drop(definition); }); cx.update(|cx| { - assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]); + assert_eq!(list_worktrees(&project, cx), [("/dir".as_ref(), true)]); }); fn list_worktrees<'a>( diff --git a/crates/project/src/project_tree.rs b/crates/project/src/project_tree.rs new file mode 100644 index 0000000000000..2a172711f45b7 --- /dev/null +++ b/crates/project/src/project_tree.rs @@ -0,0 +1,243 @@ +//! This module defines a Project Tree. +//! +//! A Project Tree is responsible for determining where the roots of subprojects are located in a project. + +mod path_trie; +mod server_tree; + +use std::{ + borrow::Borrow, + collections::{hash_map::Entry, BTreeMap}, + ops::ControlFlow, + sync::Arc, +}; + +use collections::HashMap; +use gpui::{AppContext, Context as _, EventEmitter, Model, ModelContext, Subscription}; +use language::{CachedLspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerName; +use path_trie::{LabelPresence, RootPathTrie, TriePath}; +use settings::{SettingsStore, WorktreeId}; +use worktree::{Event as WorktreeEvent, Worktree}; + +use crate::{ + worktree_store::{WorktreeStore, WorktreeStoreEvent}, + ProjectPath, +}; + +pub(crate) use server_tree::{LanguageServerTree, LaunchDisposition}; + +struct WorktreeRoots { + roots: RootPathTrie, + worktree_store: Model, + _worktree_subscription: Subscription, +} + +impl WorktreeRoots { + fn new( + worktree_store: Model, + worktree: Model, + cx: &mut AppContext, + ) -> Model { + cx.new_model(|cx| Self { + roots: RootPathTrie::new(), + worktree_store, + _worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| { + match event { + WorktreeEvent::UpdatedEntries(changes) => { + for (path, _, kind) in changes.iter() { + match kind { + worktree::PathChange::Removed => { + let path = TriePath::from(path.as_ref()); + this.roots.remove(&path); + } + _ => {} + } + } + } + WorktreeEvent::UpdatedGitRepositories(_) => {} + WorktreeEvent::DeletedEntry(entry_id) => { + let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx) + else { + return; + }; + let path = TriePath::from(entry.path.as_ref()); + this.roots.remove(&path); + } + } + }), + }) + } +} + +pub struct ProjectTree { + root_points: HashMap>, + worktree_store: Model, + _subscriptions: [Subscription; 2], +} + +#[derive(Debug, Clone)] +struct AdapterWrapper(Arc); +impl PartialEq for AdapterWrapper { + fn eq(&self, other: &Self) -> bool { + self.0.name.eq(&other.0.name) + } +} + +impl Eq for AdapterWrapper {} + +impl std::hash::Hash for AdapterWrapper { + fn hash(&self, state: &mut H) { + self.0.name.hash(state); + } +} + +impl PartialOrd for AdapterWrapper { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.0.name.cmp(&other.0.name)) + } +} + +impl Ord for AdapterWrapper { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.name.cmp(&other.0.name) + } +} + +impl Borrow for AdapterWrapper { + fn borrow(&self) -> &LanguageServerName { + &self.0.name + } +} + +#[derive(PartialEq)] +pub(crate) enum ProjectTreeEvent { + WorktreeRemoved(WorktreeId), + Cleared, +} + +impl EventEmitter for ProjectTree {} + +impl ProjectTree { + pub(crate) fn new(worktree_store: Model, cx: &mut AppContext) -> Model { + cx.new_model(|cx| Self { + root_points: Default::default(), + _subscriptions: [ + cx.subscribe(&worktree_store, Self::on_worktree_store_event), + cx.observe_global::(|this, cx| { + for (_, roots) in &mut this.root_points { + roots.update(cx, |worktree_roots, _| { + worktree_roots.roots = RootPathTrie::new(); + }) + } + cx.emit(ProjectTreeEvent::Cleared); + }), + ], + worktree_store, + }) + } + #[allow(clippy::mutable_key_type)] + fn root_for_path( + &mut self, + ProjectPath { worktree_id, path }: ProjectPath, + adapters: Vec>, + delegate: Arc, + cx: &mut AppContext, + ) -> BTreeMap { + debug_assert_eq!(delegate.worktree_id(), worktree_id); + #[allow(clippy::mutable_key_type)] + let mut roots = BTreeMap::from_iter( + adapters + .into_iter() + .map(|adapter| (AdapterWrapper(adapter), (None, LabelPresence::KnownAbsent))), + ); + let worktree_roots = match self.root_points.entry(worktree_id) { + Entry::Occupied(occupied_entry) => occupied_entry.get().clone(), + Entry::Vacant(vacant_entry) => { + let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + else { + return Default::default(); + }; + let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx); + vacant_entry.insert(roots).clone() + } + }; + + let key = TriePath::from(&*path); + worktree_roots.update(cx, |this, _| { + this.roots.walk(&key, &mut |path, labels| { + for (label, presence) in labels { + if let Some((marked_path, current_presence)) = roots.get_mut(label) { + if *current_presence > *presence { + debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase"); + } + *marked_path = Some(ProjectPath {worktree_id, path: path.clone()}); + *current_presence = *presence; + } + + } + ControlFlow::Continue(()) + }); + }); + for (adapter, (root_path, presence)) in &mut roots { + if *presence == LabelPresence::Present { + continue; + } + + let depth = root_path + .as_ref() + .map(|root_path| { + path.strip_prefix(&root_path.path) + .unwrap() + .components() + .count() + }) + .unwrap_or_else(|| path.components().count() + 1); + + if depth > 0 { + let root = adapter.0.find_project_root(&path, depth, &delegate); + match root { + Some(known_root) => worktree_roots.update(cx, |this, _| { + let root = TriePath::from(&*known_root); + this.roots + .insert(&root, adapter.0.name(), LabelPresence::Present); + *presence = LabelPresence::Present; + *root_path = Some(ProjectPath { + worktree_id, + path: known_root, + }); + }), + None => worktree_roots.update(cx, |this, _| { + this.roots + .insert(&key, adapter.0.name(), LabelPresence::KnownAbsent); + }), + } + } + } + + roots + .into_iter() + .filter_map(|(k, (path, presence))| { + let path = path?; + presence.eq(&LabelPresence::Present).then(|| (k, path)) + }) + .collect() + } + fn on_worktree_store_event( + &mut self, + _: Model, + evt: &WorktreeStoreEvent, + cx: &mut ModelContext, + ) { + match evt { + WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) => { + self.root_points.remove(&worktree_id); + cx.emit(ProjectTreeEvent::WorktreeRemoved(*worktree_id)); + } + _ => {} + } + } +} diff --git a/crates/project/src/project_tree/path_trie.rs b/crates/project/src/project_tree/path_trie.rs new file mode 100644 index 0000000000000..b2e2d8224906d --- /dev/null +++ b/crates/project/src/project_tree/path_trie.rs @@ -0,0 +1,241 @@ +use std::{ + collections::{btree_map::Entry, BTreeMap}, + ffi::OsStr, + ops::ControlFlow, + path::{Path, PathBuf}, + sync::Arc, +}; + +/// [RootPathTrie] is a workhorse of [super::ProjectTree]. It is responsible for determining the closest known project root for a given path. +/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed. +/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches. +/// +/// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path. +/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is +/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories. +pub(super) struct RootPathTrie