From ca34ead6d9d31db254dd7251ca38dae935173896 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 29 Jul 2025 00:02:12 +0200 Subject: [PATCH 1/8] onboarding: Add proper icon for action (#35241) This change updates one icon within the onboarding flow to the indended icon for that entry. Release Notes: - N/A --- assets/icons/cloud_download.svg | 1 + crates/icons/src/icons.rs | 1 + crates/onboarding/src/welcome.rs | 3 +-- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 assets/icons/cloud_download.svg diff --git a/assets/icons/cloud_download.svg b/assets/icons/cloud_download.svg new file mode 100644 index 0000000000..bc7a8376d1 --- /dev/null +++ b/assets/icons/cloud_download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index e7066ae151..7552060be4 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -71,6 +71,7 @@ pub enum IconName { CircleHelp, Close, Cloud, + CloudDownload, Code, Cog, Command, diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 2ed44cf2ce..2ea120e021 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -32,8 +32,7 @@ const CONTENT: (Section<4>, Section<3>) = ( action: &Open, }, SectionEntry { - // TODO: use proper icon - icon: IconName::Download, + icon: IconName::CloudDownload, title: "Clone a Repo", // TODO: use proper action action: &NoAction, From 11c7b498b363340965f0c32e8beac16903d56c86 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 28 Jul 2025 17:18:20 -0500 Subject: [PATCH 2/8] Fix panic feature flag detection (#35245) The flag was being checked before feature flags were resolved. Release Notes: - N/A --- crates/zed/src/zed.rs | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0a90f89fa4..c72fe39d2d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -126,17 +126,28 @@ pub fn init(cx: &mut App) { cx.on_action(quit); cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx)); - if ReleaseChannel::global(cx) == ReleaseChannel::Dev || cx.has_flag::() { - cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); - cx.on_action(|_: &TestCrash, _| { - unsafe extern "C" { - fn puts(s: *const i8); - } - unsafe { - puts(0xabad1d3a as *const i8); - } - }); - } + let flag = cx.wait_for_flag::(); + cx.spawn(async |cx| { + if cx + .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev) + .unwrap_or_default() + || flag.await + { + cx.update(|cx| { + cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); + cx.on_action(|_: &TestCrash, _| { + unsafe extern "C" { + fn puts(s: *const i8); + } + unsafe { + puts(0xabad1d3a as *const i8); + } + }); + }) + .ok(); + }; + }) + .detach(); cx.on_action(|_: &OpenLog, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_log_file(workspace, window, cx); From 798aa50df8e8a31791cd9e9461bd188ffe9ff1df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Jul 2025 01:37:48 +0300 Subject: [PATCH 3/8] Fix tasks leaked despite workspace window close (#35246) Closes https://github.com/zed-industries/zed/issues/34932 Release Notes: - Fixed tasks leaked despite workspace window close --- crates/editor/src/editor.rs | 5 ++--- crates/workspace/src/tasks.rs | 8 ++++---- crates/workspace/src/workspace.rs | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8f57fb1a20..6bbd1a409d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1774,7 +1774,7 @@ impl Editor { ) -> Self { debug_assert!( display_map.is_none() || mode.is_minimap(), - "Providing a display map for a new editor is only intended for the minimap and might have unindended side effects otherwise!" + "Providing a display map for a new editor is only intended for the minimap and might have unintended side effects otherwise!" ); let full_mode = mode.is_full(); @@ -8235,8 +8235,7 @@ impl Editor { return; }; - // Try to find a closest, enclosing node using tree-sitter that has a - // task + // Try to find a closest, enclosing node using tree-sitter that has a task let Some((buffer, buffer_row, tasks)) = self .find_enclosing_node_task(cx) // Or find the task that's closest in row-distance. diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 26edbd8d03..32d066c7eb 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -73,7 +73,7 @@ impl Workspace { if let Some(terminal_provider) = self.terminal_provider.as_ref() { let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx); - cx.background_spawn(async move { + let task = cx.background_spawn(async move { match task_status.await { Some(Ok(status)) => { if status.success() { @@ -82,11 +82,11 @@ impl Workspace { log::debug!("Task spawn failed, code: {:?}", status.code()); } } - Some(Err(e)) => log::error!("Task spawn failed: {e}"), + Some(Err(e)) => log::error!("Task spawn failed: {e:#}"), None => log::debug!("Task spawn got cancelled"), } - }) - .detach(); + }); + self.scheduled_tasks.push(task); } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0ee8177dd8..77d76b44f5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1104,6 +1104,7 @@ pub struct Workspace { serialized_ssh_project: Option, _items_serializer: Task>, session_id: Option, + scheduled_tasks: Vec>, } impl EventEmitter for Workspace {} @@ -1435,6 +1436,7 @@ impl Workspace { _items_serializer, session_id: Some(session_id), serialized_ssh_project: None, + scheduled_tasks: Vec::new(), } } From 109eddafd0eddccaffbdcd0c47badabd5c8b4bff Mon Sep 17 00:00:00 2001 From: Tom Monaghan <62273348+t-monaghan@users.noreply.github.com> Date: Tue, 29 Jul 2025 08:59:46 +1000 Subject: [PATCH 4/8] docs: Fix link in configuration documentation (#35249) # Summary The link "under the configuration page" [on this page](https://zed.dev/docs/configuring-zed#agent) is broken. It should be linking to [this page](https://zed.dev/docs/ai/configuration). ## Approach I noted that all other links in this document begin with "./" where the ai configuration link does not, I also noticed [this PR](https://github.com/zed-industries/zed/pull/31119) fixing a link with the same approach. I don't fully understand why this is the fix. ## Previous Approaches I have tried writing the following redirect in `docs/book.toml`: `"/ai/configuration.html" = "/docs/ai/configuration.html"`. However this broke the `mdbook` build with the below error. ``` 2025-07-29 08:49:36 [ERROR] (mdbook::utils): Caused By: Not redirecting "/Users/tmonaghan/dev/zed/docs/book/ai/configuration.html" to "/docs/ai/configuration.html" because it already exists. Are you sure it needs to be redirected? ``` Release Notes: - N/A --- docs/src/configuring-zed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index fd1761ebfa..556bad22b4 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3390,7 +3390,7 @@ Run the `theme selector: toggle` action in the command palette to see a current ## Agent -Visit [the Configuration page](/ai/configuration.md) under the AI section to learn more about all the agent-related settings. +Visit [the Configuration page](./ai/configuration.md) under the AI section to learn more about all the agent-related settings. ## Outline Panel From d2ef2877918fa6d78592cafb1c6c7d0075928af4 Mon Sep 17 00:00:00 2001 From: Bedis Nbiba Date: Tue, 29 Jul 2025 00:45:41 +0100 Subject: [PATCH 5/8] Add runnable support for Deno.test (#34593) example of detected code: ```ts Deno.test("t", () => { console.log("Hello, World!"); }); Deno.test(function azaz() { console.log("Hello, World!"); }); ``` I can't build zed locally so I didn't test this, but I think the code is straightforward enough, hopefully someone else can verify it Closes #ISSUE Release Notes: - N/A --- crates/languages/src/typescript/runnables.scm | 41 ++++++++++++++++++- docs/src/languages/deno.md | 34 +++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/crates/languages/src/typescript/runnables.scm b/crates/languages/src/typescript/runnables.scm index 85702cf99d..6bfc536329 100644 --- a/crates/languages/src/typescript/runnables.scm +++ b/crates/languages/src/typescript/runnables.scm @@ -1,4 +1,4 @@ -; Add support for (node:test, bun:test and Jest) runnable +; Add support for (node:test, bun:test, Jest and Deno.test) runnable ; Function expression that has `it`, `test` or `describe` as the function name ( (call_expression @@ -44,3 +44,42 @@ (#set! tag js-test) ) + +; Add support for Deno.test with string names +( + (call_expression + function: (member_expression + object: (identifier) @_namespace + property: (property_identifier) @_method + ) + (#eq? @_namespace "Deno") + (#eq? @_method "test") + arguments: ( + arguments . [ + (string (string_fragment) @run @DENO_TEST_NAME) + (identifier) @run @DENO_TEST_NAME + ] + ) + ) @_js-test + + (#set! tag js-test) +) + +; Add support for Deno.test with named function expressions +( + (call_expression + function: (member_expression + object: (identifier) @_namespace + property: (property_identifier) @_method + ) + (#eq? @_namespace "Deno") + (#eq? @_method "test") + arguments: ( + arguments . (function_expression + name: (identifier) @run @DENO_TEST_NAME + ) + ) + ) @_js-test + + (#set! tag js-test) +) diff --git a/docs/src/languages/deno.md b/docs/src/languages/deno.md index c18b112326..c40b6531e6 100644 --- a/docs/src/languages/deno.md +++ b/docs/src/languages/deno.md @@ -57,6 +57,40 @@ See [Configuring supported languages](../configuring-languages.md) in the Zed do TBD: Deno Typescript REPL instructions [docs/repl#typescript-deno](../repl.md#typescript-deno) --> +## DAP support + +To debug deno programs, add this to `.zed/debug.json` + +```json +[ + { + "adapter": "JavaScript", + "label": "Deno", + "request": "launch", + "type": "pwa-node", + "cwd": "$ZED_WORKTREE_ROOT", + "program": "$ZED_FILE", + "runtimeExecutable": "deno", + "runtimeArgs": ["run", "--allow-all", "--inspect-wait"], + "attachSimplePort": 9229 + } +] +``` + +## Runnable support + +To run deno tasks like tests from the ui, add this to `.zed/tasks.json` + +```json +[ + { + "label": "deno test", + "command": "deno test -A --filter '/^$ZED_CUSTOM_DENO_TEST_NAME$/' $ZED_FILE", + "tags": ["js-test"] + } +] +``` + ## See also: - [TypeScript](./typescript.md) From e5269212ade866d33866af5dc008d0710058e0ec Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 29 Jul 2025 02:10:32 +0200 Subject: [PATCH 6/8] lsp/python: Temporarily report just a singular workspace folder instead of all of the roots (#35243) Temporarily fixes #29133 Co-authored-by: Cole Release Notes: - python: Zed now reports a slightly different set of workspace folders for Python projects to work around quirks in handling of multi-lsp projects with virtual environment. This behavior will be revisited in a near future. Co-authored-by: Cole --- crates/language/src/language.rs | 16 +++++++ crates/language_tools/src/lsp_log.rs | 2 +- crates/languages/src/python.rs | 8 +++- crates/lsp/src/lsp.rs | 64 +++++++++++++++++++--------- crates/project/src/lsp_store.rs | 13 ++++-- 5 files changed, 76 insertions(+), 27 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1df33286ee..7cda2b4b5a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -313,6 +313,15 @@ impl Attach { } } +/// Determines what gets sent out as a workspace folders content +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum WorkspaceFoldersContent { + /// Send out a single entry with the root of the workspace. + WorktreeRoot, + /// Send out a list of subproject roots. + SubprojectRoots, +} + /// [`LspAdapterDelegate`] allows [`LspAdapter]` implementations to interface with the application // e.g. to display a notification or fetch data from the web. #[async_trait] @@ -606,6 +615,13 @@ pub trait LspAdapter: 'static + Send + Sync { Attach::Shared } + /// Determines whether a language server supports workspace folders. + /// + /// And does not trip over itself in the process. + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::SubprojectRoots + } + fn manifest_name(&self) -> Option { None } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d1a90d7dbb..2b0e13f4be 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -867,7 +867,7 @@ impl LspLogView { BINARY = server.binary(), WORKSPACE_FOLDERS = server .workspace_folders() - .iter() + .into_iter() .filter_map(|path| path .to_file_path() .ok() diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index dc6996d399..ebdbd93248 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -4,13 +4,13 @@ use async_trait::async_trait; use collections::HashMap; use gpui::{App, Task}; use gpui::{AsyncApp, SharedString}; -use language::Toolchain; use language::ToolchainList; use language::ToolchainLister; use language::language_settings::language_settings; use language::{ContextLocation, LanguageToolchainStore}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; +use language::{Toolchain, WorkspaceFoldersContent}; use lsp::LanguageServerBinary; use lsp::LanguageServerName; use node_runtime::NodeRuntime; @@ -400,6 +400,9 @@ impl LspAdapter for PythonLspAdapter { fn manifest_name(&self) -> Option { Some(SharedString::new_static("pyproject.toml").into()) } + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::WorktreeRoot + } } async fn get_cached_server_binary( @@ -1282,6 +1285,9 @@ impl LspAdapter for PyLspAdapter { fn manifest_name(&self) -> Option { Some(SharedString::new_static("pyproject.toml").into()) } + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::WorktreeRoot + } } #[cfg(test)] diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 9978d7ebb1..ccb39ab8a2 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -29,7 +29,7 @@ use std::{ ffi::{OsStr, OsString}, fmt, io::Write, - ops::{Deref, DerefMut}, + ops::DerefMut, path::PathBuf, pin::Pin, sync::{ @@ -100,7 +100,7 @@ pub struct LanguageServer { io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, server: Arc>>, - workspace_folders: Arc>>, + workspace_folders: Option>>>, root_uri: Url, } @@ -307,7 +307,7 @@ impl LanguageServer { binary: LanguageServerBinary, root_path: &Path, code_action_kinds: Option>, - workspace_folders: Arc>>, + workspace_folders: Option>>>, cx: &mut AsyncApp, ) -> Result { let working_dir = if root_path.is_dir() { @@ -381,7 +381,7 @@ impl LanguageServer { code_action_kinds: Option>, binary: LanguageServerBinary, root_uri: Url, - workspace_folders: Arc>>, + workspace_folders: Option>>>, cx: &mut AsyncApp, on_unhandled_notification: F, ) -> Self @@ -595,16 +595,26 @@ impl LanguageServer { } pub fn default_initialize_params(&self, pull_diagnostics: bool, cx: &App) -> InitializeParams { - let workspace_folders = self - .workspace_folders - .lock() - .iter() - .cloned() - .map(|uri| WorkspaceFolder { - name: Default::default(), - uri, - }) - .collect::>(); + let workspace_folders = self.workspace_folders.as_ref().map_or_else( + || { + vec![WorkspaceFolder { + name: Default::default(), + uri: self.root_uri.clone(), + }] + }, + |folders| { + folders + .lock() + .iter() + .cloned() + .map(|uri| WorkspaceFolder { + name: Default::default(), + uri, + }) + .collect() + }, + ); + #[allow(deprecated)] InitializeParams { process_id: None, @@ -1315,7 +1325,10 @@ impl LanguageServer { return; } - let is_new_folder = self.workspace_folders.lock().insert(uri.clone()); + let Some(workspace_folders) = self.workspace_folders.as_ref() else { + return; + }; + let is_new_folder = workspace_folders.lock().insert(uri.clone()); if is_new_folder { let params = DidChangeWorkspaceFoldersParams { event: WorkspaceFoldersChangeEvent { @@ -1345,7 +1358,10 @@ impl LanguageServer { { return; } - let was_removed = self.workspace_folders.lock().remove(&uri); + let Some(workspace_folders) = self.workspace_folders.as_ref() else { + return; + }; + let was_removed = workspace_folders.lock().remove(&uri); if was_removed { let params = DidChangeWorkspaceFoldersParams { event: WorkspaceFoldersChangeEvent { @@ -1360,7 +1376,10 @@ impl LanguageServer { } } pub fn set_workspace_folders(&self, folders: BTreeSet) { - let mut workspace_folders = self.workspace_folders.lock(); + let Some(workspace_folders) = self.workspace_folders.as_ref() else { + return; + }; + let mut workspace_folders = workspace_folders.lock(); let old_workspace_folders = std::mem::take(&mut *workspace_folders); let added: Vec<_> = folders @@ -1389,8 +1408,11 @@ impl LanguageServer { } } - pub fn workspace_folders(&self) -> impl Deref> + '_ { - self.workspace_folders.lock() + pub fn workspace_folders(&self) -> BTreeSet { + self.workspace_folders.as_ref().map_or_else( + || BTreeSet::from_iter([self.root_uri.clone()]), + |folders| folders.lock().clone(), + ) } pub fn register_buffer( @@ -1535,7 +1557,7 @@ impl FakeLanguageServer { None, binary.clone(), root, - workspace_folders.clone(), + Some(workspace_folders.clone()), cx, |_| {}, ); @@ -1554,7 +1576,7 @@ impl FakeLanguageServer { None, binary, Self::root_path(), - workspace_folders, + Some(workspace_folders), cx, move |msg| { notifications_tx diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 0cd375e0c5..3645839271 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -46,6 +46,7 @@ use language::{ DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, + WorkspaceFoldersContent, language_settings::{ FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings, }, @@ -217,6 +218,7 @@ impl LocalLspStore { let binary = self.get_language_server_binary(adapter.clone(), delegate.clone(), true, cx); let pending_workspace_folders: Arc>> = Default::default(); + let pending_server = cx.spawn({ let adapter = adapter.clone(); let server_name = adapter.name.clone(); @@ -242,14 +244,18 @@ impl LocalLspStore { return Ok(server); } + let code_action_kinds = adapter.code_action_kinds(); lsp::LanguageServer::new( stderr_capture, server_id, server_name, binary, &root_path, - adapter.code_action_kinds(), - pending_workspace_folders, + code_action_kinds, + Some(pending_workspace_folders).filter(|_| { + adapter.adapter.workspace_folders_content() + == WorkspaceFoldersContent::SubprojectRoots + }), cx, ) } @@ -575,8 +581,7 @@ impl LocalLspStore { }; let root = server.workspace_folders(); Ok(Some( - root.iter() - .cloned() + root.into_iter() .map(|uri| WorkspaceFolder { uri, name: Default::default(), From cfd5b8ff10cd88a97988292c964689f67301520b Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 28 Jul 2025 23:19:31 -0400 Subject: [PATCH 7/8] python: Uplift basedpyright support into core (#35250) This PR adds a built-in adapter for the basedpyright language server. For now, it's behind the `basedpyright` feature flag, and needs to be requested explicitly like this for staff: ``` "languages": { "Python": { "language_servers": ["basedpyright", "!pylsp", "!pyright"] } } ``` (After uninstalling the basedpyright extension.) Release Notes: - N/A --- Cargo.lock | 1 + crates/languages/Cargo.toml | 1 + crates/languages/src/lib.rs | 24 ++- crates/languages/src/python.rs | 337 +++++++++++++++++++++++++++++++++ 4 files changed, 362 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 25196fc349..7ab4a85c7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9226,6 +9226,7 @@ dependencies = [ "chrono", "collections", "dap", + "feature_flags", "futures 0.3.31", "gpui", "http_client", diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index 2e8f007cff..260126da63 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -41,6 +41,7 @@ async-trait.workspace = true chrono.workspace = true collections.workspace = true dap.workspace = true +feature_flags.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index a224111002..001fd15200 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -1,4 +1,5 @@ use anyhow::Context as _; +use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use gpui::{App, UpdateGlobal}; use node_runtime::NodeRuntime; use python::PyprojectTomlManifestProvider; @@ -11,7 +12,7 @@ use util::{ResultExt, asset_str}; pub use language::*; -use crate::json::JsonTaskProvider; +use crate::{json::JsonTaskProvider, python::BasedPyrightLspAdapter}; mod bash; mod c; @@ -52,6 +53,12 @@ pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock> = )) }); +struct BasedPyrightFeatureFlag; + +impl FeatureFlag for BasedPyrightFeatureFlag { + const NAME: &'static str = "basedpyright"; +} + pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { #[cfg(feature = "load-grammars")] languages.register_native_grammars([ @@ -88,6 +95,7 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { let py_lsp_adapter = Arc::new(python::PyLspAdapter::new()); let python_context_provider = Arc::new(python::PythonContextProvider); let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone())); + let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new()); let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default()); let rust_context_provider = Arc::new(rust::RustContextProvider); let rust_lsp_adapter = Arc::new(rust::RustLspAdapter); @@ -228,6 +236,20 @@ pub fn init(languages: Arc, node: NodeRuntime, cx: &mut App) { ); } + let mut basedpyright_lsp_adapter = Some(basedpyright_lsp_adapter); + cx.observe_flag::({ + let languages = languages.clone(); + move |enabled, _| { + if enabled { + if let Some(adapter) = basedpyright_lsp_adapter.take() { + languages + .register_available_lsp_adapter(adapter.name(), move || adapter.clone()); + } + } + } + }) + .detach(); + // Register globally available language servers. // // This will allow users to add support for a built-in language server (e.g., Tailwind) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index ebdbd93248..4a0cc7078b 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1290,6 +1290,343 @@ impl LspAdapter for PyLspAdapter { } } +pub(crate) struct BasedPyrightLspAdapter { + python_venv_base: OnceCell, String>>, +} + +impl BasedPyrightLspAdapter { + const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright"); + const BINARY_NAME: &'static str = "basedpyright-langserver"; + + pub(crate) fn new() -> Self { + Self { + python_venv_base: OnceCell::new(), + } + } + + async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result> { + let python_path = Self::find_base_python(delegate) + .await + .context("Could not find Python installation for basedpyright")?; + let work_dir = delegate + .language_server_download_dir(&Self::SERVER_NAME) + .await + .context("Could not get working directory for basedpyright")?; + let mut path = PathBuf::from(work_dir.as_ref()); + path.push("basedpyright-venv"); + if !path.exists() { + util::command::new_smol_command(python_path) + .arg("-m") + .arg("venv") + .arg("basedpyright-venv") + .current_dir(work_dir) + .spawn()? + .output() + .await?; + } + + Ok(path.into()) + } + + // Find "baseline", user python version from which we'll create our own venv. + async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option { + for path in ["python3", "python"] { + if let Some(path) = delegate.which(path.as_ref()).await { + return Some(path); + } + } + None + } + + async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result, String> { + self.python_venv_base + .get_or_init(move || async move { + Self::ensure_venv(delegate) + .await + .map_err(|e| format!("{e}")) + }) + .await + .clone() + } +} + +#[async_trait(?Send)] +impl LspAdapter for BasedPyrightLspAdapter { + fn name(&self) -> LanguageServerName { + Self::SERVER_NAME.clone() + } + + async fn initialization_options( + self: Arc, + _: &dyn Fs, + _: &Arc, + ) -> Result> { + // Provide minimal initialization options + // Virtual environment configuration will be handled through workspace configuration + Ok(Some(json!({ + "python": { + "analysis": { + "autoSearchPaths": true, + "useLibraryCodeForTypes": true, + "autoImportCompletions": true + } + } + }))) + } + + async fn check_if_user_installed( + &self, + delegate: &dyn LspAdapterDelegate, + toolchains: Arc, + cx: &AsyncApp, + ) -> Option { + if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await { + let env = delegate.shell_env().await; + Some(LanguageServerBinary { + path: bin, + env: Some(env), + arguments: vec!["--stdio".into()], + }) + } else { + let venv = toolchains + .active_toolchain( + delegate.worktree_id(), + Arc::from("".as_ref()), + LanguageName::new("Python"), + &mut cx.clone(), + ) + .await?; + let path = Path::new(venv.path.as_ref()) + .parent()? + .join(Self::BINARY_NAME); + path.exists().then(|| LanguageServerBinary { + path, + arguments: vec!["--stdio".into()], + env: None, + }) + } + } + + async fn fetch_latest_server_version( + &self, + _: &dyn LspAdapterDelegate, + ) -> Result> { + Ok(Box::new(()) as Box<_>) + } + + async fn fetch_server_binary( + &self, + _latest_version: Box, + _container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Result { + let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?; + let pip_path = venv.join(BINARY_DIR).join("pip3"); + ensure!( + util::command::new_smol_command(pip_path.as_path()) + .arg("install") + .arg("basedpyright") + .arg("-U") + .output() + .await? + .status + .success(), + "basedpyright installation failed" + ); + let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME); + Ok(LanguageServerBinary { + path: pylsp, + env: None, + arguments: vec!["--stdio".into()], + }) + } + + async fn cached_server_binary( + &self, + _container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Option { + let venv = self.base_venv(delegate).await.ok()?; + let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME); + Some(LanguageServerBinary { + path: pylsp, + env: None, + arguments: vec!["--stdio".into()], + }) + } + + async fn process_completions(&self, items: &mut [lsp::CompletionItem]) { + // Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`. + // Where `XX` is the sorting category, `YYYY` is based on most recent usage, + // and `name` is the symbol name itself. + // + // Because the symbol name is included, there generally are not ties when + // sorting by the `sortText`, so the symbol's fuzzy match score is not taken + // into account. Here, we remove the symbol name from the sortText in order + // to allow our own fuzzy score to be used to break ties. + // + // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 + for item in items { + let Some(sort_text) = &mut item.sort_text else { + continue; + }; + let mut parts = sort_text.split('.'); + let Some(first) = parts.next() else { continue }; + let Some(second) = parts.next() else { continue }; + let Some(_) = parts.next() else { continue }; + sort_text.replace_range(first.len() + second.len() + 1.., ""); + } + } + + async fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &Arc, + ) -> Option { + let label = &item.label; + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?, + lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?, + lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?, + lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?, + _ => return None, + }; + let filter_range = item + .filter_text + .as_deref() + .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len())) + .unwrap_or(0..label.len()); + Some(language::CodeLabel { + text: label.clone(), + runs: vec![(0..label.len(), highlight_id)], + filter_range, + }) + } + + async fn label_for_symbol( + &self, + name: &str, + kind: lsp::SymbolKind, + language: &Arc, + ) -> Option { + let (text, filter_range, display_range) = match kind { + lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => { + let text = format!("def {}():\n", name); + let filter_range = 4..4 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::CLASS => { + let text = format!("class {}:", name); + let filter_range = 6..6 + name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + lsp::SymbolKind::CONSTANT => { + let text = format!("{} = 0", name); + let filter_range = 0..name.len(); + let display_range = 0..filter_range.end; + (text, filter_range, display_range) + } + _ => return None, + }; + + Some(language::CodeLabel { + runs: language.highlight_text(&text.as_str().into(), display_range.clone()), + text: text[display_range].to_string(), + filter_range, + }) + } + + async fn workspace_configuration( + self: Arc, + _: &dyn Fs, + adapter: &Arc, + toolchains: Arc, + cx: &mut AsyncApp, + ) -> Result { + let toolchain = toolchains + .active_toolchain( + adapter.worktree_id(), + Arc::from("".as_ref()), + LanguageName::new("Python"), + cx, + ) + .await; + cx.update(move |cx| { + let mut user_settings = + language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) + .and_then(|s| s.settings.clone()) + .unwrap_or_default(); + + // If we have a detected toolchain, configure Pyright to use it + if let Some(toolchain) = toolchain { + if user_settings.is_null() { + user_settings = Value::Object(serde_json::Map::default()); + } + let object = user_settings.as_object_mut().unwrap(); + + let interpreter_path = toolchain.path.to_string(); + + // Detect if this is a virtual environment + if let Some(interpreter_dir) = Path::new(&interpreter_path).parent() { + if let Some(venv_dir) = interpreter_dir.parent() { + // Check if this looks like a virtual environment + if venv_dir.join("pyvenv.cfg").exists() + || venv_dir.join("bin/activate").exists() + || venv_dir.join("Scripts/activate.bat").exists() + { + // Set venvPath and venv at the root level + // This matches the format of a pyrightconfig.json file + if let Some(parent) = venv_dir.parent() { + // Use relative path if the venv is inside the workspace + let venv_path = if parent == adapter.worktree_root_path() { + ".".to_string() + } else { + parent.to_string_lossy().into_owned() + }; + object.insert("venvPath".to_string(), Value::String(venv_path)); + } + + if let Some(venv_name) = venv_dir.file_name() { + object.insert( + "venv".to_owned(), + Value::String(venv_name.to_string_lossy().into_owned()), + ); + } + } + } + } + + // Always set the python interpreter path + // Get or create the python section + let python = object + .entry("python") + .or_insert(Value::Object(serde_json::Map::default())) + .as_object_mut() + .unwrap(); + + // Set both pythonPath and defaultInterpreterPath for compatibility + python.insert( + "pythonPath".to_owned(), + Value::String(interpreter_path.clone()), + ); + python.insert( + "defaultInterpreterPath".to_owned(), + Value::String(interpreter_path), + ); + } + + user_settings + }) + } + + fn manifest_name(&self) -> Option { + Some(SharedString::new_static("pyproject.toml").into()) + } +} + #[cfg(test)] mod tests { use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext}; From 691b3ca238f6eb68e3fb42b94321855801b03f44 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Jul 2025 09:51:58 +0300 Subject: [PATCH 8/8] Cache LSP code lens requests (#35207) --- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_tests.rs | 108 ++++++++++++-- crates/editor/src/lsp_colors.rs | 6 +- crates/project/src/lsp_store.rs | 233 ++++++++++++++++++++++-------- crates/project/src/project.rs | 27 ++-- 5 files changed, 291 insertions(+), 89 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6bbd1a409d..3c877873a0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -21834,11 +21834,11 @@ impl CodeActionProvider for Entity { cx: &mut App, ) -> Task>> { self.update(cx, |project, cx| { - let code_lens = project.code_lens(buffer, range.clone(), cx); + let code_lens_actions = project.code_lens_actions(buffer, range.clone(), cx); let code_actions = project.code_actions(buffer, range, None, cx); cx.background_spawn(async move { - let (code_lens, code_actions) = join(code_lens, code_actions).await; - Ok(code_lens + let (code_lens_actions, code_actions) = join(code_lens_actions, code_actions).await; + Ok(code_lens_actions .context("code lens fetch")? .into_iter() .chain(code_actions.context("code action fetch")?) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a0333bb494..a13708c580 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -10072,8 +10072,14 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) { ); } -#[gpui::test] -async fn test_range_format_during_save(cx: &mut TestAppContext) { +async fn setup_range_format_test( + cx: &mut TestAppContext, +) -> ( + Entity, + Entity, + &mut gpui::VisualTestContext, + lsp::FakeLanguageServer, +) { init_test(cx, |_| {}); let fs = FakeFs::new(cx.executor()); @@ -10088,9 +10094,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { FakeLspAdapter { capabilities: lsp::ServerCapabilities { document_range_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -10105,14 +10111,22 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { let (editor, cx) = cx.add_window_view(|window, cx| { build_editor_with_project(project.clone(), buffer, window, cx) }); + + cx.executor().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + + (project, editor, cx, fake_server) +} + +#[gpui::test] +async fn test_range_format_on_save_success(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; + editor.update_in(cx, |editor, window, cx| { editor.set_text("one\ntwo\nthree\n", window, cx) }); assert!(cx.read(|cx| editor.is_dirty(cx))); - cx.executor().start_waiting(); - let fake_server = fake_servers.next().await.unwrap(); - let save = editor .update_in(cx, |editor, window, cx| { editor.save( @@ -10147,13 +10161,18 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { "one, two\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); +} + +#[gpui::test] +async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; editor.update_in(cx, |editor, window, cx| { editor.set_text("one\ntwo\nthree\n", window, cx) }); assert!(cx.read(|cx| editor.is_dirty(cx))); - // Ensure we can still save even if formatting hangs. + // Test that save still works when formatting hangs fake_server.set_request_handler::( move |params, _| async move { assert_eq!( @@ -10185,8 +10204,13 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { "one\ntwo\nthree\n" ); assert!(!cx.read(|cx| editor.is_dirty(cx))); +} - // For non-dirty buffer, no formatting request should be sent +#[gpui::test] +async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; + + // Buffer starts clean, no formatting should be requested let save = editor .update_in(cx, |editor, window, cx| { editor.save( @@ -10207,6 +10231,12 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { .next(); cx.executor().start_waiting(); save.await; + cx.run_until_parked(); +} + +#[gpui::test] +async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) { + let (project, editor, cx, fake_server) = setup_range_format_test(cx).await; // Set Rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { @@ -10220,7 +10250,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { }); editor.update_in(cx, |editor, window, cx| { - editor.set_text("somehting_new\n", window, cx) + editor.set_text("something_new\n", window, cx) }); assert!(cx.read(|cx| editor.is_dirty(cx))); let save = editor @@ -21310,16 +21340,32 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex }, ); - let (buffer, _handle) = project - .update(cx, |p, cx| { - p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx) + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from(path!("/dir/a.ts")), + OpenOptions::default(), + window, + cx, + ) }) + .unwrap() .await + .unwrap() + .downcast::() .unwrap(); cx.executor().run_until_parked(); let fake_server = fake_language_servers.next().await.unwrap(); + let buffer = editor.update(cx, |editor, cx| { + editor + .buffer() + .read(cx) + .as_singleton() + .expect("have opened a single file by path") + }); + let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left); drop(buffer_snapshot); @@ -21377,7 +21423,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex assert_eq!( actions.len(), 1, - "Should have only one valid action for the 0..0 range" + "Should have only one valid action for the 0..0 range, got: {actions:#?}" ); let action = actions[0].clone(); let apply = project.update(cx, |project, cx| { @@ -21423,7 +21469,7 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex .into_iter() .collect(), ), - ..Default::default() + ..lsp::WorkspaceEdit::default() }, }, ) @@ -21446,6 +21492,38 @@ async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContex buffer.undo(cx); assert_eq!(buffer.text(), "a"); }); + + let actions_after_edits = cx + .update_window(*workspace, |_, window, cx| { + project.code_actions(&buffer, anchor..anchor, window, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + actions, actions_after_edits, + "For the same selection, same code lens actions should be returned" + ); + + let _responses = + fake_server.set_request_handler::(|_, _| async move { + panic!("No more code lens requests are expected"); + }); + editor.update_in(cx, |editor, window, cx| { + editor.select_all(&SelectAll, window, cx); + }); + cx.executor().run_until_parked(); + let new_actions = cx + .update_window(*workspace, |_, window, cx| { + project.code_actions(&buffer, anchor..anchor, window, cx) + }) + .unwrap() + .await + .unwrap(); + assert_eq!( + actions, new_actions, + "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now" + ); } #[gpui::test] diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index ce07dd43fe..08cf9078f2 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -6,7 +6,7 @@ use gpui::{Hsla, Rgba}; use itertools::Itertools; use language::point_from_lsp; use multi_buffer::Anchor; -use project::{DocumentColor, lsp_store::ColorFetchStrategy}; +use project::{DocumentColor, lsp_store::LspFetchStrategy}; use settings::Settings as _; use text::{Bias, BufferId, OffsetRangeExt as _}; use ui::{App, Context, Window}; @@ -180,9 +180,9 @@ impl Editor { .filter_map(|buffer| { let buffer_id = buffer.read(cx).remote_id(); let fetch_strategy = if ignore_cache { - ColorFetchStrategy::IgnoreCache + LspFetchStrategy::IgnoreCache } else { - ColorFetchStrategy::UseCache { + LspFetchStrategy::UseCache { known_cache_version: self.colors.as_ref().and_then(|colors| { Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used) }), diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3645839271..defe056dd8 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3556,7 +3556,8 @@ pub struct LspStore { _maintain_buffer_languages: Task<()>, diagnostic_summaries: HashMap, HashMap>>, - lsp_data: HashMap, + lsp_document_colors: HashMap, + lsp_code_lens: HashMap, } #[derive(Debug, Default, Clone)] @@ -3566,6 +3567,7 @@ pub struct DocumentColors { } type DocumentColorTask = Shared>>>; +type CodeLensTask = Shared, Arc>>>; #[derive(Debug, Default)] struct DocumentColorData { @@ -3575,8 +3577,15 @@ struct DocumentColorData { colors_update: Option<(Global, DocumentColorTask)>, } +#[derive(Debug, Default)] +struct CodeLensData { + lens_for_version: Global, + lens: HashMap>, + update: Option<(Global, CodeLensTask)>, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum ColorFetchStrategy { +pub enum LspFetchStrategy { IgnoreCache, UseCache { known_cache_version: Option }, } @@ -3809,7 +3818,8 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), - lsp_data: HashMap::default(), + lsp_document_colors: HashMap::default(), + lsp_code_lens: HashMap::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3866,7 +3876,8 @@ impl LspStore { language_server_statuses: Default::default(), nonce: StdRng::from_entropy().r#gen(), diagnostic_summaries: HashMap::default(), - lsp_data: HashMap::default(), + lsp_document_colors: HashMap::default(), + lsp_code_lens: HashMap::default(), active_entry: None, toolchain_store, _maintain_workspace_config, @@ -4167,7 +4178,8 @@ impl LspStore { *refcount }; if refcount == 0 { - lsp_store.lsp_data.remove(&buffer_id); + lsp_store.lsp_document_colors.remove(&buffer_id); + lsp_store.lsp_code_lens.remove(&buffer_id); let local = lsp_store.as_local_mut().unwrap(); local.registered_buffers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id); @@ -5707,69 +5719,168 @@ impl LspStore { } } - pub fn code_lens( + pub fn code_lens_actions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, cx: &mut Context, - ) -> Task>> { + ) -> CodeLensTask { + let version_queried_for = buffer.read(cx).version(); + let buffer_id = buffer.read(cx).remote_id(); + + if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) { + if !version_queried_for.changed_since(&cached_data.lens_for_version) { + let has_different_servers = self.as_local().is_some_and(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + != cached_data.lens.keys().copied().collect() + }); + if !has_different_servers { + return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) + .shared(); + } + } + } + + let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); + if let Some((updating_for, running_update)) = &lsp_data.update { + if !version_queried_for.changed_since(&updating_for) { + return running_update.clone(); + } + } + let buffer = buffer.clone(); + let query_version_queried_for = version_queried_for.clone(); + let new_task = cx + .spawn(async move |lsp_store, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + let fetched_lens = lsp_store + .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx)) + .map_err(Arc::new)? + .await + .context("fetching code lens") + .map_err(Arc::new); + let fetched_lens = match fetched_lens { + Ok(fetched_lens) => fetched_lens, + Err(e) => { + lsp_store + .update(cx, |lsp_store, _| { + lsp_store.lsp_code_lens.entry(buffer_id).or_default().update = None; + }) + .ok(); + return Err(e); + } + }; + + lsp_store + .update(cx, |lsp_store, _| { + let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); + if lsp_data.lens_for_version == query_version_queried_for { + lsp_data.lens.extend(fetched_lens.clone()); + } else if !lsp_data + .lens_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.lens_for_version = query_version_queried_for; + lsp_data.lens = fetched_lens.clone(); + } + lsp_data.update = None; + lsp_data.lens.values().flatten().cloned().collect() + }) + .map_err(Arc::new) + }) + .shared(); + lsp_data.update = Some((version_queried_for, new_task.clone())); + new_task + } + + fn fetch_code_lens( + &mut self, + buffer: &Entity, + cx: &mut Context, + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request_task = upstream_client.request(proto::MultiLspQuery { - buffer_id: buffer_handle.read(cx).remote_id().into(), - version: serialize_version(&buffer_handle.read(cx).version()), + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, strategy: Some(proto::multi_lsp_query::Strategy::All( proto::AllLanguageServers {}, )), request: Some(proto::multi_lsp_query::Request::GetCodeLens( - GetCodeLens.to_proto(project_id, buffer_handle.read(cx)), + GetCodeLens.to_proto(project_id, buffer.read(cx)), )), }); - let buffer = buffer_handle.clone(); - cx.spawn(async move |weak_project, cx| { - let Some(project) = weak_project.upgrade() else { - return Ok(Vec::new()); + let buffer = buffer.clone(); + cx.spawn(async move |weak_lsp_store, cx| { + let Some(lsp_store) = weak_lsp_store.upgrade() else { + return Ok(HashMap::default()); }; let responses = request_task.await?.responses; - let code_lens = join_all( + let code_lens_actions = join_all( responses .into_iter() - .filter_map(|lsp_response| match lsp_response.response? { - proto::lsp_response::Response::GetCodeLensResponse(response) => { - Some(response) - } - unexpected => { - debug_panic!("Unexpected response: {unexpected:?}"); - None - } + .filter_map(|lsp_response| { + let response = match lsp_response.response? { + proto::lsp_response::Response::GetCodeLensResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }?; + let server_id = LanguageServerId::from_proto(lsp_response.server_id); + Some((server_id, response)) }) - .map(|code_lens_response| { - GetCodeLens.response_from_proto( - code_lens_response, - project.clone(), - buffer.clone(), - cx.clone(), - ) + .map(|(server_id, code_lens_response)| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + async move { + ( + server_id, + GetCodeLens + .response_from_proto( + code_lens_response, + lsp_store, + buffer, + cx, + ) + .await, + ) + } }), ) .await; - Ok(code_lens + let mut has_errors = false; + let code_lens_actions = code_lens_actions .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect()) + .filter_map(|(server_id, code_lens)| match code_lens { + Ok(code_lens) => Some((server_id, code_lens)), + Err(e) => { + has_errors = true; + log::error!("{e:#}"); + None + } + }) + .collect::>(); + anyhow::ensure!( + !has_errors || !code_lens_actions.is_empty(), + "Failed to fetch code lens" + ); + Ok(code_lens_actions) }) } else { - let code_lens_task = - self.request_multiple_lsp_locally(buffer_handle, None::, GetCodeLens, cx); - cx.spawn(async move |_, _| { - Ok(code_lens_task - .await - .into_iter() - .flat_map(|(_, code_lens)| code_lens) - .collect()) - }) + let code_lens_actions_task = + self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); + cx.background_spawn( + async move { Ok(code_lens_actions_task.await.into_iter().collect()) }, + ) } } @@ -6602,7 +6713,7 @@ impl LspStore { pub fn document_colors( &mut self, - fetch_strategy: ColorFetchStrategy, + fetch_strategy: LspFetchStrategy, buffer: Entity, cx: &mut Context, ) -> Option { @@ -6610,11 +6721,11 @@ impl LspStore { let buffer_id = buffer.read(cx).remote_id(); match fetch_strategy { - ColorFetchStrategy::IgnoreCache => {} - ColorFetchStrategy::UseCache { + LspFetchStrategy::IgnoreCache => {} + LspFetchStrategy::UseCache { known_cache_version, } => { - if let Some(cached_data) = self.lsp_data.get(&buffer_id) { + if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) { if !version_queried_for.changed_since(&cached_data.colors_for_version) { let has_different_servers = self.as_local().is_some_and(|local| { local @@ -6647,7 +6758,7 @@ impl LspStore { } } - let lsp_data = self.lsp_data.entry(buffer_id).or_default(); + let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); if let Some((updating_for, running_update)) = &lsp_data.colors_update { if !version_queried_for.changed_since(&updating_for) { return Some(running_update.clone()); @@ -6661,14 +6772,14 @@ impl LspStore { .await; let fetched_colors = lsp_store .update(cx, |lsp_store, cx| { - lsp_store.fetch_document_colors_for_buffer(buffer.clone(), cx) + lsp_store.fetch_document_colors_for_buffer(&buffer, cx) })? .await .context("fetching document colors") .map_err(Arc::new); let fetched_colors = match fetched_colors { Ok(fetched_colors) => { - if fetch_strategy != ColorFetchStrategy::IgnoreCache + if fetch_strategy != LspFetchStrategy::IgnoreCache && Some(true) == buffer .update(cx, |buffer, _| { @@ -6684,7 +6795,7 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { lsp_store - .lsp_data + .lsp_document_colors .entry(buffer_id) .or_default() .colors_update = None; @@ -6696,7 +6807,7 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.lsp_data.entry(buffer_id).or_default(); + let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); if lsp_data.colors_for_version == query_version_queried_for { lsp_data.colors.extend(fetched_colors.clone()); @@ -6730,7 +6841,7 @@ impl LspStore { fn fetch_document_colors_for_buffer( &mut self, - buffer: Entity, + buffer: &Entity, cx: &mut Context, ) -> Task>>> { if let Some((client, project_id)) = self.upstream_client() { @@ -6745,6 +6856,7 @@ impl LspStore { GetDocumentColor {}.to_proto(project_id, buffer.read(cx)), )), }); + let buffer = buffer.clone(); cx.spawn(async move |project, cx| { let Some(project) = project.upgrade() else { return Ok(HashMap::default()); @@ -6790,7 +6902,7 @@ impl LspStore { }) } else { let document_colors_task = - self.request_multiple_lsp_locally(&buffer, None::, GetDocumentColor, cx); + self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); cx.spawn(async move |_, _| { Ok(document_colors_task .await @@ -11283,9 +11395,12 @@ impl LspStore { } fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { - for buffer_lsp_data in self.lsp_data.values_mut() { - buffer_lsp_data.colors.remove(&for_server); - buffer_lsp_data.cache_version += 1; + for buffer_colors in self.lsp_document_colors.values_mut() { + buffer_colors.colors.remove(&for_server); + buffer_colors.cache_version += 1; + } + for buffer_lens in self.lsp_code_lens.values_mut() { + buffer_lens.lens.remove(&for_server); } if let Some(local) = self.as_local_mut() { local.buffer_pull_diagnostics_result_ids.remove(&for_server); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9c59d2e95..a4e76ed475 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -113,7 +113,7 @@ use std::{ use task_store::TaskStore; use terminals::Terminals; -use text::{Anchor, BufferId, Point}; +use text::{Anchor, BufferId, OffsetRangeExt, Point}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, @@ -590,7 +590,7 @@ pub(crate) struct CoreCompletion { } /// A code action provided by a language server. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct CodeAction { /// The id of the language server that produced this code action. pub server_id: LanguageServerId, @@ -604,7 +604,7 @@ pub struct CodeAction { } /// An action sent back by a language server. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum LspAction { /// An action with the full data, may have a command or may not. /// May require resolving. @@ -3607,20 +3607,29 @@ impl Project { }) } - pub fn code_lens( + pub fn code_lens_actions( &mut self, - buffer_handle: &Entity, + buffer: &Entity, range: Range, cx: &mut Context, ) -> Task>> { - let snapshot = buffer_handle.read(cx).snapshot(); - let range = snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end); + let snapshot = buffer.read(cx).snapshot(); + let range = range.clone().to_owned().to_point(&snapshot); + let range_start = snapshot.anchor_before(range.start); + let range_end = if range.start == range.end { + range_start + } else { + snapshot.anchor_after(range.end) + }; + let range = range_start..range_end; let code_lens_actions = self .lsp_store - .update(cx, |lsp_store, cx| lsp_store.code_lens(buffer_handle, cx)); + .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(buffer, cx)); cx.background_spawn(async move { - let mut code_lens_actions = code_lens_actions.await?; + let mut code_lens_actions = code_lens_actions + .await + .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?; code_lens_actions.retain(|code_lens_action| { range .start