From 189ea49e00c4e1a60bb97534d3bdbf1b3d22b117 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 13 Aug 2025 12:47:57 -0400 Subject: [PATCH 01/30] v0.200.x preview --- crates/zed/RELEASE_CHANNEL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 38f8e886e1..4de2f126df 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -dev +preview \ No newline at end of file From d29341bf44760b92911b15de893e399ff43bc790 Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 00:19:37 +0530 Subject: [PATCH 02/30] copilot: Fix Copilot fails to sign in (#36138) Closes #36093 Pin copilot version to 1.354 for now until further investigation. Release Notes: - Fixes issue where Copilot failed to sign in. Co-authored-by: MrSubidubi --- crates/copilot/src/copilot.rs | 12 ++++++------ crates/languages/src/css.rs | 8 +++++++- crates/languages/src/json.rs | 8 +++++++- crates/languages/src/python.rs | 1 + crates/languages/src/tailwind.rs | 8 +++++++- crates/languages/src/typescript.rs | 1 + crates/languages/src/vtsls.rs | 2 ++ crates/languages/src/yaml.rs | 8 +++++++- crates/node_runtime/src/node_runtime.rs | 15 ++++++++++++++- 9 files changed, 52 insertions(+), 11 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 49ae2b9d9c..166a582c70 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -21,7 +21,7 @@ use language::{ point_from_lsp, point_to_lsp, }; use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName}; -use node_runtime::NodeRuntime; +use node_runtime::{NodeRuntime, VersionCheck}; use parking_lot::Mutex; use project::DisableAiSettings; use request::StatusNotification; @@ -1169,9 +1169,8 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: const SERVER_PATH: &str = "node_modules/@github/copilot-language-server/dist/language-server.js"; - let latest_version = node_runtime - .npm_package_latest_version(PACKAGE_NAME) - .await?; + // pinning it: https://github.com/zed-industries/zed/issues/36093 + const PINNED_VERSION: &str = "1.354"; let server_path = paths::copilot_dir().join(SERVER_PATH); fs.create_dir(paths::copilot_dir()).await?; @@ -1181,12 +1180,13 @@ async fn get_copilot_lsp(fs: Arc, node_runtime: NodeRuntime) -> anyhow:: PACKAGE_NAME, &server_path, paths::copilot_dir(), - &latest_version, + &PINNED_VERSION, + VersionCheck::VersionMismatch, ) .await; if should_install { node_runtime - .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &latest_version)]) + .npm_install_packages(paths::copilot_dir(), &[(PACKAGE_NAME, &PINNED_VERSION)]) .await?; } diff --git a/crates/languages/src/css.rs b/crates/languages/src/css.rs index 7725e079be..19329fcc6e 100644 --- a/crates/languages/src/css.rs +++ b/crates/languages/src/css.rs @@ -103,7 +103,13 @@ impl LspAdapter for CssLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index ca82bb2431..019b45d396 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -340,7 +340,13 @@ impl LspAdapter for JsonLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 0524c02fd5..5513324487 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -206,6 +206,7 @@ impl LspAdapter for PythonLspAdapter { &server_path, &container_dir, &version, + Default::default(), ) .await; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index a7edbb148c..6f03eeda8d 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -108,7 +108,13 @@ impl LspAdapter for TailwindLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index f976b62614..a8ba880889 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -589,6 +589,7 @@ impl LspAdapter for TypeScriptLspAdapter { &server_path, &container_dir, version.typescript_version.as_str(), + Default::default(), ) .await; diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index 33751f733e..73498fc579 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -116,6 +116,7 @@ impl LspAdapter for VtslsLspAdapter { &server_path, &container_dir, &latest_version.server_version, + Default::default(), ) .await { @@ -129,6 +130,7 @@ impl LspAdapter for VtslsLspAdapter { &container_dir.join(Self::TYPESCRIPT_TSDK_PATH), &container_dir, &latest_version.typescript_version, + Default::default(), ) .await { diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 815605d524..28be2cc1a4 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -104,7 +104,13 @@ impl LspAdapter for YamlLspAdapter { let should_install_language_server = self .node - .should_install_npm_package(Self::PACKAGE_NAME, &server_path, &container_dir, &version) + .should_install_npm_package( + Self::PACKAGE_NAME, + &server_path, + &container_dir, + &version, + Default::default(), + ) .await; if should_install_language_server { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 08698a1d6c..6fcc3a728a 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -29,6 +29,15 @@ pub struct NodeBinaryOptions { pub use_paths: Option<(PathBuf, PathBuf)>, } +#[derive(Default)] +pub enum VersionCheck { + /// Check whether the installed and requested version have a mismatch + VersionMismatch, + /// Only check whether the currently installed version is older than the newest one + #[default] + OlderVersion, +} + #[derive(Clone)] pub struct NodeRuntime(Arc>); @@ -287,6 +296,7 @@ impl NodeRuntime { local_executable_path: &Path, local_package_directory: &Path, latest_version: &str, + version_check: VersionCheck, ) -> bool { // In the case of the local system not having the package installed, // or in the instances where we fail to parse package.json data, @@ -311,7 +321,10 @@ impl NodeRuntime { return true; }; - installed_version < latest_version + match version_check { + VersionCheck::VersionMismatch => installed_version != latest_version, + VersionCheck::OlderVersion => installed_version < latest_version, + } } } From ab41359e24603dde1ab9d81dda864b8f125da38a Mon Sep 17 00:00:00 2001 From: smit Date: Thu, 14 Aug 2025 00:59:12 +0530 Subject: [PATCH 03/30] ci: Disable FreeBSD builds (#36140) Revert accidental change introduced in [#35880](https://github.com/zed-industries/zed/pull/35880/files#diff-b803fcb7f17ed9235f1e5cb1fcd2f5d3b2838429d4368ae4c57ce4436577f03fL706) Release Notes: - N/A --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b70271e57..f4ba227168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -718,7 +718,7 @@ jobs: timeout-minutes: 60 runs-on: github-8vcpu-ubuntu-2404 if: | - ( startsWith(github.ref, 'refs/tags/v') + false && ( startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) needs: [linux_tests] name: Build Zed on FreeBSD From 08351cb3e74ea0b758f050ca53c286b243237323 Mon Sep 17 00:00:00 2001 From: Zed Bot Date: Wed, 13 Aug 2025 20:05:16 +0000 Subject: [PATCH 04/30] Bump to 0.200.1 for @smitbarmase --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8458c4af4b..f78007d037 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20500,7 +20500,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.0" +version = "0.200.1" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bdbb39698c..728f112e80 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.0" +version = "0.200.1" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From fda9369bfd1f511dfa06f644cbfbd7eb8822d89b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Aug 2025 11:22:38 -0400 Subject: [PATCH 05/30] Emit a `BreadcrumbsChanged` event when associated settings changed (#36177) Closes https://github.com/zed-industries/zed/issues/36149 Release Notes: - Fixed a bug where changing the `toolbar.breadcrumbs` setting didn't immediately update the UI when saving the `settings.json` file. --- crates/editor/src/editor.rs | 6 ++++++ crates/editor/src/items.rs | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8a9398e71f..9afb7425a6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20200,6 +20200,7 @@ impl Editor { ); let old_cursor_shape = self.cursor_shape; + let old_show_breadcrumbs = self.show_breadcrumbs; { let editor_settings = EditorSettings::get_global(cx); @@ -20213,6 +20214,10 @@ impl Editor { cx.emit(EditorEvent::CursorShapeChanged); } + if old_show_breadcrumbs != self.show_breadcrumbs { + cx.emit(EditorEvent::BreadcrumbsChanged); + } + let project_settings = ProjectSettings::get_global(cx); self.serialize_dirty_buffers = !self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers; @@ -22834,6 +22839,7 @@ pub enum EditorEvent { }, Reloaded, CursorShapeChanged, + BreadcrumbsChanged, PushedToNavHistory { anchor: Anchor, is_deactivate: bool, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1da82c605d..480757a491 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1036,6 +1036,10 @@ impl Item for Editor { f(ItemEvent::UpdateBreadcrumbs); } + EditorEvent::BreadcrumbsChanged => { + f(ItemEvent::UpdateBreadcrumbs); + } + EditorEvent::DirtyChanged => { f(ItemEvent::UpdateTab); } From 177cf12ca1f5a0811680b76e91477a18b7cc0a6b Mon Sep 17 00:00:00 2001 From: smit Date: Fri, 15 Aug 2025 16:24:54 +0530 Subject: [PATCH 06/30] project: Fix LSP TextDocumentSyncCapability dynamic registration (#36234) Closes #36213 Use `textDocument/didChange` ([docs](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization)) instead of `textDocument/synchronization`. Release Notes: - Fixed an issue where Dart projects were being formatted incorrectly by the language server. --- crates/project/src/lsp_store.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 827341d60d..3dd9dd6b8b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11813,14 +11813,16 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/synchronization" => { - if let Some(caps) = reg + "textDocument/didChange" => { + if let Some(sync_kind) = reg .register_options - .map(serde_json::from_value) + .and_then(|opts| opts.get("syncKind").cloned()) + .map(serde_json::from_value::) .transpose()? { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = Some(caps); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); }); notify_server_capabilities_updated(&server, cx); } @@ -11970,7 +11972,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/synchronization" => { + "textDocument/didChange" => { server.update_capabilities(|capabilities| { capabilities.text_document_sync = None; }); From e9a4f6767bbb1525751ef9571d8e603d0fd57d13 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 12:12:18 +0300 Subject: [PATCH 07/30] openai: Don't send reasoning_effort if it's not set (#36228) Release Notes: - N/A --- crates/open_ai/src/open_ai.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 5801f29623..8bbe858995 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -257,6 +257,7 @@ pub struct Request { pub tools: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub prompt_cache_key: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, } From b96f76f3771787ffcdd38fa45f7c0c10e12ddb10 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 13:54:24 +0300 Subject: [PATCH 08/30] openai: Don't send prompt_cache_key for OpenAI-compatible models (#36231) Some APIs fail when they get this parameter Closes #36215 Release Notes: - Fixed OpenAI-compatible providers that don't support prompt caching and/or reasoning --- crates/language_models/src/provider/cloud.rs | 1 + crates/language_models/src/provider/open_ai.rs | 8 +++++++- crates/language_models/src/provider/open_ai_compatible.rs | 5 ++++- crates/language_models/src/provider/vercel.rs | 1 + crates/language_models/src/provider/x_ai.rs | 1 + crates/open_ai/src/open_ai.rs | 7 +++++++ crates/vercel/src/vercel.rs | 4 ++++ crates/x_ai/src/x_ai.rs | 4 ++++ 8 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index ff8048040e..c1337399f9 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -941,6 +941,7 @@ impl LanguageModel for CloudLanguageModel { request, model.id(), model.supports_parallel_tool_calls(), + model.supports_prompt_cache_key(), None, None, ); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 725027b2a7..eaf8d885b3 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -370,6 +370,7 @@ impl LanguageModel for OpenAiLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), self.model.reasoning_effort(), ); @@ -386,6 +387,7 @@ pub fn into_open_ai( request: LanguageModelRequest, model_id: &str, supports_parallel_tool_calls: bool, + supports_prompt_cache_key: bool, max_output_tokens: Option, reasoning_effort: Option, ) -> open_ai::Request { @@ -477,7 +479,11 @@ pub fn into_open_ai( } else { None }, - prompt_cache_key: request.thread_id, + prompt_cache_key: if supports_prompt_cache_key { + request.thread_id + } else { + None + }, tools: request .tools .into_iter() diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 6e912765cd..5f546f5219 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -355,10 +355,13 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { + let supports_parallel_tool_call = true; + let supports_prompt_cache_key = false; let request = into_open_ai( request, &self.model.name, - true, + supports_parallel_tool_call, + supports_prompt_cache_key, self.max_output_tokens(), None, ); diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 57a89ba4aa..9f447cb68b 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -355,6 +355,7 @@ impl LanguageModel for VercelLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), None, ); diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 5e7190ea96..fed6fe92bf 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -359,6 +359,7 @@ impl LanguageModel for XAiLanguageModel { request, self.model.id(), self.model.supports_parallel_tool_calls(), + self.model.supports_prompt_cache_key(), self.max_output_tokens(), None, ); diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 8bbe858995..604e8fe622 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -236,6 +236,13 @@ impl Model { Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, } } + + /// Returns whether the given model supports the `prompt_cache_key` parameter. + /// + /// If the model does not support the parameter, do not pass it up. + pub fn supports_prompt_cache_key(&self) -> bool { + return true; + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/vercel/src/vercel.rs b/crates/vercel/src/vercel.rs index 1ae22c5fef..8686fda53f 100644 --- a/crates/vercel/src/vercel.rs +++ b/crates/vercel/src/vercel.rs @@ -71,4 +71,8 @@ impl Model { Model::Custom { .. } => false, } } + + pub fn supports_prompt_cache_key(&self) -> bool { + false + } } diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs index ac116b2f8f..23cd5b9320 100644 --- a/crates/x_ai/src/x_ai.rs +++ b/crates/x_ai/src/x_ai.rs @@ -105,6 +105,10 @@ impl Model { } } + pub fn supports_prompt_cache_key(&self) -> bool { + false + } + pub fn supports_tool(&self) -> bool { match self { Self::Grok2Vision From 2ab445dfd436530f387f58932dfcb31cb660a483 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Aug 2025 13:17:26 -0400 Subject: [PATCH 09/30] zed 0.200.2 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f78007d037..b084393512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20500,7 +20500,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.1" +version = "0.200.2" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 728f112e80..57388d0b5e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.1" +version = "0.200.2" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From 2bd61668dc8337544dedb841dcd00c88a30ae1b0 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 15 Aug 2025 16:59:57 -0500 Subject: [PATCH 10/30] keymap_ui: Don't try to parse empty action arguments as JSON (#36278) Closes #ISSUE Release Notes: - Keymap Editor: Fixed an issue where leaving the arguments field empty would result in an error even if arguments were optional --- crates/settings_ui/src/keybindings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index a62c669488..33e1687a27 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2154,6 +2154,7 @@ impl KeybindingEditorModal { let value = action_arguments .as_ref() + .filter(|args| !args.is_empty()) .map(|args| { serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) From c2f0df9b8ec771aa3b75dbe3e99b182b8ba7e362 Mon Sep 17 00:00:00 2001 From: Cale Sennett Date: Mon, 18 Aug 2025 03:36:52 -0500 Subject: [PATCH 11/30] Add capabilities to OpenAI-compatible model settings (#36370) ### TL;DR * Adds `capabilities` configuration for OpenAI-compatible models * Relates to https://github.com/zed-industries/zed/issues/36215#issuecomment-3193920491 ### Summary This PR introduces support for configuring model capabilities for OpenAI-compatible language models. The implementation addresses the issue that not all OpenAI-compatible APIs support the same features - for example, Cerebras' API explicitly does not support `parallel_tool_calls` as documented in their [OpenAI compatibility guide](https://inference-docs.cerebras.ai/resources/openai#currently-unsupported-openai-features). ### Changes 1. **Model Capabilities Structure**: - Added `ModelCapabilityToggles` struct for UI representation with boolean toggle states - Implemented proper parsing of capability toggles into `ModelCapabilities` 2. **UI Updates**: - Modified the "Add LLM Provider" modal to include checkboxes for each capability - Each OpenAI-compatible model can now be configured with its specific capabilities through the UI 3. **Configuration File Structure**: - Updated the settings schema to support a `capabilities` object for each `openai_compatible` model - Each capability (`tools`, `images`, `parallel_tool_calls`, `prompt_cache_key`) can be individually specified per model ### Example Configuration ```json { "openai_compatible": { "Cerebras": { "api_url": "https://api.cerebras.ai/v1", "available_models": [ { "name": "gpt-oss-120b", "max_tokens": 131000, "capabilities": { "tools": true, "images": false, "parallel_tool_calls": false, "prompt_cache_key": false } } ] } } } ``` ### Tests Added - Added tests to verify default capability values are correctly applied - Added tests to verify that deselected toggles are properly parsed as `false` - Added tests to verify that mixed capability selections work correctly Thanks to @osyvokon for the desired `capabilities` configuration structure! Release Notes: - OpenAI-compatible models now have configurable capabilities (#36370; thanks @calesennett) --------- Co-authored-by: Oleksiy Syvokon --- .../add_llm_provider_modal.rs | 168 +++++++++++++++++- .../src/provider/open_ai_compatible.rs | 35 +++- docs/src/ai/llm-providers.md | 17 +- 3 files changed, 208 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs index 401a633488..c68c9c2730 100644 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -7,10 +7,12 @@ use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, T use language_model::LanguageModelRegistry; use language_models::{ AllLanguageModelSettings, OpenAiCompatibleSettingsContent, - provider::open_ai_compatible::AvailableModel, + provider::open_ai_compatible::{AvailableModel, ModelCapabilities}, }; use settings::update_settings_file; -use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*}; +use ui::{ + Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*, +}; use ui_input::SingleLineInput; use workspace::{ModalView, Workspace}; @@ -69,11 +71,19 @@ impl AddLlmProviderInput { } } +struct ModelCapabilityToggles { + pub supports_tools: ToggleState, + pub supports_images: ToggleState, + pub supports_parallel_tool_calls: ToggleState, + pub supports_prompt_cache_key: ToggleState, +} + struct ModelInput { name: Entity, max_completion_tokens: Entity, max_output_tokens: Entity, max_tokens: Entity, + capabilities: ModelCapabilityToggles, } impl ModelInput { @@ -100,11 +110,23 @@ impl ModelInput { cx, ); let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx); + let ModelCapabilities { + tools, + images, + parallel_tool_calls, + prompt_cache_key, + } = ModelCapabilities::default(); Self { name: model_name, max_completion_tokens, max_output_tokens, max_tokens, + capabilities: ModelCapabilityToggles { + supports_tools: tools.into(), + supports_images: images.into(), + supports_parallel_tool_calls: parallel_tool_calls.into(), + supports_prompt_cache_key: prompt_cache_key.into(), + }, } } @@ -136,6 +158,12 @@ impl ModelInput { .text(cx) .parse::() .map_err(|_| SharedString::from("Max Tokens must be a number"))?, + capabilities: ModelCapabilities { + tools: self.capabilities.supports_tools.selected(), + images: self.capabilities.supports_images.selected(), + parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(), + prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(), + }, }) } } @@ -322,6 +350,55 @@ impl AddLlmProviderModal { .child(model.max_output_tokens.clone()), ) .child(model.max_tokens.clone()) + .child( + v_flex() + .gap_1() + .child( + Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools) + .label("Supports tools") + .on_click(cx.listener(move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_tools = *checked; + cx.notify(); + })), + ) + .child( + Checkbox::new(("supports-images", ix), model.capabilities.supports_images) + .label("Supports images") + .on_click(cx.listener(move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_images = *checked; + cx.notify(); + })), + ) + .child( + Checkbox::new( + ("supports-parallel-tool-calls", ix), + model.capabilities.supports_parallel_tool_calls, + ) + .label("Supports parallel_tool_calls") + .on_click(cx.listener( + move |this, checked, _window, cx| { + this.input.models[ix] + .capabilities + .supports_parallel_tool_calls = *checked; + cx.notify(); + }, + )), + ) + .child( + Checkbox::new( + ("supports-prompt-cache-key", ix), + model.capabilities.supports_prompt_cache_key, + ) + .label("Supports prompt_cache_key") + .on_click(cx.listener( + move |this, checked, _window, cx| { + this.input.models[ix].capabilities.supports_prompt_cache_key = + *checked; + cx.notify(); + }, + )), + ), + ) .when(has_more_than_one_model, |this| { this.child( Button::new(("remove-model", ix), "Remove Model") @@ -562,6 +639,93 @@ mod tests { ); } + #[gpui::test] + async fn test_model_input_default_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + assert_eq!( + model_input.capabilities.supports_tools, + ToggleState::Selected + ); + assert_eq!( + model_input.capabilities.supports_images, + ToggleState::Unselected + ); + assert_eq!( + model_input.capabilities.supports_parallel_tool_calls, + ToggleState::Unselected + ); + assert_eq!( + model_input.capabilities.supports_prompt_cache_key, + ToggleState::Unselected + ); + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.capabilities.tools, true); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + + #[gpui::test] + async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let mut model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + + model_input.capabilities.supports_tools = ToggleState::Unselected; + model_input.capabilities.supports_images = ToggleState::Unselected; + model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected; + model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.capabilities.tools, false); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, false); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + + #[gpui::test] + async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|window, cx| { + let mut model_input = ModelInput::new(window, cx); + model_input.name.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text("somemodel", window, cx); + }); + }); + + model_input.capabilities.supports_tools = ToggleState::Selected; + model_input.capabilities.supports_images = ToggleState::Unselected; + model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected; + model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; + + let parsed_model = model_input.parse(cx).unwrap(); + assert_eq!(parsed_model.name, "somemodel"); + assert_eq!(parsed_model.capabilities.tools, true); + assert_eq!(parsed_model.capabilities.images, false); + assert_eq!(parsed_model.capabilities.parallel_tool_calls, true); + assert_eq!(parsed_model.capabilities.prompt_cache_key, false); + }); + } + async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext { cx.update(|cx| { let store = SettingsStore::test(cx); diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index 5f546f5219..e2d3adb198 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -38,6 +38,27 @@ pub struct AvailableModel { pub max_tokens: u64, pub max_output_tokens: Option, pub max_completion_tokens: Option, + #[serde(default)] + pub capabilities: ModelCapabilities, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ModelCapabilities { + pub tools: bool, + pub images: bool, + pub parallel_tool_calls: bool, + pub prompt_cache_key: bool, +} + +impl Default for ModelCapabilities { + fn default() -> Self { + Self { + tools: true, + images: false, + parallel_tool_calls: false, + prompt_cache_key: false, + } + } } pub struct OpenAiCompatibleLanguageModelProvider { @@ -293,17 +314,17 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { } fn supports_tools(&self) -> bool { - true + self.model.capabilities.tools } fn supports_images(&self) -> bool { - false + self.model.capabilities.images } fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { match choice { - LanguageModelToolChoice::Auto => true, - LanguageModelToolChoice::Any => true, + LanguageModelToolChoice::Auto => self.model.capabilities.tools, + LanguageModelToolChoice::Any => self.model.capabilities.tools, LanguageModelToolChoice::None => true, } } @@ -355,13 +376,11 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { LanguageModelCompletionError, >, > { - let supports_parallel_tool_call = true; - let supports_prompt_cache_key = false; let request = into_open_ai( request, &self.model.name, - supports_parallel_tool_call, - supports_prompt_cache_key, + self.model.capabilities.parallel_tool_calls, + self.model.capabilities.prompt_cache_key, self.max_output_tokens(), None, ); diff --git a/docs/src/ai/llm-providers.md b/docs/src/ai/llm-providers.md index 21ff2a8a51..0976c5e851 100644 --- a/docs/src/ai/llm-providers.md +++ b/docs/src/ai/llm-providers.md @@ -427,7 +427,7 @@ Custom models will be listed in the model dropdown in the Agent Panel. Zed supports using [OpenAI compatible APIs](https://platform.openai.com/docs/api-reference/chat) by specifying a custom `api_url` and `available_models` for the OpenAI provider. This is useful for connecting to other hosted services (like Together AI, Anyscale, etc.) or local models. -You can add a custom, OpenAI-compatible model via either via the UI or by editing your `settings.json`. +You can add a custom, OpenAI-compatible model either via the UI or by editing your `settings.json`. To do it via the UI, go to the Agent Panel settings (`agent: open settings`) and look for the "Add Provider" button to the right of the "LLM Providers" section title. Then, fill up the input fields available in the modal. @@ -443,7 +443,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod { "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", "display_name": "Together Mixtral 8x7B", - "max_tokens": 32768 + "max_tokens": 32768, + "capabilities": { + "tools": true, + "images": false, + "parallel_tool_calls": false, + "prompt_cache_key": false + } } ] } @@ -451,6 +457,13 @@ To do it via your `settings.json`, add the following snippet under `language_mod } ``` +By default, OpenAI-compatible models inherit the following capabilities: + +- `tools`: true (supports tool/function calling) +- `images`: false (does not support image inputs) +- `parallel_tool_calls`: false (does not support `parallel_tool_calls` parameter) +- `prompt_cache_key`: false (does not support `prompt_cache_key` parameter) + Note that LLM API keys aren't stored in your settings file. So, ensure you have it set in your environment variables (`OPENAI_API_KEY=`) so your settings can pick it up. From 4a0e8f0844debcbe7833b970861ada1e9b1ad2ce Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sun, 17 Aug 2025 17:19:38 +0200 Subject: [PATCH 12/30] agent_ui: Ensure that all configuration views get rendered with full width (#36362) Closes #36097 Release Notes: - Fixed API key input fields getting shrunk in Agent Panel settings view on low panel widths paired with high UI font sizes. --- crates/agent_ui/src/agent_configuration.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 5f72fa58c8..7bea2c467d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -300,6 +300,7 @@ impl AgentConfiguration { ) .child( div() + .w_full() .px_2() .when(is_expanded, |parent| match configuration_view { Some(configuration_view) => parent.child(configuration_view), From e2dec8536502a3bee1aba278492bb799f029fc08 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 15 Aug 2025 18:37:24 +0300 Subject: [PATCH 13/30] agent: Create checkpoint before/after every edit operation (#36253) 1. Previously, checkpoints only appeared when an agent's edit happened immediately after a user message. This is rare (agent usually collects some context first), so they were almost never shown. This is now fixed. 2. After this change, a checkpoint is created after every edit operation. So when the agent edits files five times in a single dialog turn, we will now display five checkpoints. As a bonus, it's now possible to undo only a part of a long agent response. Closes #36092, #32917 Release Notes: - Create agent checkpoints more frequently (before every edit) --- crates/agent/src/thread.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 1d417efbba..f3f1088483 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -844,11 +844,17 @@ impl Thread { .await .unwrap_or(false); - if !equal { - this.update(cx, |this, cx| { - this.insert_checkpoint(pending_checkpoint, cx) - })?; - } + this.update(cx, |this, cx| { + this.pending_checkpoint = if equal { + Some(pending_checkpoint) + } else { + this.insert_checkpoint(pending_checkpoint, cx); + Some(ThreadCheckpoint { + message_id: this.next_message_id, + git_checkpoint: final_checkpoint, + }) + } + })?; Ok(()) } From 0367e93667219728b76dd4ef997918a68346a8d2 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 13 Aug 2025 22:31:28 +0200 Subject: [PATCH 14/30] onboarding: Fix minimap typo on editing page (#36143) This PR fixes a small typo on the onboarding editing page where it should be "Minimap" instead of "Mini Map" Release Notes: - N/A --- crates/onboarding/src/editing_page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index aa7f4eee74..d941a0315a 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -721,7 +721,7 @@ fn render_popular_settings_section( .items_start() .justify_between() .child( - v_flex().child(Label::new("Mini Map")).child( + v_flex().child(Label::new("Minimap")).child( Label::new("See a high-level overview of your source code.") .color(Color::Muted), ), From 78e56ce8fde8d0453b3ba268ec3773d58398de91 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 18 Aug 2025 13:01:32 +0200 Subject: [PATCH 15/30] keymap_ui: Ensure keybind with empty arguments can be saved (#36393) Follow up to #36278 to ensure this bug is actually fixed. Also fixes this on two layers and adds a test for the lower layer, as we cannot properly test it in the UI. Furthermore, this improves the error message to show some more context and ensures the status toast is actually only shown when the keybind was successfully updated: Before, we would show the success toast whilst also showing an error in the editor. Lastly, this also fixes some issues with the status toast (and animations) where no status toast or no animation would show in certain scenarios. Release Notes: - N/A --- crates/settings/src/keymap_file.rs | 24 +++++++- crates/settings_ui/src/keybindings.rs | 84 +++++++++++++-------------- crates/ui/src/styles/animation.rs | 27 +++++---- crates/workspace/src/toast_layer.rs | 32 +++++----- 4 files changed, 93 insertions(+), 74 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 7802671fec..fb03662290 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -928,14 +928,14 @@ impl<'a> KeybindUpdateTarget<'a> { } let action_name: Value = self.action_name.into(); let value = match self.action_arguments { - Some(args) => { + Some(args) if !args.is_empty() => { let args = serde_json::from_str::(args) .context("Failed to parse action arguments as JSON")?; serde_json::json!([action_name, args]) } - None => action_name, + _ => action_name, }; - return Ok(value); + Ok(value) } fn keystrokes_unparsed(&self) -> String { @@ -1084,6 +1084,24 @@ mod tests { .unindent(), ); + check_keymap_update( + "[]", + KeybindUpdateOperation::add(KeybindUpdateTarget { + keystrokes: &parse_keystrokes("ctrl-a"), + action_name: "zed::SomeAction", + context: None, + action_arguments: Some(""), + }), + r#"[ + { + "bindings": { + "ctrl-a": "zed::SomeAction" + } + } + ]"# + .unindent(), + ); + check_keymap_update( r#"[ { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 33e1687a27..af7910e46c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2150,11 +2150,11 @@ impl KeybindingEditorModal { let action_arguments = self .action_arguments_editor .as_ref() - .map(|editor| editor.read(cx).editor.read(cx).text(cx)); + .map(|arguments_editor| arguments_editor.read(cx).editor.read(cx).text(cx)) + .filter(|args| !args.is_empty()); let value = action_arguments .as_ref() - .filter(|args| !args.is_empty()) .map(|args| { serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) @@ -2262,29 +2262,11 @@ impl KeybindingEditorModal { let create = self.creating; - let status_toast = StatusToast::new( - format!( - "Saved edits to the {} action.", - &self.editing_keybind.action().humanized_name - ), - cx, - move |this, _cx| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - .dismiss_button(true) - // .action("Undo", f) todo: wire the undo functionality - }, - ); - - self.workspace - .update(cx, |workspace, cx| { - workspace.toggle_status_toast(status_toast, cx); - }) - .log_err(); - cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; + let humanized_action_name = existing_keybind.action().humanized_name.clone(); - if let Err(err) = save_keybinding_update( + match save_keybinding_update( create, existing_keybind, &action_mapping, @@ -2294,25 +2276,43 @@ impl KeybindingEditorModal { ) .await { - this.update(cx, |this, cx| { - this.set_error(InputError::error(err), cx); - }) - .log_err(); - } else { - this.update(cx, |this, cx| { - this.keymap_editor.update(cx, |keymap, cx| { - keymap.previous_edit = Some(PreviousEdit::Keybinding { - action_mapping, - action_name, - fallback: keymap - .table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), - }) - }); - cx.emit(DismissEvent); - }) - .ok(); + Ok(_) => { + this.update(cx, |this, cx| { + this.keymap_editor.update(cx, |keymap, cx| { + keymap.previous_edit = Some(PreviousEdit::Keybinding { + action_mapping, + action_name, + fallback: keymap + .table_interaction_state + .read(cx) + .get_scrollbar_offset(Axis::Vertical), + }); + let status_toast = StatusToast::new( + format!("Saved edits to the {} action.", humanized_action_name), + cx, + move |this, _cx| { + this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) + .dismiss_button(true) + // .action("Undo", f) todo: wire the undo functionality + }, + ); + + this.workspace + .update(cx, |workspace, cx| { + workspace.toggle_status_toast(status_toast, cx); + }) + .log_err(); + }); + cx.emit(DismissEvent); + }) + .ok(); + } + Err(err) => { + this.update(cx, |this, cx| { + this.set_error(InputError::error(err), cx); + }) + .log_err(); + } } }) .detach(); @@ -2984,7 +2984,7 @@ async fn save_keybinding_update( let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) - .context("Failed to update keybinding")?; + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs index ee5352d454..acea834548 100644 --- a/crates/ui/src/styles/animation.rs +++ b/crates/ui/src/styles/animation.rs @@ -31,7 +31,7 @@ pub enum AnimationDirection { FromTop, } -pub trait DefaultAnimations: Styled + Sized { +pub trait DefaultAnimations: Styled + Sized + Element { fn animate_in( self, animation_type: AnimationDirection, @@ -44,8 +44,13 @@ pub trait DefaultAnimations: Styled + Sized { AnimationDirection::FromTop => "animate_from_top", }; + let animation_id = self.id().map_or_else( + || ElementId::from(animation_name), + |id| (id, animation_name).into(), + ); + self.with_animation( - animation_name, + animation_id, gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()), move |mut this, delta| { let start_opacity = 0.4; @@ -91,7 +96,7 @@ pub trait DefaultAnimations: Styled + Sized { } } -impl DefaultAnimations for E {} +impl DefaultAnimations for E {} // Don't use this directly, it only exists to show animation previews #[derive(RegisterComponent)] @@ -132,7 +137,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::red()) - .animate_in(AnimationDirection::FromBottom, false), + .animate_in_from_bottom(false), ) .into_any_element(), ), @@ -151,7 +156,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::blue()) - .animate_in(AnimationDirection::FromTop, false), + .animate_in_from_top(false), ) .into_any_element(), ), @@ -170,7 +175,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::green()) - .animate_in(AnimationDirection::FromLeft, false), + .animate_in_from_left(false), ) .into_any_element(), ), @@ -189,7 +194,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::yellow()) - .animate_in(AnimationDirection::FromRight, false), + .animate_in_from_right(false), ) .into_any_element(), ), @@ -214,7 +219,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::red()) - .animate_in(AnimationDirection::FromBottom, true), + .animate_in_from_bottom(true), ) .into_any_element(), ), @@ -233,7 +238,7 @@ impl Component for Animation { .left(px(offset)) .rounded_md() .bg(gpui::blue()) - .animate_in(AnimationDirection::FromTop, true), + .animate_in_from_top(true), ) .into_any_element(), ), @@ -252,7 +257,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::green()) - .animate_in(AnimationDirection::FromLeft, true), + .animate_in_from_left(true), ) .into_any_element(), ), @@ -271,7 +276,7 @@ impl Component for Animation { .top(px(offset)) .rounded_md() .bg(gpui::yellow()) - .animate_in(AnimationDirection::FromRight, true), + .animate_in_from_right(true), ) .into_any_element(), ), diff --git a/crates/workspace/src/toast_layer.rs b/crates/workspace/src/toast_layer.rs index 28be3e7e47..5157945548 100644 --- a/crates/workspace/src/toast_layer.rs +++ b/crates/workspace/src/toast_layer.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use gpui::{AnyView, DismissEvent, Entity, FocusHandle, ManagedView, Subscription, Task}; +use gpui::{AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, Subscription, Task}; use ui::{animation::DefaultAnimations, prelude::*}; use zed_actions::toast; @@ -76,6 +76,7 @@ impl ToastViewHandle for Entity { } pub struct ActiveToast { + id: EntityId, toast: Box, action: Option, _subscriptions: [Subscription; 1], @@ -113,9 +114,9 @@ impl ToastLayer { V: ToastView, { if let Some(active_toast) = &self.active_toast { - let is_close = active_toast.toast.view().downcast::().is_ok(); - let did_close = self.hide_toast(cx); - if is_close || !did_close { + let show_new = active_toast.id != new_toast.entity_id(); + self.hide_toast(cx); + if !show_new { return; } } @@ -130,11 +131,12 @@ impl ToastLayer { let focus_handle = cx.focus_handle(); self.active_toast = Some(ActiveToast { - toast: Box::new(new_toast.clone()), - action, _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| { this.hide_toast(cx); })], + id: new_toast.entity_id(), + toast: Box::new(new_toast), + action, focus_handle, }); @@ -143,11 +145,9 @@ impl ToastLayer { cx.notify(); } - pub fn hide_toast(&mut self, cx: &mut Context) -> bool { + pub fn hide_toast(&mut self, cx: &mut Context) { self.active_toast.take(); cx.notify(); - - true } pub fn active_toast(&self) -> Option> @@ -218,11 +218,10 @@ impl Render for ToastLayer { let Some(active_toast) = &self.active_toast else { return div(); }; - let handle = cx.weak_entity(); div().absolute().size_full().bottom_0().left_0().child( v_flex() - .id("toast-layer-container") + .id(("toast-layer-container", active_toast.id)) .absolute() .w_full() .bottom(px(0.)) @@ -234,17 +233,14 @@ impl Render for ToastLayer { h_flex() .id("active-toast-container") .occlude() - .on_hover(move |hover_start, _window, cx| { - let Some(this) = handle.upgrade() else { - return; - }; + .on_hover(cx.listener(|this, hover_start, _window, cx| { if *hover_start { - this.update(cx, |this, _| this.pause_dismiss_timer()); + this.pause_dismiss_timer(); } else { - this.update(cx, |this, cx| this.restart_dismiss_timer(cx)); + this.restart_dismiss_timer(cx); } cx.stop_propagation(); - }) + })) .on_click(|_, _, cx| { cx.stop_propagation(); }) From e9e376deb591c7d8f82c76d217bd04b04bb61a6d Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sat, 16 Aug 2025 01:33:32 -0500 Subject: [PATCH 16/30] Separate minidump crashes from panics (#36267) The minidump-based crash reporting is now entirely separate from our legacy panic_hook-based reporting. This should improve the association of minidumps with their metadata and give us more consistent crash reports. Release Notes: - N/A --------- Co-authored-by: Max Brunsfeld --- Cargo.lock | 2 + crates/crashes/Cargo.toml | 2 + crates/crashes/src/crashes.rs | 157 +++++++++++++----- crates/proto/proto/app.proto | 6 +- crates/remote/src/ssh_session.rs | 30 ++-- crates/remote_server/src/unix.rs | 93 +++++------ crates/zed/src/main.rs | 11 +- crates/zed/src/reliability.rs | 262 ++++++++++++++----------------- 8 files changed, 315 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b084393512..ba112acb6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4065,6 +4065,8 @@ dependencies = [ "minidumper", "paths", "release_channel", + "serde", + "serde_json", "smol", "workspace-hack", ] diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index afb4936b63..2420b499f8 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -12,6 +12,8 @@ minidumper.workspace = true paths.workspace = true release_channel.workspace = true smol.workspace = true +serde.workspace = true +serde_json.workspace = true workspace-hack.workspace = true [lints] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 5b9ae0b546..ddf6468be8 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -2,15 +2,17 @@ use crash_handler::CrashHandler; use log::info; use minidumper::{Client, LoopAction, MinidumpBinary}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; +use serde::{Deserialize, Serialize}; use std::{ env, - fs::File, + fs::{self, File}, io, + panic::Location, path::{Path, PathBuf}, process::{self, Command}, sync::{ - LazyLock, OnceLock, + Arc, OnceLock, atomic::{AtomicBool, Ordering}, }, thread, @@ -18,19 +20,17 @@ use std::{ }; // set once the crash handler has initialized and the client has connected to it -pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); +pub static CRASH_HANDLER: OnceLock> = OnceLock::new(); // set when the first minidump request is made to avoid generating duplicate crash reports pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); -const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60); +const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60); +const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); -pub static GENERATE_MINIDUMPS: LazyLock = LazyLock::new(|| { - *RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok() -}); - -pub async fn init(id: String) { - if !*GENERATE_MINIDUMPS { +pub async fn init(crash_init: InitCrashHandler) { + if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { return; } + let exe = env::current_exe().expect("unable to find ourselves"); let zed_pid = process::id(); // TODO: we should be able to get away with using 1 crash-handler process per machine, @@ -61,9 +61,11 @@ pub async fn init(id: String) { smol::Timer::after(retry_frequency).await; } let client = maybe_client.unwrap(); - client.send_message(1, id).unwrap(); // set session id on the server + client + .send_message(1, serde_json::to_vec(&crash_init).unwrap()) + .unwrap(); - let client = std::sync::Arc::new(client); + let client = Arc::new(client); let handler = crash_handler::CrashHandler::attach(unsafe { let client = client.clone(); crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| { @@ -72,7 +74,6 @@ pub async fn init(id: String) { .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .is_ok() { - client.send_message(2, "mistakes were made").unwrap(); client.ping().unwrap(); client.request_dump(crash_context).is_ok() } else { @@ -87,7 +88,7 @@ pub async fn init(id: String) { { handler.set_ptracer(Some(server_pid)); } - CRASH_HANDLER.store(true, Ordering::Release); + CRASH_HANDLER.set(client.clone()).ok(); std::mem::forget(handler); info!("crash handler registered"); @@ -98,14 +99,43 @@ pub async fn init(id: String) { } pub struct CrashServer { - session_id: OnceLock, + initialization_params: OnceLock, + panic_info: OnceLock, + has_connection: Arc, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct CrashInfo { + pub init: InitCrashHandler, + pub panic: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct InitCrashHandler { + pub session_id: String, + pub zed_version: String, + pub release_channel: String, + pub commit_sha: String, + // pub gpu: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct CrashPanic { + pub message: String, + pub span: String, } impl minidumper::ServerHandler for CrashServer { fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> { - let err_message = "Need to send a message with the ID upon starting the crash handler"; + let err_message = "Missing initialization data"; let dump_path = paths::logs_dir() - .join(self.session_id.get().expect(err_message)) + .join( + &self + .initialization_params + .get() + .expect(err_message) + .session_id, + ) .with_extension("dmp"); let file = File::create(&dump_path)?; Ok((file, dump_path)) @@ -122,38 +152,71 @@ impl minidumper::ServerHandler for CrashServer { info!("failed to write minidump: {:#}", e); } } + + let crash_info = CrashInfo { + init: self + .initialization_params + .get() + .expect("not initialized") + .clone(), + panic: self.panic_info.get().cloned(), + }; + + let crash_data_path = paths::logs_dir() + .join(&crash_info.init.session_id) + .with_extension("json"); + + fs::write(crash_data_path, serde_json::to_vec(&crash_info).unwrap()).ok(); + LoopAction::Exit } fn on_message(&self, kind: u32, buffer: Vec) { - let message = String::from_utf8(buffer).expect("invalid utf-8"); - info!("kind: {kind}, message: {message}",); - if kind == 1 { - self.session_id - .set(message) - .expect("session id already initialized"); + match kind { + 1 => { + let init_data = + serde_json::from_slice::(&buffer).expect("invalid init data"); + self.initialization_params + .set(init_data) + .expect("already initialized"); + } + 2 => { + let panic_data = + serde_json::from_slice::(&buffer).expect("invalid panic data"); + self.panic_info.set(panic_data).expect("already panicked"); + } + _ => { + panic!("invalid message kind"); + } } } - fn on_client_disconnected(&self, clients: usize) -> LoopAction { - info!("client disconnected, {clients} remaining"); - if clients == 0 { - LoopAction::Exit - } else { - LoopAction::Continue - } + fn on_client_disconnected(&self, _clients: usize) -> LoopAction { + LoopAction::Exit + } + + fn on_client_connected(&self, _clients: usize) -> LoopAction { + self.has_connection.store(true, Ordering::SeqCst); + LoopAction::Continue } } -pub fn handle_panic() { - if !*GENERATE_MINIDUMPS { - return; - } +pub fn handle_panic(message: String, span: Option<&Location>) { + let span = span + .map(|loc| format!("{}:{}", loc.file(), loc.line())) + .unwrap_or_default(); + // wait 500ms for the crash handler process to start up // if it's still not there just write panic info and no minidump let retry_frequency = Duration::from_millis(100); for _ in 0..5 { - if CRASH_HANDLER.load(Ordering::Acquire) { + if let Some(client) = CRASH_HANDLER.get() { + client + .send_message( + 2, + serde_json::to_vec(&CrashPanic { message, span }).unwrap(), + ) + .ok(); log::error!("triggering a crash to generate a minidump..."); #[cfg(target_os = "linux")] CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); @@ -170,14 +233,30 @@ pub fn crash_server(socket: &Path) { log::info!("Couldn't create socket, there may already be a running crash server"); return; }; - let ab = AtomicBool::new(false); + + let shutdown = Arc::new(AtomicBool::new(false)); + let has_connection = Arc::new(AtomicBool::new(false)); + + std::thread::spawn({ + let shutdown = shutdown.clone(); + let has_connection = has_connection.clone(); + move || { + std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT); + if !has_connection.load(Ordering::SeqCst) { + shutdown.store(true, Ordering::SeqCst); + } + } + }); + server .run( Box::new(CrashServer { - session_id: OnceLock::new(), + initialization_params: OnceLock::new(), + panic_info: OnceLock::new(), + has_connection, }), - &ab, - Some(CRASH_HANDLER_TIMEOUT), + &shutdown, + Some(CRASH_HANDLER_PING_TIMEOUT), ) .expect("failed to run server"); } diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto index 353f19adb2..6b42ac9c40 100644 --- a/crates/proto/proto/app.proto +++ b/crates/proto/proto/app.proto @@ -84,11 +84,13 @@ message GetCrashFiles { message GetCrashFilesResponse { repeated CrashReport crashes = 1; + repeated string legacy_panics = 2; } message CrashReport { - optional string panic_contents = 1; - optional bytes minidump_contents = 2; + reserved 1, 2; + string metadata = 3; + bytes minidump_contents = 4; } message Extension { diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 4306251e44..d23b4f8cd6 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -1484,20 +1484,17 @@ impl RemoteConnection for SshRemoteConnection { identifier = &unique_identifier, ); - if let Some(rust_log) = std::env::var("RUST_LOG").ok() { - start_proxy_command = format!( - "RUST_LOG={} {}", - shlex::try_quote(&rust_log).unwrap(), - start_proxy_command - ) - } - if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() { - start_proxy_command = format!( - "RUST_BACKTRACE={} {}", - shlex::try_quote(&rust_backtrace).unwrap(), - start_proxy_command - ) + for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { + if let Some(value) = std::env::var(env_var).ok() { + start_proxy_command = format!( + "{}={} {} ", + env_var, + shlex::try_quote(&value).unwrap(), + start_proxy_command, + ); + } } + if reconnect { start_proxy_command.push_str(" --reconnect"); } @@ -2229,8 +2226,7 @@ impl SshRemoteConnection { #[cfg(not(target_os = "windows"))] { - run_cmd(Command::new("gzip").args(["-9", "-f", &bin_path.to_string_lossy()])) - .await?; + run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; } #[cfg(target_os = "windows")] { @@ -2462,7 +2458,7 @@ impl ChannelClient { }, async { smol::Timer::after(timeout).await; - anyhow::bail!("Timeout detected") + anyhow::bail!("Timed out resyncing remote client") }, ) .await @@ -2476,7 +2472,7 @@ impl ChannelClient { }, async { smol::Timer::after(timeout).await; - anyhow::bail!("Timeout detected") + anyhow::bail!("Timed out pinging remote client") }, ) .await diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index 9bb5645dc7..dc7fab8c3c 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -34,10 +34,10 @@ use smol::io::AsyncReadExt; use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; -use std::collections::HashMap; use std::ffi::OsStr; use std::ops::ControlFlow; use std::str::FromStr; +use std::sync::LazyLock; use std::{env, thread}; use std::{ io::Write, @@ -48,6 +48,13 @@ use std::{ use telemetry_events::LocationData; use util::ResultExt; +pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { + ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"), + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha") + } +}); + fn init_logging_proxy() { env_logger::builder() .format(|buf, record| { @@ -113,7 +120,6 @@ fn init_logging_server(log_file_path: PathBuf) -> Result>> { fn init_panic_hook(session_id: String) { std::panic::set_hook(Box::new(move |info| { - crashes::handle_panic(); let payload = info .payload() .downcast_ref::<&str>() @@ -121,6 +127,8 @@ fn init_panic_hook(session_id: String) { .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); + crashes::handle_panic(payload.clone(), info.location()); + let backtrace = backtrace::Backtrace::new(); let mut backtrace = backtrace .frames() @@ -150,14 +158,6 @@ fn init_panic_hook(session_id: String) { (&backtrace).join("\n") ); - let release_channel = *RELEASE_CHANNEL; - let version = match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"), - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha") - } - }; - let panic_data = telemetry_events::Panic { thread: thread_name.into(), payload: payload.clone(), @@ -165,9 +165,9 @@ fn init_panic_hook(session_id: String) { file: location.file().into(), line: location.line(), }), - app_version: format!("remote-server-{version}"), + app_version: format!("remote-server-{}", *VERSION), app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()), - release_channel: release_channel.dev_name().into(), + release_channel: RELEASE_CHANNEL.dev_name().into(), target: env!("TARGET").to_owned().into(), os_name: telemetry::os_name(), os_version: Some(telemetry::os_version()), @@ -204,8 +204,8 @@ fn handle_crash_files_requests(project: &Entity, client: &Arc, _cx| async move { + let mut legacy_panics = Vec::new(); let mut crashes = Vec::new(); - let mut minidumps_by_session_id = HashMap::new(); let mut children = smol::fs::read_dir(paths::logs_dir()).await?; while let Some(child) = children.next().await { let child = child?; @@ -227,41 +227,31 @@ fn handle_crash_files_requests(project: &Entity, client: &Arc Result<()> { let server_paths = ServerPaths::new(&identifier)?; let id = std::process::id().to_string(); - smol::spawn(crashes::init(id.clone())).detach(); + smol::spawn(crashes::init(crashes::InitCrashHandler { + session_id: id.clone(), + zed_version: VERSION.to_owned(), + release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), + commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), + })) + .detach(); init_panic_hook(id); log::info!("starting proxy process. PID: {}", std::process::id()); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index fd987ef6c5..2a82f81b5b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -8,6 +8,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME; use client::{Client, ProxySettings, UserStore, parse_zed_link}; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use crashes::InitCrashHandler; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use editor::Editor; use extension::ExtensionHostProxy; @@ -269,7 +270,15 @@ pub fn main() { let session = app.background_executor().block(Session::new()); app.background_executor() - .spawn(crashes::init(session_id.clone())) + .spawn(crashes::init(InitCrashHandler { + session_id: session_id.clone(), + zed_version: app_version.to_string(), + release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), + commit_sha: app_commit_sha + .as_ref() + .map(|sha| sha.full()) + .unwrap_or_else(|| "no sha".to_owned()), + })) .detach(); reliability::init_panic_hook( app_version, diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index fde44344b1..c27f4cb0a8 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -12,6 +12,7 @@ use gpui::{App, AppContext as _, SemanticVersion}; use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method}; use paths::{crashes_dir, crashes_retired_dir}; use project::Project; +use proto::{CrashReport, GetCrashFilesResponse}; use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel}; use reqwest::multipart::{Form, Part}; use settings::Settings; @@ -51,10 +52,6 @@ pub fn init_panic_hook( thread::yield_now(); } } - crashes::handle_panic(); - - let thread = thread::current(); - let thread_name = thread.name().unwrap_or(""); let payload = info .payload() @@ -63,6 +60,11 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); + crashes::handle_panic(payload.clone(), info.location()); + + let thread = thread::current(); + let thread_name = thread.name().unwrap_or(""); + if *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { let location = info.location().unwrap(); let backtrace = Backtrace::new(); @@ -214,45 +216,53 @@ pub fn init( let installation_id = installation_id.clone(); let system_id = system_id.clone(); - if let Some(ssh_client) = project.ssh_client() { - ssh_client.update(cx, |client, cx| { - if TelemetrySettings::get_global(cx).diagnostics { - let request = client.proto_client().request(proto::GetCrashFiles {}); - cx.background_spawn(async move { - let crash_files = request.await?; - for crash in crash_files.crashes { - let mut panic: Option = crash - .panic_contents - .and_then(|s| serde_json::from_str(&s).log_err()); + let Some(ssh_client) = project.ssh_client() else { + return; + }; + ssh_client.update(cx, |client, cx| { + if !TelemetrySettings::get_global(cx).diagnostics { + return; + } + let request = client.proto_client().request(proto::GetCrashFiles {}); + cx.background_spawn(async move { + let GetCrashFilesResponse { + legacy_panics, + crashes, + } = request.await?; - if let Some(panic) = panic.as_mut() { - panic.session_id = session_id.clone(); - panic.system_id = system_id.clone(); - panic.installation_id = installation_id.clone(); - } - - if let Some(minidump) = crash.minidump_contents { - upload_minidump( - http_client.clone(), - minidump.clone(), - panic.as_ref(), - ) - .await - .log_err(); - } - - if let Some(panic) = panic { - upload_panic(&http_client, &panic_report_url, panic, &mut None) - .await?; - } - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + for panic in legacy_panics { + if let Some(mut panic) = serde_json::from_str::(&panic).log_err() { + panic.session_id = session_id.clone(); + panic.system_id = system_id.clone(); + panic.installation_id = installation_id.clone(); + upload_panic(&http_client, &panic_report_url, panic, &mut None).await?; + } } + + let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else { + return Ok(()); + }; + for CrashReport { + metadata, + minidump_contents, + } in crashes + { + if let Some(metadata) = serde_json::from_str(&metadata).log_err() { + upload_minidump( + http_client.clone(), + endpoint, + minidump_contents, + &metadata, + ) + .await + .log_err(); + } + } + + anyhow::Ok(()) }) - } + .detach_and_log_err(cx); + }) }) .detach(); } @@ -466,16 +476,18 @@ fn upload_panics_and_crashes( installation_id: Option, cx: &App, ) { - let telemetry_settings = *client::TelemetrySettings::get_global(cx); + if !client::TelemetrySettings::get_global(cx).diagnostics { + return; + } cx.background_spawn(async move { - let most_recent_panic = - upload_previous_panics(http.clone(), &panic_report_url, telemetry_settings) - .await - .log_err() - .flatten(); - upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings) + upload_previous_minidumps(http.clone()).await.warn_on_err(); + let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url) .await .log_err() + .flatten(); + upload_previous_crashes(http, most_recent_panic, installation_id) + .await + .log_err(); }) .detach() } @@ -484,7 +496,6 @@ fn upload_panics_and_crashes( async fn upload_previous_panics( http: Arc, panic_report_url: &Url, - telemetry_settings: client::TelemetrySettings, ) -> anyhow::Result> { let mut children = smol::fs::read_dir(paths::logs_dir()).await?; @@ -507,58 +518,41 @@ async fn upload_previous_panics( continue; } - if telemetry_settings.diagnostics { - let panic_file_content = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; + let panic_file_content = smol::fs::read_to_string(&child_path) + .await + .context("error reading panic file")?; - let panic: Option = serde_json::from_str(&panic_file_content) - .log_err() - .or_else(|| { - panic_file_content - .lines() - .next() - .and_then(|line| serde_json::from_str(line).ok()) - }) - .unwrap_or_else(|| { - log::error!("failed to deserialize panic file {:?}", panic_file_content); - None - }); + let panic: Option = serde_json::from_str(&panic_file_content) + .log_err() + .or_else(|| { + panic_file_content + .lines() + .next() + .and_then(|line| serde_json::from_str(line).ok()) + }) + .unwrap_or_else(|| { + log::error!("failed to deserialize panic file {:?}", panic_file_content); + None + }); - if let Some(panic) = panic { - let minidump_path = paths::logs_dir() - .join(&panic.session_id) - .with_extension("dmp"); - if minidump_path.exists() { - let minidump = smol::fs::read(&minidump_path) - .await - .context("Failed to read minidump")?; - if upload_minidump(http.clone(), minidump, Some(&panic)) - .await - .log_err() - .is_some() - { - fs::remove_file(minidump_path).ok(); - } - } - - if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? { - continue; - } - } + if let Some(panic) = panic + && upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? + { + // We've done what we can, delete the file + fs::remove_file(child_path) + .context("error removing panic") + .log_err(); } - - // We've done what we can, delete the file - fs::remove_file(child_path) - .context("error removing panic") - .log_err(); } - if MINIDUMP_ENDPOINT.is_none() { - return Ok(most_recent_panic); - } + Ok(most_recent_panic) +} + +pub async fn upload_previous_minidumps(http: Arc) -> anyhow::Result<()> { + let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { + return Err(anyhow::anyhow!("Minidump endpoint not set")); + }; - // loop back over the directory again to upload any minidumps that are missing panics let mut children = smol::fs::read_dir(paths::logs_dir()).await?; while let Some(child) = children.next().await { let child = child?; @@ -566,33 +560,35 @@ async fn upload_previous_panics( if child_path.extension() != Some(OsStr::new("dmp")) { continue; } - if upload_minidump( - http.clone(), - smol::fs::read(&child_path) - .await - .context("Failed to read minidump")?, - None, - ) - .await - .log_err() - .is_some() - { - fs::remove_file(child_path).ok(); + let mut json_path = child_path.clone(); + json_path.set_extension("json"); + if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) { + if upload_minidump( + http.clone(), + &minidump_endpoint, + smol::fs::read(&child_path) + .await + .context("Failed to read minidump")?, + &metadata, + ) + .await + .log_err() + .is_some() + { + fs::remove_file(child_path).ok(); + fs::remove_file(json_path).ok(); + } } } - - Ok(most_recent_panic) + Ok(()) } async fn upload_minidump( http: Arc, + endpoint: &str, minidump: Vec, - panic: Option<&Panic>, + metadata: &crashes::CrashInfo, ) -> Result<()> { - let minidump_endpoint = MINIDUMP_ENDPOINT - .to_owned() - .ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?; - let mut form = Form::new() .part( "upload_file_minidump", @@ -600,38 +596,22 @@ async fn upload_minidump( .file_name("minidump.dmp") .mime_str("application/octet-stream")?, ) + .text( + "sentry[tags][channel]", + metadata.init.release_channel.clone(), + ) + .text("sentry[tags][version]", metadata.init.zed_version.clone()) + .text("sentry[release]", metadata.init.commit_sha.clone()) .text("platform", "rust"); - if let Some(panic) = panic { - form = form - .text("sentry[tags][channel]", panic.release_channel.clone()) - .text("sentry[tags][version]", panic.app_version.clone()) - .text("sentry[context][os][name]", panic.os_name.clone()) - .text( - "sentry[context][device][architecture]", - panic.architecture.clone(), - ) - .text("sentry[logentry][formatted]", panic.payload.clone()); - - if let Some(sha) = panic.app_commit_sha.clone() { - form = form.text("sentry[release]", sha) - } else { - form = form.text( - "sentry[release]", - format!("{}-{}", panic.release_channel, panic.app_version), - ) - } - if let Some(v) = panic.os_version.clone() { - form = form.text("sentry[context][os][release]", v); - } - if let Some(location) = panic.location_data.as_ref() { - form = form.text("span", format!("{}:{}", location.file, location.line)) - } + if let Some(panic_info) = metadata.panic.as_ref() { + form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); + form = form.text("span", panic_info.span.clone()); // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu // name, screen resolution, available ram, device model, etc } let mut response_text = String::new(); - let mut response = http.send_multipart_form(&minidump_endpoint, form).await?; + let mut response = http.send_multipart_form(endpoint, form).await?; response .body_mut() .read_to_string(&mut response_text) @@ -681,11 +661,7 @@ async fn upload_previous_crashes( http: Arc, most_recent_panic: Option<(i64, String)>, installation_id: Option, - telemetry_settings: client::TelemetrySettings, ) -> Result<()> { - if !telemetry_settings.diagnostics { - return Ok(()); - } let last_uploaded = KEY_VALUE_STORE .read_kvp(LAST_CRASH_UPLOADED)? .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. From 6c0eaf674ea1b1fb1e12d7a381a5534b61bb9dd0 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 18 Aug 2025 09:32:33 -0700 Subject: [PATCH 17/30] zed 0.200.3 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba112acb6f..9f6a5a981f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20502,7 +20502,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.2" +version = "0.200.3" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 57388d0b5e..ad0be6937f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.2" +version = "0.200.3" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From 66e6649aedcd903bf4a47351f12f17724610da1f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 18 Aug 2025 15:57:28 -0400 Subject: [PATCH 18/30] client: Parse auth callback query parameters before showing sign-in success page (#36440) This PR fixes an issue where we would redirect the user's browser to the sign-in success page even if the OAuth callback was malformed. We now parse the OAuth callback parameters from the query string and only redirect to the sign-in success page when they are valid. Release Notes: - Updated the sign-in flow to not show the sign-in success page prematurely. --- Cargo.lock | 1 + Cargo.toml | 1 + crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 24 +++++++++++++----------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f6a5a981f..373bcd9bf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3094,6 +3094,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "serde_urlencoded", "settings", "sha2", "smol", diff --git a/Cargo.toml b/Cargo.toml index 1baa6d3d74..0f87e6aa42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -585,6 +585,7 @@ serde_json_lenient = { version = "0.2", features = [ "raw_value", ] } serde_repr = "0.1" +serde_urlencoded = "0.7" sha2 = "0.10" shellexpand = "2.1.0" shlex = "1.3.0" diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 365625b445..5c6d1157fd 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -44,6 +44,7 @@ rpc = { workspace = true, features = ["gpui"] } schemars.workspace = true serde.workspace = true serde_json.workspace = true +serde_urlencoded.workspace = true settings.workspace = true sha2.workspace = true smol.workspace = true diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index f09c012a85..0f00471356 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1410,6 +1410,12 @@ impl Client { open_url_tx.send(url).log_err(); + #[derive(Deserialize)] + struct CallbackParams { + pub user_id: String, + pub access_token: String, + } + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted // access token from the query params. // @@ -1420,17 +1426,13 @@ impl Client { for _ in 0..100 { if let Some(req) = server.recv_timeout(Duration::from_secs(1))? { let path = req.url(); - let mut user_id = None; - let mut access_token = None; let url = Url::parse(&format!("http://example.com{}", path)) .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); - } - } + let callback_params: CallbackParams = + serde_urlencoded::from_str(url.query().unwrap_or_default()) + .context( + "failed to parse sign-in callback query parameters", + )?; let post_auth_url = http.build_url("/native_app_signin_succeeded"); @@ -1445,8 +1447,8 @@ impl Client { ) .context("failed to respond to login http request")?; return Ok(( - user_id.context("missing user_id parameter")?, - access_token.context("missing access_token parameter")?, + callback_params.user_id, + callback_params.access_token, )); } } From 3593691a05ccd7a501b4e2dc4e50c1098653adfd Mon Sep 17 00:00:00 2001 From: Zed Bot Date: Mon, 18 Aug 2025 20:48:00 +0000 Subject: [PATCH 19/30] Bump to 0.200.4 for @maxdeviant --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 373bcd9bf9..ad0f116511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20503,7 +20503,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.3" +version = "0.200.4" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ad0be6937f..678b3b36af 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.3" +version = "0.200.4" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From 7703cdb70be6eff58ee8a83ec62cc601ebbf3867 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 18 Aug 2025 08:16:17 +0530 Subject: [PATCH 20/30] gpui: Fix crash when starting Zed on macOS during texture creation (#36382) Closes #36229 Fix zero-sized texture creation that triggers a SIGABRT in the Metal renderer. Not sure why this happens yet, but it likely occurs when `native_window.contentView()` returns a zero `NSSize` during initial window creation, before the view size is computed. Release Notes: - Fixed a rare startup crash on macOS. --- crates/gpui/src/platform/mac/metal_renderer.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 629654014d..a686d8c45b 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -314,6 +314,15 @@ impl MetalRenderer { } fn update_path_intermediate_textures(&mut self, size: Size) { + // We are uncertain when this happens, but sometimes size can be 0 here. Most likely before + // the layout pass on window creation. Zero-sized texture creation causes SIGABRT. + // https://github.com/zed-industries/zed/issues/36229 + if size.width.0 <= 0 || size.height.0 <= 0 { + self.path_intermediate_texture = None; + self.path_intermediate_msaa_texture = None; + return; + } + let texture_descriptor = metal::TextureDescriptor::new(); texture_descriptor.set_width(size.width.0 as u64); texture_descriptor.set_height(size.height.0 as u64); From 1adbbfc6f4cd8e93b02b079676038cf3d47046cd Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 18 Aug 2025 16:32:01 +0530 Subject: [PATCH 21/30] editor: Fix panic in inlay hint while padding (#36405) Closes #36247 Fix a panic when padding inlay hints if the last character is a multi-byte character. Regressed in https://github.com/zed-industries/zed/pull/35786. Release Notes: - Fixed a crash that could occur when an inlay hint ended with `...`. --- crates/editor/src/display_map/inlay_map.rs | 25 +++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index b296b3e62a..76148af587 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -48,7 +48,7 @@ pub struct Inlay { impl Inlay { pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self { let mut text = hint.text(); - if hint.padding_right && text.chars_at(text.len().saturating_sub(1)).next() != Some(' ') { + if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { text.push(" "); } if hint.padding_left && text.chars_at(0).next() != Some(' ') { @@ -1305,6 +1305,29 @@ mod tests { ); } + #[gpui::test] + fn test_inlay_hint_padding_with_multibyte_chars() { + assert_eq!( + Inlay::hint( + 0, + Anchor::min(), + &InlayHint { + label: InlayHintLabel::String("🎨".to_string()), + position: text::Anchor::default(), + padding_left: true, + padding_right: true, + tooltip: None, + kind: None, + resolve_state: ResolveState::Resolved, + }, + ) + .text + .to_string(), + " 🎨 ", + "Should pad single emoji correctly" + ); + } + #[gpui::test] fn test_basic_inlays(cx: &mut App) { let buffer = MultiBuffer::build_simple("abcdefghi", cx); From 320d706576e5a14fa697c09bfbb6c195bc2774d5 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 19 Aug 2025 02:27:40 +0530 Subject: [PATCH 22/30] project: Handle `textDocument/didSave` and `textDocument/didChange` (un)registration and usage correctly (#36441) Follow-up of https://github.com/zed-industries/zed/pull/35306 This PR contains two changes: Both changes are inspired from: https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/textSynchronization.ts 1. Handling `textDocument/didSave` and `textDocument/didChange` registration and unregistration correctly: ```rs #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum TextDocumentSyncCapability { Kind(TextDocumentSyncKind), Options(TextDocumentSyncOptions), } ``` - `textDocument/didSave` dynamic registration contains "includeText" - `textDocument/didChange` dynamic registration contains "syncKind" While storing this to Language Server, we use `TextDocumentSyncCapability::Options` instead of `TextDocumentSyncCapability::Kind` since it also include [change](https://github.com/gluon-lang/lsp-types/blob/be7336e92a6ad23f214df19bcdceab17f39531a9/src/lib.rs#L1714-L1717) field as `TextDocumentSyncCapability::Kind` as well as [save](https://github.com/gluon-lang/lsp-types/blob/be7336e92a6ad23f214df19bcdceab17f39531a9/src/lib.rs#L1727-L1729) field as `TextDocumentSyncSaveOptions`. This way while registering or unregistering both of them, we don't accidentaly mess with other data. So, if at intialization we end up getting `TextDocumentSyncCapability::Kind` and we receive any above kind of dynamic registration, we change `TextDocumentSyncCapability::Kind` to `TextDocumentSyncCapability::Options` so we can store more data anyway. 2. Modify `include_text` method to only depend on `TextDocumentSyncSaveOptions`, instead of depending on `TextDocumentSyncKind`. Idea behind this is, `TextDocumentSyncSaveOptions` should be responsible for "textDocument/didSave" notification, and `TextDocumentSyncKind` should be responsible for "textDocument/didChange", which it already is: https://github.com/zed-industries/zed/blob/4b79eade1da2f5f7dfa18208cf882c8e6ca8a97f/crates/project/src/lsp_store.rs#L7324-L7331 Release Notes: - N/A --- crates/project/src/lsp_store.rs | 72 +++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3dd9dd6b8b..f16a2b7e23 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11821,8 +11821,28 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + } + "textDocument/didSave" => { + if let Some(save_options) = reg + .register_options + .and_then(|opts| opts.get("includeText").cloned()) + .map(serde_json::from_value::) + .transpose()? + { + server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.save = Some(save_options); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11974,7 +11994,19 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = None; + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.change = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/didSave" => { + server.update_capabilities(|capabilities| { + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.save = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -12002,6 +12034,20 @@ impl LspStore { Ok(()) } + + fn take_text_document_sync_options( + capabilities: &mut lsp::ServerCapabilities, + ) -> lsp::TextDocumentSyncOptions { + match capabilities.text_document_sync.take() { + Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { + let mut sync_options = lsp::TextDocumentSyncOptions::default(); + sync_options.change = Some(sync_kind); + sync_options + } + None => lsp::TextDocumentSyncOptions::default(), + } + } } // Registration with empty capabilities should be ignored. @@ -13104,24 +13150,18 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { - lsp::TextDocumentSyncKind::NONE => None, - lsp::TextDocumentSyncKind::FULL => Some(true), - lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), - _ => None, - }, - lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { - lsp::TextDocumentSyncSaveOptions::Supported(supported) => { - if *supported { - Some(true) - } else { - None - } - } + lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + // Server wants didSave but didn't specify includeText. + lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), + // Server doesn't want didSave at all. + lsp::TextDocumentSyncSaveOptions::Supported(false) => None, + // Server provided SaveOptions. lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, + // We do not have any save info. Kind affects didChange only. + lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 76afc0d67e2022c300a627cc0646f0e0b2ef3759 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 19 Aug 2025 14:32:26 +0200 Subject: [PATCH 23/30] Revert "project: Handle `textDocument/didSave` and `textDocument/didChange` (un)registration and usage correctly (#36441)" (#36480) This reverts commit c5991e74bb6f305c299684dc7ac3f6ee9055efcd. This PR broke rust-analyzer's check on save function, so reverting for now Release Notes: - N/A --- crates/lsp/src/lsp.rs | 2 +- crates/project/src/lsp_store.rs | 72 ++++++++------------------------- 2 files changed, 17 insertions(+), 57 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 22a227c231..76d2106817 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -827,7 +827,7 @@ impl LanguageServer { }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(true), + dynamic_registration: Some(false), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index f16a2b7e23..3dd9dd6b8b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -11821,28 +11821,8 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didSave" => { - if let Some(save_options) = reg - .register_options - .and_then(|opts| opts.get("includeText").cloned()) - .map(serde_json::from_value::) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.save = Some(save_options); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); }); notify_server_capabilities_updated(&server, cx); } @@ -11994,19 +11974,7 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.change = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didSave" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.save = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + capabilities.text_document_sync = None; }); notify_server_capabilities_updated(&server, cx); } @@ -12034,20 +12002,6 @@ impl LspStore { Ok(()) } - - fn take_text_document_sync_options( - capabilities: &mut lsp::ServerCapabilities, - ) -> lsp::TextDocumentSyncOptions { - match capabilities.text_document_sync.take() { - Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { - let mut sync_options = lsp::TextDocumentSyncOptions::default(); - sync_options.change = Some(sync_kind); - sync_options - } - None => lsp::TextDocumentSyncOptions::default(), - } - } } // Registration with empty capabilities should be ignored. @@ -13150,18 +13104,24 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { - // Server wants didSave but didn't specify includeText. - lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), - // Server doesn't want didSave at all. - lsp::TextDocumentSyncSaveOptions::Supported(false) => None, - // Server provided SaveOptions. + lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { + lsp::TextDocumentSyncKind::NONE => None, + lsp::TextDocumentSyncKind::FULL => Some(true), + lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), + _ => None, + }, + lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { + lsp::TextDocumentSyncSaveOptions::Supported(supported) => { + if *supported { + Some(true) + } else { + None + } + } lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, - // We do not have any save info. Kind affects didChange only. - lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 6f69698257587f740092380e01071cc0fbe3239b Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 19 Aug 2025 19:04:48 +0530 Subject: [PATCH 24/30] project: Take 2 on Handle textDocument/didSave and textDocument/didChange (un)registration and usage correctly (#36485) Relands https://github.com/zed-industries/zed/pull/36441 with a deserialization fix. Previously, deserializing `"includeText"` into `lsp::TextDocumentSyncSaveOptions` resulted in a `Supported(false)` type instead of `SaveOptions(SaveOptions { include_text: Option })`. ```rs impl From for TextDocumentSyncSaveOptions { fn from(from: bool) -> Self { Self::Supported(from) } } ``` Looks like, while dynamic registartion we only get `SaveOptions` type and never `Supported` type. (https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentSaveRegistrationOptions) Release Notes: - N/A --------- Co-authored-by: Lukas Wirth --- crates/project/src/lsp_store.rs | 88 ++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 3dd9dd6b8b..349c46e348 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -63,8 +63,8 @@ use lsp::{ FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, - WorkspaceFolder, notification::DidRenameFiles, + OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, + WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -11821,8 +11821,40 @@ impl LspStore { .transpose()? { server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.change = Some(sync_kind); capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)); + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + } + "textDocument/didSave" => { + if let Some(include_text) = reg + .register_options + .map(|opts| { + let transpose = opts + .get("includeText") + .cloned() + .map(serde_json::from_value::>) + .transpose(); + match transpose { + Ok(value) => Ok(value.flatten()), + Err(e) => Err(e), + } + }) + .transpose()? + { + server.update_capabilities(|capabilities| { + let mut sync_options = + Self::take_text_document_sync_options(capabilities); + sync_options.save = + Some(TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { + include_text, + })); + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -11974,7 +12006,19 @@ impl LspStore { } "textDocument/didChange" => { server.update_capabilities(|capabilities| { - capabilities.text_document_sync = None; + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.change = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); + }); + notify_server_capabilities_updated(&server, cx); + } + "textDocument/didSave" => { + server.update_capabilities(|capabilities| { + let mut sync_options = Self::take_text_document_sync_options(capabilities); + sync_options.save = None; + capabilities.text_document_sync = + Some(lsp::TextDocumentSyncCapability::Options(sync_options)); }); notify_server_capabilities_updated(&server, cx); } @@ -12002,6 +12046,20 @@ impl LspStore { Ok(()) } + + fn take_text_document_sync_options( + capabilities: &mut lsp::ServerCapabilities, + ) -> lsp::TextDocumentSyncOptions { + match capabilities.text_document_sync.take() { + Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, + Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { + let mut sync_options = lsp::TextDocumentSyncOptions::default(); + sync_options.change = Some(sync_kind); + sync_options + } + None => lsp::TextDocumentSyncOptions::default(), + } + } } // Registration with empty capabilities should be ignored. @@ -13104,24 +13162,18 @@ async fn populate_labels_for_symbols( fn include_text(server: &lsp::LanguageServer) -> Option { match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Kind(kind) => match *kind { - lsp::TextDocumentSyncKind::NONE => None, - lsp::TextDocumentSyncKind::FULL => Some(true), - lsp::TextDocumentSyncKind::INCREMENTAL => Some(false), - _ => None, - }, - lsp::TextDocumentSyncCapability::Options(options) => match options.save.as_ref()? { - lsp::TextDocumentSyncSaveOptions::Supported(supported) => { - if *supported { - Some(true) - } else { - None - } - } + lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { + // Server wants didSave but didn't specify includeText. + lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), + // Server doesn't want didSave at all. + lsp::TextDocumentSyncSaveOptions::Supported(false) => None, + // Server provided SaveOptions. lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { Some(save_options.include_text.unwrap_or(false)) } }, + // We do not have any save info. Kind affects didChange only. + lsp::TextDocumentSyncCapability::Kind(_) => None, } } From 27156279bb890b796e9126ea16ec7ac55e4b4eb3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 19 Aug 2025 20:30:25 +0530 Subject: [PATCH 25/30] lsp: Enable dynamic registration for TextDocumentSyncClientCapabilities post revert (#36494) Follow up: https://github.com/zed-industries/zed/pull/36485 Release Notes: - N/A --- crates/lsp/src/lsp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 76d2106817..22a227c231 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -827,7 +827,7 @@ impl LanguageServer { }), synchronization: Some(TextDocumentSyncClientCapabilities { did_save: Some(true), - dynamic_registration: Some(false), + dynamic_registration: Some(true), ..TextDocumentSyncClientCapabilities::default() }), code_lens: Some(CodeLensClientCapabilities { From e22c7592d9d6d3fc2522c78258f079a2e1b9d8d8 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 20 Aug 2025 12:20:09 +0530 Subject: [PATCH 26/30] project: Register dynamic capabilities even when registerOptions doesn't exist (#36554) Closes #36482 Looks like we accidentally referenced [common/formatting.ts#L67-L70](https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70) instead of [common/client.ts#L2133](https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133). Release Notes: - Fixed code not formatting on save in language servers like Biome. (Preview Only) --- crates/project/src/lsp_store.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 349c46e348..47e26bec68 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12062,16 +12062,15 @@ impl LspStore { } } -// Registration with empty capabilities should be ignored. -// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/formatting.ts#L67-L70 +// Registration with registerOptions as null, should fallback to true. +// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, ) -> anyhow::Result>> { - Ok(reg - .register_options - .map(|options| serde_json::from_value::(options)) - .transpose()? - .map(OneOf::Right)) + Ok(match reg.register_options { + Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), + None => Some(OneOf::Left(true)), + }) } fn subscribe_to_binary_statuses( From 55ffbb43c8cdec2d923954fddf4a75cc771c26e2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 20 Aug 2025 15:13:52 -0400 Subject: [PATCH 27/30] v0.200.x stable --- crates/zed/RELEASE_CHANNEL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 4de2f126df..870bbe4e50 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -preview \ No newline at end of file +stable \ No newline at end of file From 608495ec2f0c1febd4ace5b398925f76bac4b971 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Thu, 21 Aug 2025 12:19:57 -0500 Subject: [PATCH 28/30] Use Tokio::spawn instead of getting an executor handle (#36701) This was causing panics due to the handles being dropped out of order. It doesn't seem possible to guarantee the correct drop ordering given that we're holding them over await points, so lets just spawn on the tokio executor itself which gives us access to the state we needed those handles for in the first place. Fixes: ZED-1R Release Notes: - N/A Co-authored-by: Conrad Irwin Co-authored-by: Marshall Bowers --- Cargo.lock | 1 + crates/client/src/client.rs | 24 ++++++++++--------- .../cloud_api_client/src/cloud_api_client.rs | 8 +------ crates/gpui_tokio/Cargo.toml | 1 + crates/gpui_tokio/src/gpui_tokio.rs | 22 +++++++++++++++++ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad0f116511..bef752a9a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7559,6 +7559,7 @@ dependencies = [ name = "gpui_tokio" version = "0.1.0" dependencies = [ + "anyhow", "gpui", "tokio", "util", diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 0f00471356..5ffc04a0b0 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1284,19 +1284,21 @@ impl Client { "http" => Http, _ => Err(anyhow!("invalid rpc url: {}", rpc_url))?, }; - let rpc_host = rpc_url - .host_str() - .zip(rpc_url.port_or_known_default()) - .context("missing host in rpc url")?; - let stream = { - let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap(); - let _guard = handle.enter(); - match proxy { - Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, - None => Box::new(TcpStream::connect(rpc_host).await?), + let stream = gpui_tokio::Tokio::spawn_result(cx, { + let rpc_url = rpc_url.clone(); + async move { + let rpc_host = rpc_url + .host_str() + .zip(rpc_url.port_or_known_default()) + .context("missing host in rpc url")?; + Ok(match proxy { + Some(proxy) => connect_proxy_stream(&proxy, rpc_host).await?, + None => Box::new(TcpStream::connect(rpc_host).await?), + }) } - }; + })? + .await?; log::info!("connected to rpc endpoint {}", rpc_url); diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index ef9a1a9a55..055d4560fb 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -102,13 +102,7 @@ impl CloudApiClient { let credentials = credentials.as_ref().context("no credentials provided")?; let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token); - Ok(cx.spawn(async move |cx| { - let handle = cx - .update(|cx| Tokio::handle(cx)) - .ok() - .context("failed to get Tokio handle")?; - let _guard = handle.enter(); - + Ok(Tokio::spawn_result(cx, async move { let ws = WebSocket::connect(connect_url) .with_request( request::Builder::new() diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml index 46d5eafd5a..2d4abf4063 100644 --- a/crates/gpui_tokio/Cargo.toml +++ b/crates/gpui_tokio/Cargo.toml @@ -13,6 +13,7 @@ path = "src/gpui_tokio.rs" doctest = false [dependencies] +anyhow.workspace = true util.workspace = true gpui.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs index fffe18a616..8384f2a88e 100644 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ b/crates/gpui_tokio/src/gpui_tokio.rs @@ -52,6 +52,28 @@ impl Tokio { }) } + /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task + /// Note that the Tokio task will be cancelled if the GPUI task is dropped + pub fn spawn_result(cx: &C, f: Fut) -> C::Result>> + where + C: AppContext, + Fut: Future> + Send + 'static, + R: Send + 'static, + { + cx.read_global(|tokio: &GlobalTokio, cx| { + let join_handle = tokio.runtime.spawn(f); + let abort_handle = join_handle.abort_handle(); + let cancel = defer(move || { + abort_handle.abort(); + }); + cx.background_spawn(async move { + let result = join_handle.await?; + drop(cancel); + result + }) + }) + } + pub fn handle(cx: &App) -> tokio::runtime::Handle { GlobalTokio::global(cx).runtime.handle().clone() } From f74281b2ecc06a2d70e0e9646762e987b9ee67cd Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Fri, 22 Aug 2025 19:03:47 +0300 Subject: [PATCH 29/30] themes: Implement Bright Black and Bright White colors (#36761) Before: image After: image Release Notes: - Fixed ANSI Bright Black and Bright White colors --- assets/themes/ayu/ayu.json | 6 +++--- assets/themes/gruvbox/gruvbox.json | 12 ++++++------ assets/themes/one/one.json | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/themes/ayu/ayu.json b/assets/themes/ayu/ayu.json index f9f8720729..0ffbb9f61e 100644 --- a/assets/themes/ayu/ayu.json +++ b/assets/themes/ayu/ayu.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#bfbdb6ff", - "terminal.ansi.bright_white": "#bfbdb6ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#787876ff", "link_text.hover": "#5ac1feff", "conflict": "#feb454ff", @@ -479,7 +479,7 @@ "terminal.ansi.bright_cyan": "#ace0cbff", "terminal.ansi.dim_cyan": "#2a5f4aff", "terminal.ansi.white": "#fcfcfcff", - "terminal.ansi.bright_white": "#fcfcfcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#bcbec0ff", "link_text.hover": "#3b9ee5ff", "conflict": "#f1ad49ff", @@ -865,7 +865,7 @@ "terminal.ansi.bright_cyan": "#4c806fff", "terminal.ansi.dim_cyan": "#cbf2e4ff", "terminal.ansi.white": "#cccac2ff", - "terminal.ansi.bright_white": "#cccac2ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#898a8aff", "link_text.hover": "#72cffeff", "conflict": "#fecf72ff", diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 459825c733..f0f0358b76 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -94,7 +94,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -494,7 +494,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -894,7 +894,7 @@ "terminal.ansi.bright_cyan": "#45603eff", "terminal.ansi.dim_cyan": "#c7dfbdff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#83a598ff", "version_control.added": "#b7bb26ff", @@ -1294,7 +1294,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#fbf1c7ff", - "terminal.ansi.bright_white": "#fbf1c7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -1694,7 +1694,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f9f5d7ff", - "terminal.ansi.bright_white": "#f9f5d7ff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", @@ -2094,7 +2094,7 @@ "terminal.ansi.bright_cyan": "#9fbca8ff", "terminal.ansi.dim_cyan": "#253e2eff", "terminal.ansi.white": "#f2e5bcff", - "terminal.ansi.bright_white": "#f2e5bcff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#b0a189ff", "link_text.hover": "#0b6678ff", "version_control.added": "#797410ff", diff --git a/assets/themes/one/one.json b/assets/themes/one/one.json index 23ebbcc67e..33f6d3c622 100644 --- a/assets/themes/one/one.json +++ b/assets/themes/one/one.json @@ -93,7 +93,7 @@ "terminal.ansi.bright_cyan": "#3a565bff", "terminal.ansi.dim_cyan": "#b9d9dfff", "terminal.ansi.white": "#dce0e5ff", - "terminal.ansi.bright_white": "#dce0e5ff", + "terminal.ansi.bright_white": "#fafafaff", "terminal.ansi.dim_white": "#575d65ff", "link_text.hover": "#74ade8ff", "version_control.added": "#27a657ff", @@ -468,7 +468,7 @@ "terminal.bright_foreground": "#242529ff", "terminal.dim_foreground": "#fafafaff", "terminal.ansi.black": "#242529ff", - "terminal.ansi.bright_black": "#242529ff", + "terminal.ansi.bright_black": "#747579ff", "terminal.ansi.dim_black": "#97979aff", "terminal.ansi.red": "#d36151ff", "terminal.ansi.bright_red": "#f0b0a4ff", @@ -489,7 +489,7 @@ "terminal.ansi.bright_cyan": "#a3bedaff", "terminal.ansi.dim_cyan": "#254058ff", "terminal.ansi.white": "#fafafaff", - "terminal.ansi.bright_white": "#fafafaff", + "terminal.ansi.bright_white": "#ffffffff", "terminal.ansi.dim_white": "#aaaaaaff", "link_text.hover": "#5c78e2ff", "version_control.added": "#27a657ff", From 537f6781a9dd6d453a3e132e10528982fd299cb5 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 22 Aug 2025 13:13:33 -0400 Subject: [PATCH 30/30] zed 0.200.5 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bef752a9a3..8999c99b1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20504,7 +20504,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.200.4" +version = "0.200.5" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 678b3b36af..adcfa56439 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.200.4" +version = "0.200.5" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "]