Merge branch 'main' into mcp-acp-gemini

Ben Brandt created

Change summary

Cargo.lock                                    |   1 
assets/icons/cloud_download.svg               |   1 
crates/editor/src/editor.rs                   |  11 
crates/editor/src/editor_tests.rs             | 108 +++++
crates/editor/src/lsp_colors.rs               |   6 
crates/icons/src/icons.rs                     |   1 
crates/language/src/language.rs               |  16 
crates/language_tools/src/lsp_log.rs          |   2 
crates/languages/Cargo.toml                   |   1 
crates/languages/src/lib.rs                   |  24 +
crates/languages/src/python.rs                | 345 ++++++++++++++++++++
crates/languages/src/typescript/runnables.scm |  41 ++
crates/lsp/src/lsp.rs                         |  64 ++-
crates/onboarding/src/welcome.rs              |   3 
crates/project/src/lsp_store.rs               | 246 +++++++++++---
crates/project/src/project.rs                 |  27 +
crates/workspace/src/tasks.rs                 |   8 
crates/workspace/src/workspace.rs             |   2 
crates/zed/src/zed.rs                         |  33 +
docs/src/configuring-zed.md                   |   2 
docs/src/languages/deno.md                    |  34 ++
21 files changed, 837 insertions(+), 139 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9226,6 +9226,7 @@ dependencies = [
  "chrono",
  "collections",
  "dap",
+ "feature_flags",
  "futures 0.3.31",
  "gpui",
  "http_client",

assets/icons/cloud_download.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-download-icon lucide-cloud-download"><path d="M12 13v8l-4-4"/><path d="m12 21 4-4"/><path d="M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284"/></svg>

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.
@@ -21835,11 +21834,11 @@ impl CodeActionProvider for Entity<Project> {
         cx: &mut App,
     ) -> Task<Result<Vec<CodeAction>>> {
         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")?)

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<Project>,
+    Entity<Editor>,
+    &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::<lsp::request::RangeFormatting, _, _>(
         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)));
+}
+
+#[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;
 
-    // For non-dirty buffer, no formatting request should be sent
+    // 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::<Editor>()
         .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::<lsp::request::CodeLensRequest, _, _>(|_, _| 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]

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)
                             }),

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<ManifestName> {
         None
     }

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()

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

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<Arc<Language>> =
         ))
     });
 
+struct BasedPyrightFeatureFlag;
+
+impl FeatureFlag for BasedPyrightFeatureFlag {
+    const NAME: &'static str = "basedpyright";
+}
+
 pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
     #[cfg(feature = "load-grammars")]
     languages.register_native_grammars([
@@ -88,6 +95,7 @@ pub fn init(languages: Arc<LanguageRegistry>, 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<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
         );
     }
 
+    let mut basedpyright_lsp_adapter = Some(basedpyright_lsp_adapter);
+    cx.observe_flag::<BasedPyrightFeatureFlag, _>({
+        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)

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<ManifestName> {
         Some(SharedString::new_static("pyproject.toml").into())
     }
+    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+        WorkspaceFoldersContent::WorktreeRoot
+    }
 }
 
 async fn get_cached_server_binary(
@@ -1282,6 +1285,346 @@ impl LspAdapter for PyLspAdapter {
     fn manifest_name(&self) -> Option<ManifestName> {
         Some(SharedString::new_static("pyproject.toml").into())
     }
+    fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
+        WorkspaceFoldersContent::WorktreeRoot
+    }
+}
+
+pub(crate) struct BasedPyrightLspAdapter {
+    python_venv_base: OnceCell<Result<Arc<Path>, 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<Arc<Path>> {
+        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<PathBuf> {
+        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<Arc<Path>, 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<Self>,
+        _: &dyn Fs,
+        _: &Arc<dyn LspAdapterDelegate>,
+    ) -> Result<Option<Value>> {
+        // 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<dyn LanguageToolchainStore>,
+        cx: &AsyncApp,
+    ) -> Option<LanguageServerBinary> {
+        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<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(()) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        _latest_version: Box<dyn 'static + Send + Any>,
+        _container_dir: PathBuf,
+        delegate: &dyn LspAdapterDelegate,
+    ) -> Result<LanguageServerBinary> {
+        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<LanguageServerBinary> {
+        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<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        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<language::Language>,
+    ) -> Option<language::CodeLabel> {
+        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<Self>,
+        _: &dyn Fs,
+        adapter: &Arc<dyn LspAdapterDelegate>,
+        toolchains: Arc<dyn LanguageToolchainStore>,
+        cx: &mut AsyncApp,
+    ) -> Result<Value> {
+        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<ManifestName> {
+        Some(SharedString::new_static("pyproject.toml").into())
+    }
 }
 
 #[cfg(test)]

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)
+)

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<Option<(Task<Option<()>>, Task<Option<()>>)>>,
     output_done_rx: Mutex<Option<barrier::Receiver>>,
     server: Arc<Mutex<Option<Child>>>,
-    workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+    workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
     root_uri: Url,
 }
 
@@ -307,7 +307,7 @@ impl LanguageServer {
         binary: LanguageServerBinary,
         root_path: &Path,
         code_action_kinds: Option<Vec<CodeActionKind>>,
-        workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+        workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
         cx: &mut AsyncApp,
     ) -> Result<Self> {
         let working_dir = if root_path.is_dir() {
@@ -381,7 +381,7 @@ impl LanguageServer {
         code_action_kinds: Option<Vec<CodeActionKind>>,
         binary: LanguageServerBinary,
         root_uri: Url,
-        workspace_folders: Arc<Mutex<BTreeSet<Url>>>,
+        workspace_folders: Option<Arc<Mutex<BTreeSet<Url>>>>,
         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::<Vec<_>>();
+        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<Url>) {
-        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<Target = BTreeSet<Url>> + '_ {
-        self.workspace_folders.lock()
+    pub fn workspace_folders(&self) -> BTreeSet<Url> {
+        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

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,

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<Mutex<BTreeSet<Url>>> = 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(),
@@ -3551,7 +3556,8 @@ pub struct LspStore {
     _maintain_buffer_languages: Task<()>,
     diagnostic_summaries:
         HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
-    lsp_data: HashMap<BufferId, DocumentColorData>,
+    lsp_document_colors: HashMap<BufferId, DocumentColorData>,
+    lsp_code_lens: HashMap<BufferId, CodeLensData>,
 }
 
 #[derive(Debug, Default, Clone)]
@@ -3561,6 +3567,7 @@ pub struct DocumentColors {
 }
 
 type DocumentColorTask = Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>;
+type CodeLensTask = Shared<Task<std::result::Result<Vec<CodeAction>, Arc<anyhow::Error>>>>;
 
 #[derive(Debug, Default)]
 struct DocumentColorData {
@@ -3570,8 +3577,15 @@ struct DocumentColorData {
     colors_update: Option<(Global, DocumentColorTask)>,
 }
 
+#[derive(Debug, Default)]
+struct CodeLensData {
+    lens_for_version: Global,
+    lens: HashMap<LanguageServerId, Vec<CodeAction>>,
+    update: Option<(Global, CodeLensTask)>,
+}
+
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum ColorFetchStrategy {
+pub enum LspFetchStrategy {
     IgnoreCache,
     UseCache { known_cache_version: Option<usize> },
 }
@@ -3804,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),
@@ -3861,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,
@@ -4162,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);
@@ -5702,69 +5719,168 @@ impl LspStore {
         }
     }
 
-    pub fn code_lens(
+    pub fn code_lens_actions(
         &mut self,
-        buffer_handle: &Entity<Buffer>,
+        buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
-    ) -> Task<Result<Vec<CodeAction>>> {
+    ) -> 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<Buffer>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<HashMap<LanguageServerId, Vec<CodeAction>>>> {
         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::<Result<Vec<Vec<_>>>>()?
-                    .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::<HashMap<_, _>>();
+                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::<usize>, 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::<usize>, GetCodeLens, cx);
+            cx.background_spawn(
+                async move { Ok(code_lens_actions_task.await.into_iter().collect()) },
+            )
         }
     }
 
@@ -6597,7 +6713,7 @@ impl LspStore {
 
     pub fn document_colors(
         &mut self,
-        fetch_strategy: ColorFetchStrategy,
+        fetch_strategy: LspFetchStrategy,
         buffer: Entity<Buffer>,
         cx: &mut Context<Self>,
     ) -> Option<DocumentColorTask> {
@@ -6605,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
@@ -6642,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());
@@ -6656,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, _| {
@@ -6679,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;
@@ -6691,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());
@@ -6725,7 +6841,7 @@ impl LspStore {
 
     fn fetch_document_colors_for_buffer(
         &mut self,
-        buffer: Entity<Buffer>,
+        buffer: &Entity<Buffer>,
         cx: &mut Context<Self>,
     ) -> Task<anyhow::Result<HashMap<LanguageServerId, HashSet<DocumentColor>>>> {
         if let Some((client, project_id)) = self.upstream_client() {
@@ -6740,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());
@@ -6785,7 +6902,7 @@ impl LspStore {
             })
         } else {
             let document_colors_task =
-                self.request_multiple_lsp_locally(&buffer, None::<usize>, GetDocumentColor, cx);
+                self.request_multiple_lsp_locally(buffer, None::<usize>, GetDocumentColor, cx);
             cx.spawn(async move |_, _| {
                 Ok(document_colors_task
                     .await
@@ -11278,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);

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<T: Clone + ToOffset>(
+    pub fn code_lens_actions<T: Clone + ToOffset>(
         &mut self,
-        buffer_handle: &Entity<Buffer>,
+        buffer: &Entity<Buffer>,
         range: Range<T>,
         cx: &mut Context<Self>,
     ) -> Task<Result<Vec<CodeAction>>> {
-        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

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);
         }
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -1104,6 +1104,7 @@ pub struct Workspace {
     serialized_ssh_project: Option<SerializedSshProject>,
     _items_serializer: Task<Result<()>>,
     session_id: Option<String>,
+    scheduled_tasks: Vec<Task<()>>,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -1435,6 +1436,7 @@ impl Workspace {
             _items_serializer,
             session_id: Some(session_id),
             serialized_ssh_project: None,
+            scheduled_tasks: Vec::new(),
         }
     }
 

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::<PanicFeatureFlag>() {
-        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::<PanicFeatureFlag>();
+    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);

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
 

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)