From 73e028c01c4ee203613255b6da8609f3e30f1fca Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Fri, 17 Oct 2025 18:48:00 +0200 Subject: [PATCH 001/202] dap: Allow user to pass custom envs to adapter via project settings (#40490) It is now possible to configure logging level of CodeLLDB adapter via envs specified in project settings like so: ``` { "dap": { "CodeLLDB": { "envs": { "RUST_LOG": "debug" } } } } ``` Release Notes: - N/A --- Cargo.lock | 1 + assets/settings/default.json | 5 +++++ crates/dap/src/adapters.rs | 2 ++ crates/dap_adapters/src/codelldb.rs | 9 +++------ crates/dap_adapters/src/gdb.rs | 6 ++++-- crates/dap_adapters/src/go.rs | 3 ++- crates/dap_adapters/src/javascript.rs | 19 ++++++++++++++----- crates/dap_adapters/src/python.rs | 18 ++++++++++++++---- crates/debug_adapter_extension/Cargo.toml | 1 + .../src/extension_dap_adapter.rs | 3 +++ crates/project/src/debugger/dap_store.rs | 10 +++++++++- crates/project/src/project_settings.rs | 2 ++ .../settings/src/settings_content/project.rs | 2 ++ 13 files changed, 62 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a273b97345e64e7a5e6e8cbdd69e35148d06055..43c9c672eb83da9b02f6d885508189b55c5f0080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4852,6 +4852,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "collections", "dap", "extension", "gpui", diff --git a/assets/settings/default.json b/assets/settings/default.json index f1eae6c6b47d095f1952e7acfb6a5ae5c885302e..d4a23586d82fac46a2ec78ef5cfeb53de2e589ce 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1925,6 +1925,11 @@ // DAP Specific settings. "dap": { // Specify the DAP name as a key here. + "CodeLLDB": { + "env": { + "RUST_LOG": "info" + } + } }, // Common language server settings. "global_lsp_settings": { diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 6d1b89ef99920ecdd7bffedc643ade878294a6a3..cefb6dad674a5c6316ccfb7b043c97965bfa48d7 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -356,6 +356,7 @@ pub trait DebugAdapter: 'static + Send + Sync { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, cx: &mut AsyncApp, ) -> Result; @@ -455,6 +456,7 @@ impl DebugAdapter for FakeAdapter { task_definition: &DebugTaskDefinition, _: Option, _: Option>, + _: Option>, _: &mut AsyncApp, ) -> Result { let connection = task_definition diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index da5703d5791cc1e8d4086abdbf5366dbe2b80122..05aca2225aa9f0fd2a7fb4c5c1f213372f6ce899 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -2,6 +2,7 @@ use std::{path::PathBuf, sync::OnceLock}; use anyhow::{Context as _, Result}; use async_trait::async_trait; +use collections::HashMap; use dap::adapters::{DebugTaskDefinition, latest_github_release}; use futures::StreamExt; use gpui::AsyncApp; @@ -329,6 +330,7 @@ impl DebugAdapter for CodeLldbDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, _: &mut AsyncApp, ) -> Result { let mut command = user_installed_path @@ -378,11 +380,6 @@ impl DebugAdapter for CodeLldbDebugAdapter { }; let mut json_config = config.config.clone(); - // Enable info level for CodeLLDB by default. - // Logs can then be viewed in our DAP logs. - let mut envs = collections::HashMap::default(); - envs.insert("RUST_LOG".to_string(), "info".to_string()); - Ok(DebugAdapterBinary { command: Some(command.unwrap()), cwd: Some(delegate.worktree_root_path().to_path_buf()), @@ -407,7 +404,7 @@ impl DebugAdapter for CodeLldbDebugAdapter { request_args: self .request_args(delegate, json_config, &config.label) .await?, - envs, + envs: user_env.unwrap_or_default(), connection: None, }) } diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index 17b7a659111532b5fa04f2b3424e50e7867df6d6..12489247c53322612ea7d7cd33fedce51bb68b26 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -1,7 +1,8 @@ -use std::{collections::HashMap, ffi::OsStr}; +use std::ffi::OsStr; use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; +use collections::HashMap; use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::AsyncApp; use task::{DebugScenario, ZedDebugConfig}; @@ -160,6 +161,7 @@ impl DebugAdapter for GdbDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, _: &mut AsyncApp, ) -> Result { let user_setting_path = user_installed_path @@ -188,7 +190,7 @@ impl DebugAdapter for GdbDebugAdapter { Ok(DebugAdapterBinary { command: Some(gdb_path), arguments: user_args.unwrap_or_else(|| vec!["-i=dap".into()]), - envs: HashMap::default(), + envs: user_env.unwrap_or_default(), cwd: Some(delegate.worktree_root_path().to_path_buf()), connection: None, request_args: StartDebuggingRequestArguments { diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index 999909ad44f313d413ecaa3990f9816872bae588..323ca094934fc93466451246f4bc69f34ded4891 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -409,6 +409,7 @@ impl DebugAdapter for GoDebugAdapter { task_definition: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, _cx: &mut AsyncApp, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME); @@ -460,7 +461,7 @@ impl DebugAdapter for GoDebugAdapter { let connection; let mut configuration = task_definition.config.clone(); - let mut envs = HashMap::default(); + let mut envs = user_env.unwrap_or_default(); if let Some(configuration) = configuration.as_object_mut() { configuration diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 4e3dc30a7929683cc030558bed5034fe8ed69349..8c90bfc7c054f147336f9c6330d5f1d4a847d588 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -52,12 +52,13 @@ impl JsDebugAdapter { task_definition: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, _: &mut AsyncApp, ) -> Result { let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; - let mut envs = HashMap::default(); + let mut envs = user_env.unwrap_or_default(); let mut configuration = task_definition.config.clone(); if let Some(configuration) = configuration.as_object_mut() { @@ -100,9 +101,9 @@ impl JsDebugAdapter { } if let Some(env) = configuration.get("env").cloned() - && let Ok(env) = serde_json::from_value(env) + && let Ok(env) = serde_json::from_value::>(env) { - envs = env; + envs.extend(env.into_iter()); } configuration @@ -504,6 +505,7 @@ impl DebugAdapter for JsDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, cx: &mut AsyncApp, ) -> Result { if self.checked.set(()).is_ok() { @@ -521,8 +523,15 @@ impl DebugAdapter for JsDebugAdapter { } } - self.get_installed_binary(delegate, config, user_installed_path, user_args, cx) - .await + self.get_installed_binary( + delegate, + config, + user_installed_path, + user_args, + user_env, + cx, + ) + .await } fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option { diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 1efe326f4380bb91ed38ecb5ad0bd4f66e8fbfe3..66005db77029bd28c66f458bef7f1d2a1ad7a685 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,5 +1,6 @@ use crate::*; use anyhow::{Context as _, bail}; +use collections::HashMap; use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use fs::RemoveOptions; use futures::{StreamExt, TryStreamExt}; @@ -16,7 +17,6 @@ use std::ffi::OsString; use std::net::Ipv4Addr; use std::str::FromStr; use std::{ - collections::HashMap, ffi::OsStr, path::{Path, PathBuf}, }; @@ -312,6 +312,7 @@ impl PythonDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, python_from_toolchain: Option, ) -> Result { let tcp_connection = config.tcp_connection.clone().unwrap_or_default(); @@ -349,7 +350,7 @@ impl PythonDebugAdapter { timeout, }), cwd: Some(delegate.worktree_root_path().to_path_buf()), - envs: HashMap::default(), + envs: user_env.unwrap_or_default(), request_args: self.request_args(delegate, config).await?, }) } @@ -744,6 +745,7 @@ impl DebugAdapter for PythonDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option, user_args: Option>, + user_env: Option>, cx: &mut AsyncApp, ) -> Result { if let Some(local_path) = &user_installed_path { @@ -752,7 +754,14 @@ impl DebugAdapter for PythonDebugAdapter { local_path.display() ); return self - .get_installed_binary(delegate, config, Some(local_path.clone()), user_args, None) + .get_installed_binary( + delegate, + config, + Some(local_path.clone()), + user_args, + user_env, + None, + ) .await; } @@ -790,12 +799,13 @@ impl DebugAdapter for PythonDebugAdapter { config, None, user_args, + user_env, Some(toolchain.path.to_string()), ) .await; } - self.get_installed_binary(delegate, config, None, user_args, None) + self.get_installed_binary(delegate, config, None, user_args, user_env, None) .await } diff --git a/crates/debug_adapter_extension/Cargo.toml b/crates/debug_adapter_extension/Cargo.toml index 78d7cbaba3fbf92f4863228c532524cd0f0577ba..28619260978bc076a974847748c3fd76b1dffb03 100644 --- a/crates/debug_adapter_extension/Cargo.toml +++ b/crates/debug_adapter_extension/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true +collections.workspace = true dap.workspace = true extension.workspace = true gpui.workspace = true diff --git a/crates/debug_adapter_extension/src/extension_dap_adapter.rs b/crates/debug_adapter_extension/src/extension_dap_adapter.rs index 3a39027b62963aa99b53b09ab621f91a1b3f95c5..abc0fbac19faa2be0f6c1ff8c93cadd2b6b96af9 100644 --- a/crates/debug_adapter_extension/src/extension_dap_adapter.rs +++ b/crates/debug_adapter_extension/src/extension_dap_adapter.rs @@ -6,6 +6,7 @@ use std::{ use anyhow::{Context, Result}; use async_trait::async_trait; +use collections::HashMap; use dap::{ StartDebuggingRequestArgumentsRequest, adapters::{ @@ -91,6 +92,8 @@ impl DebugAdapter for ExtensionDapAdapter { user_installed_path: Option, // TODO support user args in the extension API _user_args: Option>, + // TODO support user env in the extension API + _user_env: Option>, _cx: &mut AsyncApp, ) -> Result { self.extension diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 0c16e5bbddbb985e635a4c6694bc7828cb1ed741..7d80c563e9678ec097dab030bdca047a967e2cf0 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -264,13 +264,21 @@ impl DapStore { DapBinary::Custom(binary) => Some(PathBuf::from(binary)), }); let user_args = dap_settings.map(|s| s.args.clone()); + let user_env = dap_settings.map(|s| s.env.clone()); let delegate = self.delegate(worktree, console, cx); let cwd: Arc = worktree.read(cx).abs_path().as_ref().into(); cx.spawn(async move |this, cx| { let mut binary = adapter - .get_binary(&delegate, &definition, user_installed_path, user_args, cx) + .get_binary( + &delegate, + &definition, + user_installed_path, + user_args, + user_env, + cx, + ) .await?; let env = this diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 1790313dcad43994359c07637ff3b8b534293970..88fe7bb6d215540e68d7770c799bc3028cadc674 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -1215,6 +1215,7 @@ pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSett pub struct DapSettings { pub binary: DapBinary, pub args: Vec, + pub env: HashMap, } impl From for DapSettings { @@ -1224,6 +1225,7 @@ impl From for DapSettings { .binary .map_or_else(|| DapBinary::Default, |binary| DapBinary::Custom(binary)), args: content.args.unwrap_or_default(), + env: content.env.unwrap_or_default(), } } } diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index 88d9f9803e1579a77d7140e826961ad01f5eedac..d421a7bb2aefb92f0c7cd1de1c89fe1fee95e3ec 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -154,6 +154,8 @@ pub struct DapSettingsContent { pub binary: Option, #[serde(default)] pub args: Option>, + #[serde(default)] + pub env: Option>, } #[skip_serializing_none] From 3f1319162af7f5b6b6d6a3db51cc07a258c45e95 Mon Sep 17 00:00:00 2001 From: Bennet Fenner Date: Fri, 17 Oct 2025 18:49:11 +0200 Subject: [PATCH 002/202] Remove agent1 code (#40495) Release Notes: - N/A --- .zed/settings.json | 2 +- Cargo.lock | 266 +- Cargo.toml | 8 +- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- assets/keymaps/default-windows.json | 4 +- clippy.toml | 2 +- crates/agent/Cargo.toml | 72 +- crates/agent/src/agent.rs | 1658 +++- crates/agent/src/agent_profile.rs | 341 - crates/agent/src/context_server_tool.rs | 140 - crates/{agent2 => agent}/src/db.rs | 124 +- .../src/edit_agent.rs | 0 .../src/edit_agent/create_file_parser.rs | 0 .../src/edit_agent/edit_parser.rs | 0 .../src/edit_agent/evals.rs | 80 +- .../fixtures/add_overwrite_test/before.rs | 0 .../fixtures/delete_run_git_blame/after.rs | 0 .../fixtures/delete_run_git_blame/before.rs | 0 .../disable_cursor_blinking/before.rs | 0 .../disable_cursor_blinking/possible-01.diff | 0 .../disable_cursor_blinking/possible-02.diff | 0 .../disable_cursor_blinking/possible-03.diff | 0 .../disable_cursor_blinking/possible-04.diff | 0 .../extract_handle_command_output/before.rs | 0 .../possible-01.diff | 0 .../possible-02.diff | 0 .../possible-03.diff | 0 .../possible-04.diff | 0 .../possible-05.diff | 0 .../possible-06.diff | 0 .../possible-07.diff | 0 .../possible-08.diff | 0 .../from_pixels_constructor/before.rs | 0 .../fixtures/translate_doc_comments/before.rs | 0 .../before.rs | 0 .../edit_agent/evals/fixtures/zode/prompt.md | 0 .../edit_agent/evals/fixtures/zode/react.py | 0 .../evals/fixtures/zode/react_test.py | 0 .../src/edit_agent/streaming_fuzzy_matcher.rs | 0 crates/{agent2 => agent}/src/history_store.rs | 104 +- crates/agent/src/legacy_thread.rs | 402 + .../src/native_agent_server.rs | 0 .../{assistant_tool => agent}/src/outline.rs | 158 +- .../src/prompts/stale_files_prompt_header.txt | 3 - crates/{agent2 => agent}/src/templates.rs | 0 .../src/templates/create_file_prompt.hbs | 0 .../src/templates/diff_judge.hbs | 0 .../edit_file_prompt_diff_fenced.hbs | 0 .../src/templates/edit_file_prompt_xml.hbs | 0 .../src/templates/system_prompt.hbs | 0 crates/{agent2 => agent}/src/tests/mod.rs | 20 +- .../{agent2 => agent}/src/tests/test_tools.rs | 0 crates/agent/src/thread.rs | 7030 +++++------------ crates/agent/src/thread_store.rs | 1287 --- .../src/tool_schema.rs | 43 +- crates/agent/src/tool_use.rs | 575 -- crates/agent/src/tools.rs | 88 + .../src/tools/context_server_registry.rs | 13 +- .../src/tools/copy_path_tool.rs | 0 .../src/tools/create_directory_tool.rs | 0 .../src/tools/delete_path_tool.rs | 0 .../src/tools/diagnostics_tool.rs | 0 .../src/tools/edit_file_tool.rs | 30 +- .../{agent2 => agent}/src/tools/fetch_tool.rs | 0 .../src/tools/find_path_tool.rs | 0 .../{agent2 => agent}/src/tools/grep_tool.rs | 0 .../src/tools/list_directory_tool.rs | 0 .../src/tools/move_path_tool.rs | 0 .../{agent2 => agent}/src/tools/now_tool.rs | 0 .../{agent2 => agent}/src/tools/open_tool.rs | 0 .../src/tools/read_file_tool.rs | 3 +- .../src/tools/terminal_tool.rs | 0 .../src/tools/thinking_tool.rs | 0 .../src/tools/web_search_tool.rs | 0 crates/agent2/Cargo.toml | 102 - crates/agent2/LICENSE-GPL | 1 - crates/agent2/src/agent.rs | 1588 ---- crates/agent2/src/agent2.rs | 19 - crates/agent2/src/thread.rs | 2663 ------- crates/agent2/src/tool_schema.rs | 43 - crates/agent2/src/tools.rs | 60 - crates/agent_settings/src/agent_settings.rs | 5 +- .../summarize_thread_detailed_prompt.txt | 0 .../src/prompts/summarize_thread_prompt.txt | 0 crates/agent_ui/Cargo.toml | 5 +- .../agent_ui/src/acp/completion_provider.rs | 39 +- crates/agent_ui/src/acp/entry_view_state.rs | 4 +- crates/agent_ui/src/acp/message_editor.rs | 16 +- crates/agent_ui/src/acp/thread_history.rs | 9 +- crates/agent_ui/src/acp/thread_view.rs | 16 +- crates/agent_ui/src/agent_configuration.rs | 58 +- .../configure_context_server_tools_modal.rs | 37 +- .../manage_profiles_modal.rs | 28 +- .../src/agent_configuration/tool_picker.rs | 100 +- crates/agent_ui/src/agent_panel.rs | 179 +- crates/agent_ui/src/agent_ui.rs | 13 +- crates/agent_ui/src/buffer_codegen.rs | 13 +- crates/{agent => agent_ui}/src/context.rs | 154 +- crates/agent_ui/src/context_picker.rs | 149 +- .../src/context_picker/completion_provider.rs | 126 +- .../context_picker/fetch_context_picker.rs | 3 +- .../src/context_picker/file_context_picker.rs | 6 +- .../context_picker/rules_context_picker.rs | 24 +- .../context_picker/symbol_context_picker.rs | 6 +- .../context_picker/thread_context_picker.rs | 219 +- .../{agent => agent_ui}/src/context_store.rs | 107 +- crates/agent_ui/src/context_strip.rs | 28 +- crates/agent_ui/src/inline_assistant.rs | 35 +- crates/agent_ui/src/inline_prompt_editor.rs | 23 +- crates/agent_ui/src/message_editor.rs | 38 +- .../agent_ui/src/terminal_inline_assistant.rs | 20 +- crates/agent_ui/src/ui/context_pill.rs | 12 +- crates/assistant_tool/Cargo.toml | 50 - crates/assistant_tool/LICENSE-GPL | 1 - crates/assistant_tool/src/assistant_tool.rs | 269 - crates/assistant_tool/src/tool_registry.rs | 74 - crates/assistant_tool/src/tool_working_set.rs | 415 - crates/assistant_tools/Cargo.toml | 92 - crates/assistant_tools/LICENSE-GPL | 1 - crates/assistant_tools/src/assistant_tools.rs | 167 - crates/assistant_tools/src/copy_path_tool.rs | 123 - .../src/copy_path_tool/description.md | 6 - .../src/create_directory_tool.rs | 100 - .../src/create_directory_tool/description.md | 3 - .../assistant_tools/src/delete_path_tool.rs | 144 - .../src/delete_path_tool/description.md | 1 - .../assistant_tools/src/diagnostics_tool.rs | 171 - .../src/diagnostics_tool/description.md | 21 - crates/assistant_tools/src/edit_file_tool.rs | 2423 ------ .../src/edit_file_tool/description.md | 8 - crates/assistant_tools/src/fetch_tool.rs | 178 - .../src/fetch_tool/description.md | 1 - crates/assistant_tools/src/find_path_tool.rs | 472 -- .../src/find_path_tool/description.md | 7 - crates/assistant_tools/src/grep_tool.rs | 1308 --- .../src/grep_tool/description.md | 9 - .../src/list_directory_tool.rs | 869 -- .../src/list_directory_tool/description.md | 1 - crates/assistant_tools/src/move_path_tool.rs | 132 - .../src/move_path_tool/description.md | 5 - crates/assistant_tools/src/now_tool.rs | 84 - crates/assistant_tools/src/open_tool.rs | 170 - .../src/open_tool/description.md | 9 - .../src/project_notifications_tool.rs | 360 - .../project_notifications_tool/description.md | 3 - .../prompt_header.txt | 3 - crates/assistant_tools/src/read_file_tool.rs | 1190 --- .../src/read_file_tool/description.md | 3 - crates/assistant_tools/src/schema.rs | 60 - crates/assistant_tools/src/templates.rs | 32 - crates/assistant_tools/src/terminal_tool.rs | 883 --- .../src/terminal_tool/description.md | 11 - crates/assistant_tools/src/thinking_tool.rs | 69 - .../src/thinking_tool/description.md | 1 - crates/assistant_tools/src/ui.rs | 5 - .../src/ui/tool_call_card_header.rs | 131 - .../src/ui/tool_output_preview.rs | 115 - crates/assistant_tools/src/web_search_tool.rs | 327 - crates/eval/Cargo.toml | 4 +- crates/eval/src/eval.rs | 1 - crates/eval/src/example.rs | 4 +- .../eval/src/examples/comment_translation.rs | 2 +- crates/eval/src/examples/file_search.rs | 2 +- .../src/examples/grep_params_escapement.rs | 1 - crates/eval/src/examples/overwrite_file.rs | 1 - crates/eval/src/examples/planets.rs | 7 +- crates/eval/src/instance.rs | 3 +- crates/language_model/Cargo.toml | 1 - crates/language_model/src/language_model.rs | 8 +- crates/remote_server/Cargo.toml | 3 +- .../remote_server/src/remote_editing_tests.rs | 42 +- crates/zed/Cargo.toml | 2 - crates/zed/src/main.rs | 1 - script/danger/dangerfile.ts | 11 +- 175 files changed, 5271 insertions(+), 23738 deletions(-) delete mode 100644 crates/agent/src/agent_profile.rs delete mode 100644 crates/agent/src/context_server_tool.rs rename crates/{agent2 => agent}/src/db.rs (78%) rename crates/{assistant_tools => agent}/src/edit_agent.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/create_file_parser.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/edit_parser.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals.rs (97%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/zode/prompt.md (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/zode/react.py (100%) rename crates/{assistant_tools => agent}/src/edit_agent/evals/fixtures/zode/react_test.py (100%) rename crates/{assistant_tools => agent}/src/edit_agent/streaming_fuzzy_matcher.rs (100%) rename crates/{agent2 => agent}/src/history_store.rs (80%) create mode 100644 crates/agent/src/legacy_thread.rs rename crates/{agent2 => agent}/src/native_agent_server.rs (100%) rename crates/{assistant_tool => agent}/src/outline.rs (76%) delete mode 100644 crates/agent/src/prompts/stale_files_prompt_header.txt rename crates/{agent2 => agent}/src/templates.rs (100%) rename crates/{assistant_tools => agent}/src/templates/create_file_prompt.hbs (100%) rename crates/{assistant_tools => agent}/src/templates/diff_judge.hbs (100%) rename crates/{assistant_tools => agent}/src/templates/edit_file_prompt_diff_fenced.hbs (100%) rename crates/{assistant_tools => agent}/src/templates/edit_file_prompt_xml.hbs (100%) rename crates/{agent2 => agent}/src/templates/system_prompt.hbs (100%) rename crates/{agent2 => agent}/src/tests/mod.rs (99%) rename crates/{agent2 => agent}/src/tests/test_tools.rs (100%) delete mode 100644 crates/agent/src/thread_store.rs rename crates/{assistant_tool => agent}/src/tool_schema.rs (85%) delete mode 100644 crates/agent/src/tool_use.rs create mode 100644 crates/agent/src/tools.rs rename crates/{agent2 => agent}/src/tools/context_server_registry.rs (95%) rename crates/{agent2 => agent}/src/tools/copy_path_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/create_directory_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/delete_path_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/diagnostics_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/edit_file_tool.rs (98%) rename crates/{agent2 => agent}/src/tools/fetch_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/find_path_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/grep_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/list_directory_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/move_path_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/now_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/open_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/read_file_tool.rs (99%) rename crates/{agent2 => agent}/src/tools/terminal_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/thinking_tool.rs (100%) rename crates/{agent2 => agent}/src/tools/web_search_tool.rs (100%) delete mode 100644 crates/agent2/Cargo.toml delete mode 120000 crates/agent2/LICENSE-GPL delete mode 100644 crates/agent2/src/agent.rs delete mode 100644 crates/agent2/src/agent2.rs delete mode 100644 crates/agent2/src/thread.rs delete mode 100644 crates/agent2/src/tool_schema.rs delete mode 100644 crates/agent2/src/tools.rs rename crates/{agent => agent_settings}/src/prompts/summarize_thread_detailed_prompt.txt (100%) rename crates/{agent => agent_settings}/src/prompts/summarize_thread_prompt.txt (100%) rename crates/{agent => agent_ui}/src/context.rs (90%) rename crates/{agent => agent_ui}/src/context_store.rs (87%) delete mode 100644 crates/assistant_tool/Cargo.toml delete mode 120000 crates/assistant_tool/LICENSE-GPL delete mode 100644 crates/assistant_tool/src/assistant_tool.rs delete mode 100644 crates/assistant_tool/src/tool_registry.rs delete mode 100644 crates/assistant_tool/src/tool_working_set.rs delete mode 100644 crates/assistant_tools/Cargo.toml delete mode 120000 crates/assistant_tools/LICENSE-GPL delete mode 100644 crates/assistant_tools/src/assistant_tools.rs delete mode 100644 crates/assistant_tools/src/copy_path_tool.rs delete mode 100644 crates/assistant_tools/src/copy_path_tool/description.md delete mode 100644 crates/assistant_tools/src/create_directory_tool.rs delete mode 100644 crates/assistant_tools/src/create_directory_tool/description.md delete mode 100644 crates/assistant_tools/src/delete_path_tool.rs delete mode 100644 crates/assistant_tools/src/delete_path_tool/description.md delete mode 100644 crates/assistant_tools/src/diagnostics_tool.rs delete mode 100644 crates/assistant_tools/src/diagnostics_tool/description.md delete mode 100644 crates/assistant_tools/src/edit_file_tool.rs delete mode 100644 crates/assistant_tools/src/edit_file_tool/description.md delete mode 100644 crates/assistant_tools/src/fetch_tool.rs delete mode 100644 crates/assistant_tools/src/fetch_tool/description.md delete mode 100644 crates/assistant_tools/src/find_path_tool.rs delete mode 100644 crates/assistant_tools/src/find_path_tool/description.md delete mode 100644 crates/assistant_tools/src/grep_tool.rs delete mode 100644 crates/assistant_tools/src/grep_tool/description.md delete mode 100644 crates/assistant_tools/src/list_directory_tool.rs delete mode 100644 crates/assistant_tools/src/list_directory_tool/description.md delete mode 100644 crates/assistant_tools/src/move_path_tool.rs delete mode 100644 crates/assistant_tools/src/move_path_tool/description.md delete mode 100644 crates/assistant_tools/src/now_tool.rs delete mode 100644 crates/assistant_tools/src/open_tool.rs delete mode 100644 crates/assistant_tools/src/open_tool/description.md delete mode 100644 crates/assistant_tools/src/project_notifications_tool.rs delete mode 100644 crates/assistant_tools/src/project_notifications_tool/description.md delete mode 100644 crates/assistant_tools/src/project_notifications_tool/prompt_header.txt delete mode 100644 crates/assistant_tools/src/read_file_tool.rs delete mode 100644 crates/assistant_tools/src/read_file_tool/description.md delete mode 100644 crates/assistant_tools/src/schema.rs delete mode 100644 crates/assistant_tools/src/templates.rs delete mode 100644 crates/assistant_tools/src/terminal_tool.rs delete mode 100644 crates/assistant_tools/src/terminal_tool/description.md delete mode 100644 crates/assistant_tools/src/thinking_tool.rs delete mode 100644 crates/assistant_tools/src/thinking_tool/description.md delete mode 100644 crates/assistant_tools/src/ui.rs delete mode 100644 crates/assistant_tools/src/ui/tool_call_card_header.rs delete mode 100644 crates/assistant_tools/src/ui/tool_output_preview.rs delete mode 100644 crates/assistant_tools/src/web_search_tool.rs diff --git a/.zed/settings.json b/.zed/settings.json index 68e05a426f2474cb663aa5ff843905f375170e0f..2760be95819e9340acf55f60616a9c22105ff52a 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -48,7 +48,7 @@ "remove_trailing_whitespace_on_save": true, "ensure_final_newline_on_save": true, "file_scan_exclusions": [ - "crates/assistant_tools/src/edit_agent/evals/fixtures", + "crates/agent/src/edit_agent/evals/fixtures", "crates/eval/worktrees/", "crates/eval/repos/", "**/.git", diff --git a/Cargo.lock b/Cargo.lock index 43c9c672eb83da9b02f6d885508189b55c5f0080..bb3b71a3ee45e052670ef3e67c877833253c76b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,90 +139,14 @@ dependencies = [ [[package]] name = "agent" version = "0.1.0" -dependencies = [ - "action_log", - "agent_settings", - "anyhow", - "assistant_context", - "assistant_tool", - "assistant_tools", - "chrono", - "client", - "cloud_llm_client", - "collections", - "component", - "context_server", - "convert_case 0.8.0", - "fs", - "futures 0.3.31", - "git", - "gpui", - "heed", - "http_client", - "icons", - "indoc", - "language", - "language_model", - "log", - "parking_lot", - "paths", - "postage", - "pretty_assertions", - "project", - "prompt_store", - "rand 0.9.1", - "ref-cast", - "rope", - "schemars 1.0.1", - "serde", - "serde_json", - "settings", - "smol", - "sqlez", - "telemetry", - "text", - "theme", - "thiserror 2.0.12", - "time", - "util", - "uuid", - "workspace", - "workspace-hack", - "zed_env_vars", - "zstd 0.11.2+zstd.1.5.2", -] - -[[package]] -name = "agent-client-protocol" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60" -dependencies = [ - "anyhow", - "async-broadcast", - "async-trait", - "futures 0.3.31", - "log", - "parking_lot", - "schemars 1.0.1", - "serde", - "serde_json", -] - -[[package]] -name = "agent2" -version = "0.1.0" dependencies = [ "acp_thread", "action_log", - "agent", "agent-client-protocol", "agent_servers", "agent_settings", "anyhow", "assistant_context", - "assistant_tool", - "assistant_tools", "chrono", "client", "clock", @@ -231,6 +155,7 @@ dependencies = [ "context_server", "ctor", "db", + "derive_more", "editor", "env_logger 0.11.8", "fs", @@ -254,14 +179,19 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", + "rand 0.9.1", + "regex", "reqwest_client", "rust-embed", "schemars 1.0.1", "serde", "serde_json", "settings", + "smallvec", "smol", "sqlez", + "streaming_diff", + "strsim", "task", "telemetry", "tempfile", @@ -283,6 +213,23 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] +[[package]] +name = "agent-client-protocol" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60" +dependencies = [ + "anyhow", + "async-broadcast", + "async-trait", + "futures 0.3.31", + "log", + "parking_lot", + "schemars 1.0.1", + "serde", + "serde_json", +] + [[package]] name = "agent_servers" version = "0.1.0" @@ -356,7 +303,6 @@ dependencies = [ "action_log", "agent", "agent-client-protocol", - "agent2", "agent_servers", "agent_settings", "ai_onboarding", @@ -365,8 +311,6 @@ dependencies = [ "assistant_context", "assistant_slash_command", "assistant_slash_commands", - "assistant_tool", - "assistant_tools", "audio", "buffer_diff", "chrono", @@ -411,6 +355,7 @@ dependencies = [ "prompt_store", "proto", "rand 0.9.1", + "ref-cast", "release_channel", "rope", "rules_library", @@ -965,106 +910,6 @@ dependencies = [ "zlog", ] -[[package]] -name = "assistant_tool" -version = "0.1.0" -dependencies = [ - "action_log", - "anyhow", - "buffer_diff", - "clock", - "collections", - "ctor", - "derive_more", - "gpui", - "icons", - "indoc", - "language", - "language_model", - "log", - "parking_lot", - "pretty_assertions", - "project", - "rand 0.9.1", - "regex", - "serde", - "serde_json", - "settings", - "text", - "util", - "workspace", - "workspace-hack", - "zlog", -] - -[[package]] -name = "assistant_tools" -version = "0.1.0" -dependencies = [ - "action_log", - "agent_settings", - "anyhow", - "assistant_tool", - "buffer_diff", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "component", - "derive_more", - "diffy", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "handlebars 4.5.0", - "html_to_markdown", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "language_models", - "log", - "lsp", - "markdown", - "open", - "paths", - "portable-pty", - "pretty_assertions", - "project", - "prompt_store", - "rand 0.9.1", - "regex", - "reqwest_client", - "rust-embed", - "schemars 1.0.1", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "streaming_diff", - "strsim", - "task", - "tempfile", - "terminal", - "terminal_view", - "theme", - "tree-sitter-rust", - "ui", - "unindent", - "util", - "watch", - "web_search", - "workspace", - "workspace-hack", - "zlog", -] - [[package]] name = "async-attributes" version = "1.1.2" @@ -5819,63 +5664,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "eval" -version = "0.1.0" -dependencies = [ - "agent", - "agent_settings", - "agent_ui", - "anyhow", - "assistant_tool", - "assistant_tools", - "async-trait", - "buffer_diff", - "chrono", - "clap", - "client", - "cloud_llm_client", - "collections", - "debug_adapter_extension", - "dirs 4.0.0", - "dotenvy", - "env_logger 0.11.8", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "handlebars 4.5.0", - "language", - "language_extension", - "language_model", - "language_models", - "languages", - "markdown", - "node_runtime", - "pathdiff", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "regex", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smol", - "telemetry", - "terminal_view", - "toml 0.8.20", - "unindent", - "util", - "uuid", - "watch", - "workspace-hack", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -8987,7 +8775,6 @@ dependencies = [ "open_router", "parking_lot", "proto", - "schemars 1.0.1", "serde", "serde_json", "settings", @@ -14006,10 +13793,9 @@ name = "remote_server" version = "0.1.0" dependencies = [ "action_log", + "agent", "anyhow", "askpass", - "assistant_tool", - "assistant_tools", "cargo_toml", "clap", "client", @@ -21242,14 +21028,12 @@ version = "0.210.0" dependencies = [ "acp_tools", "activity_indicator", - "agent", "agent_settings", "agent_ui", "anyhow", "ashpd 0.11.0", "askpass", "assets", - "assistant_tools", "audio", "auto_update", "auto_update_ui", diff --git a/Cargo.toml b/Cargo.toml index 3bc4123a0967a1f3bdc5d4d48d37ffedfbf372ce..33f3fa2ed3fa912e33bc24fa9303e3c2b4790dad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "crates/action_log", "crates/activity_indicator", "crates/agent", - "crates/agent2", "crates/agent_servers", "crates/agent_settings", "crates/agent_ui", @@ -17,8 +16,6 @@ members = [ "crates/assistant_context", "crates/assistant_slash_command", "crates/assistant_slash_commands", - "crates/assistant_tool", - "crates/assistant_tools", "crates/audio", "crates/auto_update", "crates/auto_update_helper", @@ -61,7 +58,7 @@ members = [ "crates/edit_prediction_context", "crates/zeta2_tools", "crates/editor", - "crates/eval", + # "crates/eval", "crates/explorer_command_injector", "crates/extension", "crates/extension_api", @@ -240,7 +237,6 @@ acp_tools = { path = "crates/acp_tools" } acp_thread = { path = "crates/acp_thread" } action_log = { path = "crates/action_log" } agent = { path = "crates/agent" } -agent2 = { path = "crates/agent2" } activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } @@ -253,8 +249,6 @@ assets = { path = "crates/assets" } assistant_context = { path = "crates/assistant_context" } assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_slash_commands = { path = "crates/assistant_slash_commands" } -assistant_tool = { path = "crates/assistant_tool" } -assistant_tools = { path = "crates/assistant_tools" } audio = { path = "crates/audio" } auto_update = { path = "crates/auto_update" } auto_update_helper = { path = "crates/auto_update_helper" } diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 6f3b0ced8feaf5ca9ca3873c47b446117cedb6e8..ff5d7533f412872908d52228590fa3afe45a02d0 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -269,14 +269,14 @@ } }, { - "context": "AgentPanel && prompt_editor", + "context": "AgentPanel && text_thread", "bindings": { "ctrl-n": "agent::NewTextThread", "ctrl-alt-t": "agent::NewThread" } }, { - "context": "AgentPanel && external_agent_thread", + "context": "AgentPanel && acp_thread", "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewExternalAgentThread", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ffa29b719f74e76c19dc7476c9c6b9643791a22f..9b20c267feeed5068b05cb08e0755d3faab75c96 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -307,7 +307,7 @@ } }, { - "context": "AgentPanel && prompt_editor", + "context": "AgentPanel && text_thread", "use_key_equivalents": true, "bindings": { "cmd-n": "agent::NewTextThread", @@ -315,7 +315,7 @@ } }, { - "context": "AgentPanel && external_agent_thread", + "context": "AgentPanel && acp_thread", "use_key_equivalents": true, "bindings": { "cmd-n": "agent::NewExternalAgentThread", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 4daacca4e2eaa224dacd3880fef2d046139587dd..87e1c350dc10c1f47ceb260b4ce2a03a032b0996 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -270,7 +270,7 @@ } }, { - "context": "AgentPanel && prompt_editor", + "context": "AgentPanel && text_thread", "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewTextThread", @@ -278,7 +278,7 @@ } }, { - "context": "AgentPanel && external_agent_thread", + "context": "AgentPanel && acp_thread", "use_key_equivalents": true, "bindings": { "ctrl-n": "agent::NewExternalAgentThread", diff --git a/clippy.toml b/clippy.toml index 0976e2eba301b9bf679baecc232c6ae7084fd5e8..4e9f2de8585e74afe76840c59306ad8ed87fd947 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,7 +3,7 @@ avoid-breaking-exported-api = false ignore-interior-mutability = [ # Suppresses clippy::mutable_key_type, which is a false positive as the Eq # and Hash impls do not use fields with interior mutability. - "agent::context::AgentContextKey" + "agent_ui::context::AgentContextKey" ] disallowed-methods = [ { path = "std::process::Command::spawn", reason = "Spawning `std::process::Command` can block the current thread for an unknown duration", replacement = "smol::process::Command::spawn" }, diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index ebd043d0c3c61bed287507e303637035a5b8156f..5fb8f915b8f19d5adf6132f0fbefda0f5081bbae 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -5,74 +5,100 @@ edition.workspace = true publish.workspace = true license = "GPL-3.0-or-later" -[lints] -workspace = true - [lib] path = "src/agent.rs" -doctest = false [features] -test-support = [ - "gpui/test-support", - "language/test-support", -] +test-support = ["db/test-support"] +e2e = [] + +[lints] +workspace = true [dependencies] +acp_thread.workspace = true action_log.workspace = true +agent-client-protocol.workspace = true +agent_servers.workspace = true agent_settings.workspace = true anyhow.workspace = true assistant_context.workspace = true -assistant_tool.workspace = true chrono.workspace = true client.workspace = true cloud_llm_client.workspace = true collections.workspace = true -component.workspace = true context_server.workspace = true -convert_case.workspace = true +db.workspace = true +derive_more.workspace = true fs.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true -heed.workspace = true +handlebars = { workspace = true, features = ["rust-embed"] } +html_to_markdown.workspace = true http_client.workspace = true -icons.workspace = true indoc.workspace = true +itertools.workspace = true language.workspace = true language_model.workspace = true +language_models.workspace = true log.workspace = true +open.workspace = true +parking_lot.workspace = true paths.workspace = true -postage.workspace = true project.workspace = true prompt_store.workspace = true -ref-cast.workspace = true -rope.workspace = true +regex.workspace = true +rust-embed.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +smallvec.workspace = true smol.workspace = true sqlez.workspace = true +streaming_diff.workspace = true +strsim.workspace = true +task.workspace = true telemetry.workspace = true +terminal.workspace = true text.workspace = true -theme.workspace = true thiserror.workspace = true -time.workspace = true +ui.workspace = true util.workspace = true uuid.workspace = true +watch.workspace = true +web_search.workspace = true workspace-hack.workspace = true zed_env_vars.workspace = true zstd.workspace = true [dev-dependencies] -assistant_tools.workspace = true +agent_servers = { workspace = true, "features" = ["test-support"] } +assistant_context = { workspace = true, "features" = ["test-support"] } +client = { workspace = true, "features" = ["test-support"] } +clock = { workspace = true, "features" = ["test-support"] } +context_server = { workspace = true, "features" = ["test-support"] } +ctor.workspace = true +db = { workspace = true, "features" = ["test-support"] } +editor = { workspace = true, "features" = ["test-support"] } +env_logger.workspace = true +fs = { workspace = true, "features" = ["test-support"] } +git = { workspace = true, "features" = ["test-support"] } gpui = { workspace = true, "features" = ["test-support"] } -indoc.workspace = true +gpui_tokio.workspace = true language = { workspace = true, "features" = ["test-support"] } language_model = { workspace = true, "features" = ["test-support"] } -parking_lot.workspace = true +lsp = { workspace = true, "features" = ["test-support"] } pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } +project = { workspace = true, "features" = ["test-support"] } rand.workspace = true +reqwest_client.workspace = true +settings = { workspace = true, "features" = ["test-support"] } +tempfile.workspace = true +terminal = { workspace = true, "features" = ["test-support"] } +theme = { workspace = true, "features" = ["test-support"] } +tree-sitter-rust.workspace = true +unindent = { workspace = true } +worktree = { workspace = true, "features" = ["test-support"] } +zlog.workspace = true diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 056c380e78de576fd7b0c065e3e5de631fdc37bb..32dec9f723a6776fd14def29be3be4eb21afa72d 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1,21 +1,1645 @@ -pub mod agent_profile; -pub mod context; -pub mod context_server_tool; -pub mod context_store; -pub mod thread; -pub mod thread_store; -pub mod tool_use; - -pub use context::{AgentContext, ContextId, ContextLoadResult}; -pub use context_store::ContextStore; +mod db; +mod edit_agent; +mod history_store; +mod legacy_thread; +mod native_agent_server; +pub mod outline; +mod templates; +mod thread; +mod tool_schema; +mod tools; + +#[cfg(test)] +mod tests; + +pub use db::*; +pub use history_store::*; +pub use native_agent_server::NativeAgentServer; +pub use templates::*; +pub use thread::*; +pub use tools::*; + +use acp_thread::{AcpThread, AgentModelSelector}; +use agent_client_protocol as acp; +use anyhow::{Context as _, Result, anyhow}; +use chrono::{DateTime, Utc}; +use collections::{HashSet, IndexMap}; use fs::Fs; -use std::sync::Arc; -pub use thread::{ - LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError, - ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio, +use futures::channel::{mpsc, oneshot}; +use futures::future::Shared; +use futures::{StreamExt, future}; +use gpui::{ + App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, +}; +use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry}; +use project::{Project, ProjectItem, ProjectPath, Worktree}; +use prompt_store::{ + ProjectContext, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, }; -pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore}; +use serde::{Deserialize, Serialize}; +use settings::{LanguageModelSelection, update_settings_file}; +use std::any::Any; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::Arc; +use util::ResultExt; +use util::rel_path::RelPath; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ProjectSnapshot { + pub worktree_snapshots: Vec, + pub timestamp: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WorktreeSnapshot { + pub worktree_path: String, + pub git_state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct GitState { + pub remote_url: Option, + pub head_sha: Option, + pub current_branch: Option, + pub diff: Option, +} + +const RULES_FILE_NAMES: [&str; 9] = [ + ".rules", + ".cursorrules", + ".windsurfrules", + ".clinerules", + ".github/copilot-instructions.md", + "CLAUDE.md", + "AGENT.md", + "AGENTS.md", + "GEMINI.md", +]; + +pub struct RulesLoadingError { + pub message: SharedString, +} + +/// Holds both the internal Thread and the AcpThread for a session +struct Session { + /// The internal thread that processes messages + thread: Entity, + /// The ACP thread that handles protocol communication + acp_thread: WeakEntity, + pending_save: Task<()>, + _subscriptions: Vec, +} + +pub struct LanguageModels { + /// Access language model by ID + models: HashMap>, + /// Cached list for returning language model information + model_list: acp_thread::AgentModelList, + refresh_models_rx: watch::Receiver<()>, + refresh_models_tx: watch::Sender<()>, + _authenticate_all_providers_task: Task<()>, +} + +impl LanguageModels { + fn new(cx: &mut App) -> Self { + let (refresh_models_tx, refresh_models_rx) = watch::channel(()); + + let mut this = Self { + models: HashMap::default(), + model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()), + refresh_models_rx, + refresh_models_tx, + _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx), + }; + this.refresh_list(cx); + this + } + + fn refresh_list(&mut self, cx: &App) { + let providers = LanguageModelRegistry::global(cx) + .read(cx) + .providers() + .into_iter() + .filter(|provider| provider.is_authenticated(cx)) + .collect::>(); + + let mut language_model_list = IndexMap::default(); + let mut recommended_models = HashSet::default(); + + let mut recommended = Vec::new(); + for provider in &providers { + for model in provider.recommended_models(cx) { + recommended_models.insert((model.provider_id(), model.id())); + recommended.push(Self::map_language_model_to_info(&model, provider)); + } + } + if !recommended.is_empty() { + language_model_list.insert( + acp_thread::AgentModelGroupName("Recommended".into()), + recommended, + ); + } + + let mut models = HashMap::default(); + for provider in providers { + let mut provider_models = Vec::new(); + for model in provider.provided_models(cx) { + let model_info = Self::map_language_model_to_info(&model, &provider); + let model_id = model_info.id.clone(); + if !recommended_models.contains(&(model.provider_id(), model.id())) { + provider_models.push(model_info); + } + models.insert(model_id, model); + } + if !provider_models.is_empty() { + language_model_list.insert( + acp_thread::AgentModelGroupName(provider.name().0.clone()), + provider_models, + ); + } + } + + self.models = models; + self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); + self.refresh_models_tx.send(()).ok(); + } + + fn watch(&self) -> watch::Receiver<()> { + self.refresh_models_rx.clone() + } + + pub fn model_from_id(&self, model_id: &acp::ModelId) -> Option> { + self.models.get(model_id).cloned() + } + + fn map_language_model_to_info( + model: &Arc, + provider: &Arc, + ) -> acp_thread::AgentModelInfo { + acp_thread::AgentModelInfo { + id: Self::model_id(model), + name: model.name().0, + description: None, + icon: Some(provider.icon()), + } + } + + fn model_id(model: &Arc) -> acp::ModelId { + acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into()) + } + + fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> { + let authenticate_all_providers = LanguageModelRegistry::global(cx) + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + cx.background_spawn(async move { + for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { + if let Err(err) = authenticate_task.await { + match err { + language_model::AuthenticateError::CredentialsNotFound => { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } + language_model::AuthenticateError::ConnectionRefused => { + // Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures. + // LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it. + // TODO: Better manage LM Studio auth logic to avoid these noisy failures. + } + _ => { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + } + } + }) + } +} + +pub struct NativeAgent { + /// Session ID -> Session mapping + sessions: HashMap, + history: Entity, + /// Shared project context for all threads + project_context: Entity, + project_context_needs_refresh: watch::Sender<()>, + _maintain_project_context: Task>, + context_server_registry: Entity, + /// Shared templates for all threads + templates: Arc, + /// Cached model information + models: LanguageModels, + project: Entity, + prompt_store: Option>, + fs: Arc, + _subscriptions: Vec, +} + +impl NativeAgent { + pub async fn new( + project: Entity, + history: Entity, + templates: Arc, + prompt_store: Option>, + fs: Arc, + cx: &mut AsyncApp, + ) -> Result> { + log::debug!("Creating new NativeAgent"); + + let project_context = cx + .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? + .await; + + cx.new(|cx| { + let mut subscriptions = vec![ + cx.subscribe(&project, Self::handle_project_event), + cx.subscribe( + &LanguageModelRegistry::global(cx), + Self::handle_models_updated_event, + ), + ]; + if let Some(prompt_store) = prompt_store.as_ref() { + subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) + } + + let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) = + watch::channel(()); + Self { + sessions: HashMap::new(), + history, + project_context: cx.new(|_| project_context), + project_context_needs_refresh: project_context_needs_refresh_tx, + _maintain_project_context: cx.spawn(async move |this, cx| { + Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await + }), + context_server_registry: cx.new(|cx| { + ContextServerRegistry::new(project.read(cx).context_server_store(), cx) + }), + templates, + models: LanguageModels::new(cx), + project, + prompt_store, + fs, + _subscriptions: subscriptions, + } + }) + } + + fn register_session( + &mut self, + thread_handle: Entity, + cx: &mut Context, + ) -> Entity { + let connection = Rc::new(NativeAgentConnection(cx.entity())); + + let thread = thread_handle.read(cx); + let session_id = thread.id().clone(); + let title = thread.title(); + let project = thread.project.clone(); + let action_log = thread.action_log.clone(); + let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); + let acp_thread = cx.new(|cx| { + acp_thread::AcpThread::new( + title, + connection, + project.clone(), + action_log.clone(), + session_id.clone(), + prompt_capabilities_rx, + cx, + ) + }); + + let registry = LanguageModelRegistry::read_global(cx); + let summarization_model = registry.thread_summary_model().map(|c| c.model); + + thread_handle.update(cx, |thread, cx| { + thread.set_summarization_model(summarization_model, cx); + thread.add_default_tools( + Rc::new(AcpThreadEnvironment { + acp_thread: acp_thread.downgrade(), + }) as _, + cx, + ) + }); + + let subscriptions = vec![ + cx.observe_release(&acp_thread, |this, acp_thread, _cx| { + this.sessions.remove(acp_thread.session_id()); + }), + cx.subscribe(&thread_handle, Self::handle_thread_title_updated), + cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), + cx.observe(&thread_handle, move |this, thread, cx| { + this.save_thread(thread, cx) + }), + ]; + + self.sessions.insert( + session_id, + Session { + thread: thread_handle, + acp_thread: acp_thread.downgrade(), + _subscriptions: subscriptions, + pending_save: Task::ready(()), + }, + ); + acp_thread + } + + pub fn models(&self) -> &LanguageModels { + &self.models + } + + async fn maintain_project_context( + this: WeakEntity, + mut needs_refresh: watch::Receiver<()>, + cx: &mut AsyncApp, + ) -> Result<()> { + while needs_refresh.changed().await.is_ok() { + let project_context = this + .update(cx, |this, cx| { + Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) + })? + .await; + this.update(cx, |this, cx| { + this.project_context = cx.new(|_| project_context); + })?; + } + + Ok(()) + } + + fn build_project_context( + project: &Entity, + prompt_store: Option<&Entity>, + cx: &mut App, + ) -> Task { + let worktrees = project.read(cx).visible_worktrees(cx).collect::>(); + let worktree_tasks = worktrees + .into_iter() + .map(|worktree| { + Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx) + }) + .collect::>(); + let default_user_rules_task = if let Some(prompt_store) = prompt_store.as_ref() { + prompt_store.read_with(cx, |prompt_store, cx| { + let prompts = prompt_store.default_prompt_metadata(); + let load_tasks = prompts.into_iter().map(|prompt_metadata| { + let contents = prompt_store.load(prompt_metadata.id, cx); + async move { (contents.await, prompt_metadata) } + }); + cx.background_spawn(future::join_all(load_tasks)) + }) + } else { + Task::ready(vec![]) + }; + + cx.spawn(async move |_cx| { + let (worktrees, default_user_rules) = + future::join(future::join_all(worktree_tasks), default_user_rules_task).await; + + let worktrees = worktrees + .into_iter() + .map(|(worktree, _rules_error)| { + // TODO: show error message + // if let Some(rules_error) = rules_error { + // this.update(cx, |_, cx| cx.emit(rules_error)).ok(); + // } + worktree + }) + .collect::>(); + + let default_user_rules = default_user_rules + .into_iter() + .flat_map(|(contents, prompt_metadata)| match contents { + Ok(contents) => Some(UserRulesContext { + uuid: match prompt_metadata.id { + prompt_store::PromptId::User { uuid } => uuid, + prompt_store::PromptId::EditWorkflow => return None, + }, + title: prompt_metadata.title.map(|title| title.to_string()), + contents, + }), + Err(_err) => { + // TODO: show error message + // this.update(cx, |_, cx| { + // cx.emit(RulesLoadingError { + // message: format!("{err:?}").into(), + // }); + // }) + // .ok(); + None + } + }) + .collect::>(); + + ProjectContext::new(worktrees, default_user_rules) + }) + } + + fn load_worktree_info_for_system_prompt( + worktree: Entity, + project: Entity, + cx: &mut App, + ) -> Task<(WorktreeContext, Option)> { + let tree = worktree.read(cx); + let root_name = tree.root_name_str().into(); + let abs_path = tree.abs_path(); + + let mut context = WorktreeContext { + root_name, + abs_path, + rules_file: None, + }; + + let rules_task = Self::load_worktree_rules_file(worktree, project, cx); + let Some(rules_task) = rules_task else { + return Task::ready((context, None)); + }; + + cx.spawn(async move |_| { + let (rules_file, rules_file_error) = match rules_task.await { + Ok(rules_file) => (Some(rules_file), None), + Err(err) => ( + None, + Some(RulesLoadingError { + message: format!("{err}").into(), + }), + ), + }; + context.rules_file = rules_file; + (context, rules_file_error) + }) + } + + fn load_worktree_rules_file( + worktree: Entity, + project: Entity, + cx: &mut App, + ) -> Option>> { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let selected_rules_file = RULES_FILE_NAMES + .into_iter() + .filter_map(|name| { + worktree + .entry_for_path(RelPath::unix(name).unwrap()) + .filter(|entry| entry.is_file()) + .map(|entry| entry.path.clone()) + }) + .next(); + + // Note that Cline supports `.clinerules` being a directory, but that is not currently + // supported. This doesn't seem to occur often in GitHub repositories. + selected_rules_file.map(|path_in_worktree| { + let project_path = ProjectPath { + worktree_id, + path: path_in_worktree.clone(), + }; + let buffer_task = + project.update(cx, |project, cx| project.open_buffer(project_path, cx)); + let rope_task = cx.spawn(async move |cx| { + buffer_task.await?.read_with(cx, |buffer, cx| { + let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; + anyhow::Ok((project_entry_id, buffer.as_rope().clone())) + })? + }); + // Build a string from the rope on a background thread. + cx.background_spawn(async move { + let (project_entry_id, rope) = rope_task.await?; + anyhow::Ok(RulesFileContext { + path_in_worktree, + text: rope.to_string().trim().to_string(), + project_entry_id: project_entry_id.to_usize(), + }) + }) + }) + } + + fn handle_thread_title_updated( + &mut self, + thread: Entity, + _: &TitleUpdated, + cx: &mut Context, + ) { + let session_id = thread.read(cx).id(); + let Some(session) = self.sessions.get(session_id) else { + return; + }; + let thread = thread.downgrade(); + let acp_thread = session.acp_thread.clone(); + cx.spawn(async move |_, cx| { + let title = thread.read_with(cx, |thread, _| thread.title())?; + let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; + task.await + }) + .detach_and_log_err(cx); + } + + fn handle_thread_token_usage_updated( + &mut self, + thread: Entity, + usage: &TokenUsageUpdated, + cx: &mut Context, + ) { + let Some(session) = self.sessions.get(thread.read(cx).id()) else { + return; + }; + session + .acp_thread + .update(cx, |acp_thread, cx| { + acp_thread.update_token_usage(usage.0.clone(), cx); + }) + .ok(); + } + + fn handle_project_event( + &mut self, + _project: Entity, + event: &project::Event, + _cx: &mut Context, + ) { + match event { + project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { + self.project_context_needs_refresh.send(()).ok(); + } + project::Event::WorktreeUpdatedEntries(_, items) => { + if items.iter().any(|(path, _, _)| { + RULES_FILE_NAMES + .iter() + .any(|name| path.as_ref() == RelPath::unix(name).unwrap()) + }) { + self.project_context_needs_refresh.send(()).ok(); + } + } + _ => {} + } + } + + fn handle_prompts_updated_event( + &mut self, + _prompt_store: Entity, + _event: &prompt_store::PromptsUpdatedEvent, + _cx: &mut Context, + ) { + self.project_context_needs_refresh.send(()).ok(); + } + + fn handle_models_updated_event( + &mut self, + _registry: Entity, + _event: &language_model::Event, + cx: &mut Context, + ) { + self.models.refresh_list(cx); + + let registry = LanguageModelRegistry::read_global(cx); + let default_model = registry.default_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model().map(|m| m.model); + + for session in self.sessions.values_mut() { + session.thread.update(cx, |thread, cx| { + if thread.model().is_none() + && let Some(model) = default_model.clone() + { + thread.set_model(model, cx); + cx.notify(); + } + thread.set_summarization_model(summarization_model.clone(), cx); + }); + } + } + + pub fn load_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context, + ) -> Task>> { + let database_future = ThreadsDatabase::connect(cx); + cx.spawn(async move |this, cx| { + let database = database_future.await.map_err(|err| anyhow!(err))?; + let db_thread = database + .load_thread(id.clone()) + .await? + .with_context(|| format!("no thread found with ID: {id:?}"))?; + + this.update(cx, |this, cx| { + let summarization_model = LanguageModelRegistry::read_global(cx) + .thread_summary_model() + .map(|c| c.model); + + cx.new(|cx| { + let mut thread = Thread::from_db( + id.clone(), + db_thread, + this.project.clone(), + this.project_context.clone(), + this.context_server_registry.clone(), + this.templates.clone(), + cx, + ); + thread.set_summarization_model(summarization_model, cx); + thread + }) + }) + }) + } + + pub fn open_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context, + ) -> Task>> { + let task = self.load_thread(id, cx); + cx.spawn(async move |this, cx| { + let thread = task.await?; + let acp_thread = + this.update(cx, |this, cx| this.register_session(thread.clone(), cx))?; + let events = thread.update(cx, |thread, cx| thread.replay(cx))?; + cx.update(|cx| { + NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx) + })? + .await?; + Ok(acp_thread) + }) + } + + pub fn thread_summary( + &mut self, + id: acp::SessionId, + cx: &mut Context, + ) -> Task> { + let thread = self.open_thread(id.clone(), cx); + cx.spawn(async move |this, cx| { + let acp_thread = thread.await?; + let result = this + .update(cx, |this, cx| { + this.sessions + .get(&id) + .unwrap() + .thread + .update(cx, |thread, cx| thread.summary(cx)) + })? + .await + .context("Failed to generate summary")?; + drop(acp_thread); + Ok(result) + }) + } + + fn save_thread(&mut self, thread: Entity, cx: &mut Context) { + if thread.read(cx).is_empty() { + return; + } + + let database_future = ThreadsDatabase::connect(cx); + let (id, db_thread) = + thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); + let Some(session) = self.sessions.get_mut(&id) else { + return; + }; + let history = self.history.clone(); + session.pending_save = cx.spawn(async move |_, cx| { + let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { + return; + }; + let db_thread = db_thread.await; + database.save_thread(id, db_thread).await.log_err(); + history.update(cx, |history, cx| history.reload(cx)).ok(); + }); + } +} + +/// Wrapper struct that implements the AgentConnection trait +#[derive(Clone)] +pub struct NativeAgentConnection(pub Entity); + +impl NativeAgentConnection { + pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option> { + self.0 + .read(cx) + .sessions + .get(session_id) + .map(|session| session.thread.clone()) + } + + pub fn load_thread(&self, id: acp::SessionId, cx: &mut App) -> Task>> { + self.0.update(cx, |this, cx| this.load_thread(id, cx)) + } + + fn run_turn( + &self, + session_id: acp::SessionId, + cx: &mut App, + f: impl 'static + + FnOnce(Entity, &mut App) -> Result>>, + ) -> Task> { + let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { + agent + .sessions + .get_mut(&session_id) + .map(|s| (s.thread.clone(), s.acp_thread.clone())) + }) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + log::debug!("Found session for: {}", session_id); + + let response_stream = match f(thread, cx) { + Ok(stream) => stream, + Err(err) => return Task::ready(Err(err)), + }; + Self::handle_thread_events(response_stream, acp_thread, cx) + } + + fn handle_thread_events( + mut events: mpsc::UnboundedReceiver>, + acp_thread: WeakEntity, + cx: &App, + ) -> Task> { + cx.spawn(async move |cx| { + // Handle response stream and forward to session.acp_thread + while let Some(result) = events.next().await { + match result { + Ok(event) => { + log::trace!("Received completion event: {:?}", event); + + match event { + ThreadEvent::UserMessage(message) => { + acp_thread.update(cx, |thread, cx| { + for content in message.content { + thread.push_user_content_block( + Some(message.id.clone()), + content.into(), + cx, + ); + } + })?; + } + ThreadEvent::AgentText(text) => { + acp_thread.update(cx, |thread, cx| { + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + meta: None, + }), + false, + cx, + ) + })?; + } + ThreadEvent::AgentThinking(text) => { + acp_thread.update(cx, |thread, cx| { + thread.push_assistant_content_block( + acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + meta: None, + }), + true, + cx, + ) + })?; + } + ThreadEvent::ToolCallAuthorization(ToolCallAuthorization { + tool_call, + options, + response, + }) => { + let outcome_task = acp_thread.update(cx, |thread, cx| { + thread.request_tool_call_authorization( + tool_call, options, true, cx, + ) + })??; + cx.background_spawn(async move { + if let acp::RequestPermissionOutcome::Selected { option_id } = + outcome_task.await + { + response + .send(option_id) + .map(|_| anyhow!("authorization receiver was dropped")) + .log_err(); + } + }) + .detach(); + } + ThreadEvent::ToolCall(tool_call) => { + acp_thread.update(cx, |thread, cx| { + thread.upsert_tool_call(tool_call, cx) + })??; + } + ThreadEvent::ToolCallUpdate(update) => { + acp_thread.update(cx, |thread, cx| { + thread.update_tool_call(update, cx) + })??; + } + ThreadEvent::Retry(status) => { + acp_thread.update(cx, |thread, cx| { + thread.update_retry_status(status, cx) + })?; + } + ThreadEvent::Stop(stop_reason) => { + log::debug!("Assistant message complete: {:?}", stop_reason); + return Ok(acp::PromptResponse { + stop_reason, + meta: None, + }); + } + } + } + Err(e) => { + log::error!("Error in model response stream: {:?}", e); + return Err(e); + } + } + } + + log::debug!("Response stream completed"); + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + meta: None, + }) + }) + } +} + +struct NativeAgentModelSelector { + session_id: acp::SessionId, + connection: NativeAgentConnection, +} + +impl acp_thread::AgentModelSelector for NativeAgentModelSelector { + fn list_models(&self, cx: &mut App) -> Task> { + log::debug!("NativeAgentConnection::list_models called"); + let list = self.connection.0.read(cx).models.model_list.clone(); + Task::ready(if list.is_empty() { + Err(anyhow::anyhow!("No models available")) + } else { + Ok(list) + }) + } + + fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task> { + log::debug!( + "Setting model for session {}: {}", + self.session_id, + model_id + ); + let Some(thread) = self + .connection + .0 + .read(cx) + .sessions + .get(&self.session_id) + .map(|session| session.thread.clone()) + else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + + let Some(model) = self.connection.0.read(cx).models.model_from_id(&model_id) else { + return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); + }; + + thread.update(cx, |thread, cx| { + thread.set_model(model.clone(), cx); + }); + + update_settings_file( + self.connection.0.read(cx).fs.clone(), + cx, + move |settings, _cx| { + let provider = model.provider_id().0.to_string(); + let model = model.id().0.to_string(); + settings + .agent + .get_or_insert_default() + .set_model(LanguageModelSelection { + provider: provider.into(), + model, + }); + }, + ); + + Task::ready(Ok(())) + } + + fn selected_model(&self, cx: &mut App) -> Task> { + let Some(thread) = self + .connection + .0 + .read(cx) + .sessions + .get(&self.session_id) + .map(|session| session.thread.clone()) + else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + let Some(model) = thread.read(cx).model() else { + return Task::ready(Err(anyhow!("Model not found"))); + }; + let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) + else { + return Task::ready(Err(anyhow!("Provider not found"))); + }; + Task::ready(Ok(LanguageModels::map_language_model_to_info( + model, &provider, + ))) + } + + fn watch(&self, cx: &mut App) -> Option> { + Some(self.connection.0.read(cx).models.watch()) + } +} + +impl acp_thread::AgentConnection for NativeAgentConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut App, + ) -> Task>> { + let agent = self.0.clone(); + log::debug!("Creating new thread for project at: {:?}", cwd); + + cx.spawn(async move |cx| { + log::debug!("Starting thread creation in async context"); + + // Create Thread + let thread = agent.update( + cx, + |agent, cx: &mut gpui::Context| -> Result<_> { + // Fetch default model from registry settings + let registry = LanguageModelRegistry::read_global(cx); + // Log available models for debugging + let available_count = registry.available_models(cx).count(); + log::debug!("Total available models: {}", available_count); + + let default_model = registry.default_model().and_then(|default_model| { + agent + .models + .model_from_id(&LanguageModels::model_id(&default_model.model)) + }); + Ok(cx.new(|cx| { + Thread::new( + project.clone(), + agent.project_context.clone(), + agent.context_server_registry.clone(), + agent.templates.clone(), + default_model, + cx, + ) + })) + }, + )??; + agent.update(cx, |agent, cx| agent.register_session(thread, cx)) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] // No auth for in-process + } + + fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task> { + Task::ready(Ok(())) + } + + fn model_selector(&self, session_id: &acp::SessionId) -> Option> { + Some(Rc::new(NativeAgentModelSelector { + session_id: session_id.clone(), + connection: self.clone(), + }) as Rc) + } + + fn prompt( + &self, + id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let id = id.expect("UserMessageId is required"); + let session_id = params.session_id.clone(); + log::info!("Received prompt request for session: {}", session_id); + log::debug!("Prompt blocks count: {}", params.prompt.len()); + + self.run_turn(session_id, cx, |thread, cx| { + let content: Vec = params + .prompt + .into_iter() + .map(Into::into) + .collect::>(); + log::debug!("Converted prompt to message: {} chars", content.len()); + log::debug!("Message id: {:?}", id); + log::debug!("Message content: {:?}", content); + + thread.update(cx, |thread, cx| thread.send(id, content, cx)) + }) + } + + fn resume( + &self, + session_id: &acp::SessionId, + _cx: &App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionResume { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + log::info!("Cancelling on session: {}", session_id); + self.0.update(cx, |agent, cx| { + if let Some(agent) = agent.sessions.get(session_id) { + agent.thread.update(cx, |thread, cx| thread.cancel(cx)); + } + }); + } + + fn truncate( + &self, + session_id: &agent_client_protocol::SessionId, + cx: &App, + ) -> Option> { + self.0.read_with(cx, |agent, _cx| { + agent.sessions.get(session_id).map(|session| { + Rc::new(NativeAgentSessionTruncate { + thread: session.thread.clone(), + acp_thread: session.acp_thread.clone(), + }) as _ + }) + }) + } + + fn set_title( + &self, + session_id: &acp::SessionId, + _cx: &App, + ) -> Option> { + Some(Rc::new(NativeAgentSessionSetTitle { + connection: self.clone(), + session_id: session_id.clone(), + }) as _) + } + + fn telemetry(&self) -> Option> { + Some(Rc::new(self.clone()) as Rc) + } + + fn into_any(self: Rc) -> Rc { + self + } +} + +impl acp_thread::AgentTelemetry for NativeAgentConnection { + fn agent_name(&self) -> String { + "Zed".into() + } + + fn thread_data( + &self, + session_id: &acp::SessionId, + cx: &mut App, + ) -> Task> { + let Some(session) = self.0.read(cx).sessions.get(session_id) else { + return Task::ready(Err(anyhow!("Session not found"))); + }; + + let task = session.thread.read(cx).to_db(cx); + cx.background_spawn(async move { + serde_json::to_value(task.await).context("Failed to serialize thread") + }) + } +} + +struct NativeAgentSessionTruncate { + thread: Entity, + acp_thread: WeakEntity, +} + +impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate { + fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { + match self.thread.update(cx, |thread, cx| { + thread.truncate(message_id.clone(), cx)?; + Ok(thread.latest_token_usage()) + }) { + Ok(usage) => { + self.acp_thread + .update(cx, |thread, cx| { + thread.update_token_usage(usage, cx); + }) + .ok(); + Task::ready(Ok(())) + } + Err(error) => Task::ready(Err(error)), + } + } +} + +struct NativeAgentSessionResume { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionResume for NativeAgentSessionResume { + fn run(&self, cx: &mut App) -> Task> { + self.connection + .run_turn(self.session_id.clone(), cx, |thread, cx| { + thread.update(cx, |thread, cx| thread.resume(cx)) + }) + } +} + +struct NativeAgentSessionSetTitle { + connection: NativeAgentConnection, + session_id: acp::SessionId, +} + +impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { + fn run(&self, title: SharedString, cx: &mut App) -> Task> { + let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else { + return Task::ready(Err(anyhow!("session not found"))); + }; + let thread = session.thread.clone(); + thread.update(cx, |thread, cx| thread.set_title(title, cx)); + Task::ready(Ok(())) + } +} + +pub struct AcpThreadEnvironment { + acp_thread: WeakEntity, +} + +impl ThreadEnvironment for AcpThreadEnvironment { + fn create_terminal( + &self, + command: String, + cwd: Option, + output_byte_limit: Option, + cx: &mut AsyncApp, + ) -> Task>> { + let task = self.acp_thread.update(cx, |thread, cx| { + thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx) + }); + + let acp_thread = self.acp_thread.clone(); + cx.spawn(async move |cx| { + let terminal = task?.await?; + + let (drop_tx, drop_rx) = oneshot::channel(); + let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?; + + cx.spawn(async move |cx| { + drop_rx.await.ok(); + acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx)) + }) + .detach(); + + let handle = AcpTerminalHandle { + terminal, + _drop_tx: Some(drop_tx), + }; + + Ok(Rc::new(handle) as _) + }) + } +} + +pub struct AcpTerminalHandle { + terminal: Entity, + _drop_tx: Option>, +} + +impl TerminalHandle for AcpTerminalHandle { + fn id(&self, cx: &AsyncApp) -> Result { + self.terminal.read_with(cx, |term, _cx| term.id().clone()) + } + + fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { + self.terminal + .read_with(cx, |term, _cx| term.wait_for_exit()) + } + + fn current_output(&self, cx: &AsyncApp) -> Result { + self.terminal + .read_with(cx, |term, cx| term.current_output(cx)) + } +} + +#[cfg(test)] +mod internal_tests { + use crate::HistoryEntryId; + + use super::*; + use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri}; + use fs::FakeFs; + use gpui::TestAppContext; + use indoc::formatdoc; + use language_model::fake_provider::FakeLanguageModel; + use serde_json::json; + use settings::SettingsStore; + use util::{path, rel_path::rel_path}; + + #[gpui::test] + async fn test_maintaining_project_context(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/", + json!({ + "a": {} + }), + ) + .await; + let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let agent = NativeAgent::new( + project.clone(), + history_store, + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + agent.read_with(cx, |agent, cx| { + assert_eq!(agent.project_context.read(cx).worktrees, vec![]) + }); + + let worktree = project + .update(cx, |project, cx| project.create_worktree("/a", true, cx)) + .await + .unwrap(); + cx.run_until_parked(); + agent.read_with(cx, |agent, cx| { + assert_eq!( + agent.project_context.read(cx).worktrees, + vec![WorktreeContext { + root_name: "a".into(), + abs_path: Path::new("/a").into(), + rules_file: None + }] + ) + }); + + // Creating `/a/.rules` updates the project context. + fs.insert_file("/a/.rules", Vec::new()).await; + cx.run_until_parked(); + agent.read_with(cx, |agent, cx| { + let rules_entry = worktree + .read(cx) + .entry_for_path(rel_path(".rules")) + .unwrap(); + assert_eq!( + agent.project_context.read(cx).worktrees, + vec![WorktreeContext { + root_name: "a".into(), + abs_path: Path::new("/a").into(), + rules_file: Some(RulesFileContext { + path_in_worktree: rel_path(".rules").into(), + text: "".into(), + project_entry_id: rules_entry.id.to_usize() + }) + }] + ) + }); + } + + #[gpui::test] + async fn test_listing_models(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree("/", json!({ "a": {} })).await; + let project = Project::test(fs.clone(), [], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let connection = NativeAgentConnection( + NativeAgent::new( + project.clone(), + history_store, + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(), + ); + + // Create a thread/session + let acp_thread = cx + .update(|cx| { + Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) + }) + .await + .unwrap(); + + let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); + + let models = cx + .update(|cx| { + connection + .model_selector(&session_id) + .unwrap() + .list_models(cx) + }) + .await + .unwrap(); + + let acp_thread::AgentModelList::Grouped(models) = models else { + panic!("Unexpected model group"); + }; + assert_eq!( + models, + IndexMap::from_iter([( + AgentModelGroupName("Fake".into()), + vec![AgentModelInfo { + id: acp::ModelId("fake/fake".into()), + name: "Fake".into(), + description: None, + icon: Some(ui::IconName::ZedAssistant), + }] + )]) + ); + } + + #[gpui::test] + async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.create_dir(paths::settings_file().parent().unwrap()) + .await + .unwrap(); + fs.insert_file( + paths::settings_file(), + json!({ + "agent": { + "default_model": { + "provider": "foo", + "model": "bar" + } + } + }) + .to_string() + .into_bytes(), + ) + .await; + let project = Project::test(fs.clone(), [], cx).await; + + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + + // Create the agent and connection + let agent = NativeAgent::new( + project.clone(), + history_store, + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + let connection = NativeAgentConnection(agent.clone()); + + // Create a thread/session + let acp_thread = cx + .update(|cx| { + Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) + }) + .await + .unwrap(); + + let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); + + // Select a model + let selector = connection.model_selector(&session_id).unwrap(); + let model_id = acp::ModelId("fake/fake".into()); + cx.update(|cx| selector.select_model(model_id.clone(), cx)) + .await + .unwrap(); + + // Verify the thread has the selected model + agent.read_with(cx, |agent, _| { + let session = agent.sessions.get(&session_id).unwrap(); + session.thread.read_with(cx, |thread, _| { + assert_eq!(thread.model().unwrap().id().0, "fake"); + }); + }); + + cx.run_until_parked(); + + // Verify settings file was updated + let settings_content = fs.load(paths::settings_file()).await.unwrap(); + let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap(); + + // Check that the agent settings contain the selected model + assert_eq!( + settings_json["agent"]["default_model"]["model"], + json!("fake") + ); + assert_eq!( + settings_json["agent"]["default_model"]["provider"], + json!("fake") + ); + } + + #[gpui::test] + async fn test_save_load_thread(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/", + json!({ + "a": { + "b.md": "Lorem" + } + }), + ) + .await; + let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; + let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let agent = NativeAgent::new( + project.clone(), + history_store.clone(), + Templates::new(), + None, + fs.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + let connection = Rc::new(NativeAgentConnection(agent.clone())); + + let acp_thread = cx + .update(|cx| { + connection + .clone() + .new_thread(project.clone(), Path::new(""), cx) + }) + .await + .unwrap(); + let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); + let thread = agent.read_with(cx, |agent, _| { + agent.sessions.get(&session_id).unwrap().thread.clone() + }); + + // Ensure empty threads are not saved, even if they get mutated. + let model = Arc::new(FakeLanguageModel::default()); + let summary_model = Arc::new(FakeLanguageModel::default()); + thread.update(cx, |thread, cx| { + thread.set_model(model.clone(), cx); + thread.set_summarization_model(Some(summary_model.clone()), cx); + }); + cx.run_until_parked(); + assert_eq!(history_entries(&history_store, cx), vec![]); + + let send = acp_thread.update(cx, |thread, cx| { + thread.send( + vec![ + "What does ".into(), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + name: "b.md".into(), + uri: MentionUri::File { + abs_path: path!("/a/b.md").into(), + } + .to_uri() + .to_string(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + meta: None, + }), + " mean?".into(), + ], + cx, + ) + }); + let send = cx.foreground_executor().spawn(send); + cx.run_until_parked(); + + model.send_last_completion_stream_text_chunk("Lorem."); + model.end_last_completion_stream(); + cx.run_until_parked(); + summary_model + .send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md"))); + summary_model.end_last_completion_stream(); + + send.await.unwrap(); + let uri = MentionUri::File { + abs_path: path!("/a/b.md").into(), + } + .to_uri(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + formatdoc! {" + ## User + + What does [@b.md]({uri}) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + + cx.run_until_parked(); + + // Drop the ACP thread, which should cause the session to be dropped as well. + cx.update(|_| { + drop(thread); + drop(acp_thread); + }); + agent.read_with(cx, |agent, _| { + assert_eq!(agent.sessions.keys().cloned().collect::>(), []); + }); + + // Ensure the thread can be reloaded from disk. + assert_eq!( + history_entries(&history_store, cx), + vec![( + HistoryEntryId::AcpThread(session_id.clone()), + format!("Explaining {}", path!("/a/b.md")) + )] + ); + let acp_thread = agent + .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) + .await + .unwrap(); + acp_thread.read_with(cx, |thread, cx| { + assert_eq!( + thread.to_markdown(cx), + formatdoc! {" + ## User + + What does [@b.md]({uri}) mean? + + ## Assistant + + Lorem. + + "} + ) + }); + } + + fn history_entries( + history: &Entity, + cx: &mut TestAppContext, + ) -> Vec<(HistoryEntryId, String)> { + history.read_with(cx, |history, _| { + history + .entries() + .map(|e| (e.id(), e.title().to_string())) + .collect::>() + }) + } -pub fn init(fs: Arc, cx: &mut gpui::App) { - thread_store::init(fs, cx); + fn init_test(cx: &mut TestAppContext) { + env_logger::try_init().ok(); + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + Project::init_settings(cx); + agent_settings::init(cx); + language::init(cx); + LanguageModelRegistry::test(cx); + }); + } } diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs deleted file mode 100644 index 40ba2f07db7ad425a5d0e9befe91499eb746b74e..0000000000000000000000000000000000000000 --- a/crates/agent/src/agent_profile.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::sync::Arc; - -use agent_settings::{AgentProfileId, AgentProfileSettings, AgentSettings}; -use assistant_tool::{Tool, ToolSource, ToolWorkingSet, UniqueToolName}; -use collections::IndexMap; -use convert_case::{Case, Casing}; -use fs::Fs; -use gpui::{App, Entity, SharedString}; -use settings::{Settings, update_settings_file}; -use util::ResultExt; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AgentProfile { - id: AgentProfileId, - tool_set: Entity, -} - -pub type AvailableProfiles = IndexMap; - -impl AgentProfile { - pub fn new(id: AgentProfileId, tool_set: Entity) -> Self { - Self { id, tool_set } - } - - /// Saves a new profile to the settings. - pub fn create( - name: String, - base_profile_id: Option, - fs: Arc, - cx: &App, - ) -> AgentProfileId { - let id = AgentProfileId(name.to_case(Case::Kebab).into()); - - let base_profile = - base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned()); - - let profile_settings = AgentProfileSettings { - name: name.into(), - tools: base_profile - .as_ref() - .map(|profile| profile.tools.clone()) - .unwrap_or_default(), - enable_all_context_servers: base_profile - .as_ref() - .map(|profile| profile.enable_all_context_servers) - .unwrap_or_default(), - context_servers: base_profile - .map(|profile| profile.context_servers) - .unwrap_or_default(), - }; - - update_settings_file(fs, cx, { - let id = id.clone(); - move |settings, _cx| { - profile_settings.save_to_settings(id, settings).log_err(); - } - }); - - id - } - - /// Returns a map of AgentProfileIds to their names - pub fn available_profiles(cx: &App) -> AvailableProfiles { - let mut profiles = AvailableProfiles::default(); - for (id, profile) in AgentSettings::get_global(cx).profiles.iter() { - profiles.insert(id.clone(), profile.name.clone()); - } - profiles - } - - pub fn id(&self) -> &AgentProfileId { - &self.id - } - - pub fn enabled_tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc)> { - let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else { - return Vec::new(); - }; - - self.tool_set - .read(cx) - .tools(cx) - .into_iter() - .filter(|(_, tool)| Self::is_enabled(settings, tool.source(), tool.name())) - .collect() - } - - pub fn is_tool_enabled(&self, source: ToolSource, tool_name: String, cx: &App) -> bool { - let Some(settings) = AgentSettings::get_global(cx).profiles.get(&self.id) else { - return false; - }; - - Self::is_enabled(settings, source, tool_name) - } - - fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool { - match source { - ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false), - ToolSource::ContextServer { id } => settings - .context_servers - .get(id.as_ref()) - .and_then(|preset| preset.tools.get(name.as_str()).copied()) - .unwrap_or(settings.enable_all_context_servers), - } - } -} - -#[cfg(test)] -mod tests { - use agent_settings::ContextServerPreset; - use assistant_tool::ToolRegistry; - use collections::IndexMap; - use gpui::SharedString; - use gpui::{AppContext, TestAppContext}; - use http_client::FakeHttpClient; - use project::Project; - use settings::{Settings, SettingsStore}; - - use super::*; - - #[gpui::test] - async fn test_enabled_built_in_tools_for_profile(cx: &mut TestAppContext) { - init_test_settings(cx); - - let id = AgentProfileId::default(); - let profile_settings = cx.read(|cx| { - AgentSettings::get_global(cx) - .profiles - .get(&id) - .unwrap() - .clone() - }); - let tool_set = default_tool_set(cx); - - let profile = AgentProfile::new(id, tool_set); - - let mut enabled_tools = cx - .read(|cx| profile.enabled_tools(cx)) - .into_iter() - .map(|(_, tool)| tool.name()) - .collect::>(); - enabled_tools.sort(); - - let mut expected_tools = profile_settings - .tools - .into_iter() - .filter_map(|(tool, enabled)| enabled.then_some(tool.to_string())) - // Provider dependent - .filter(|tool| tool != "web_search") - .collect::>(); - // Plus all registered MCP tools - expected_tools.extend(["enabled_mcp_tool".into(), "disabled_mcp_tool".into()]); - expected_tools.sort(); - - assert_eq!(enabled_tools, expected_tools); - } - - #[gpui::test] - async fn test_custom_mcp_settings(cx: &mut TestAppContext) { - init_test_settings(cx); - - let id = AgentProfileId("custom_mcp".into()); - let profile_settings = cx.read(|cx| { - AgentSettings::get_global(cx) - .profiles - .get(&id) - .unwrap() - .clone() - }); - let tool_set = default_tool_set(cx); - - let profile = AgentProfile::new(id, tool_set); - - let mut enabled_tools = cx - .read(|cx| profile.enabled_tools(cx)) - .into_iter() - .map(|(_, tool)| tool.name()) - .collect::>(); - enabled_tools.sort(); - - let mut expected_tools = profile_settings.context_servers["mcp"] - .tools - .iter() - .filter_map(|(key, enabled)| enabled.then(|| key.to_string())) - .collect::>(); - expected_tools.sort(); - - assert_eq!(enabled_tools, expected_tools); - } - - #[gpui::test] - async fn test_only_built_in(cx: &mut TestAppContext) { - init_test_settings(cx); - - let id = AgentProfileId("write_minus_mcp".into()); - let profile_settings = cx.read(|cx| { - AgentSettings::get_global(cx) - .profiles - .get(&id) - .unwrap() - .clone() - }); - let tool_set = default_tool_set(cx); - - let profile = AgentProfile::new(id, tool_set); - - let mut enabled_tools = cx - .read(|cx| profile.enabled_tools(cx)) - .into_iter() - .map(|(_, tool)| tool.name()) - .collect::>(); - enabled_tools.sort(); - - let mut expected_tools = profile_settings - .tools - .into_iter() - .filter_map(|(tool, enabled)| enabled.then_some(tool.to_string())) - // Provider dependent - .filter(|tool| tool != "web_search") - .collect::>(); - expected_tools.sort(); - - assert_eq!(enabled_tools, expected_tools); - } - - fn init_test_settings(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - Project::init_settings(cx); - AgentSettings::register(cx); - language_model::init_settings(cx); - ToolRegistry::default_global(cx); - assistant_tools::init(FakeHttpClient::with_404_response(), cx); - }); - - cx.update(|cx| { - let mut agent_settings = AgentSettings::get_global(cx).clone(); - agent_settings.profiles.insert( - AgentProfileId("write_minus_mcp".into()), - AgentProfileSettings { - name: "write_minus_mcp".into(), - enable_all_context_servers: false, - ..agent_settings.profiles[&AgentProfileId::default()].clone() - }, - ); - agent_settings.profiles.insert( - AgentProfileId("custom_mcp".into()), - AgentProfileSettings { - name: "mcp".into(), - tools: IndexMap::default(), - enable_all_context_servers: false, - context_servers: IndexMap::from_iter([("mcp".into(), context_server_preset())]), - }, - ); - AgentSettings::override_global(agent_settings, cx); - }) - } - - fn context_server_preset() -> ContextServerPreset { - ContextServerPreset { - tools: IndexMap::from_iter([ - ("enabled_mcp_tool".into(), true), - ("disabled_mcp_tool".into(), false), - ]), - } - } - - fn default_tool_set(cx: &mut TestAppContext) -> Entity { - cx.new(|cx| { - let mut tool_set = ToolWorkingSet::default(); - tool_set.insert(Arc::new(FakeTool::new("enabled_mcp_tool", "mcp")), cx); - tool_set.insert(Arc::new(FakeTool::new("disabled_mcp_tool", "mcp")), cx); - tool_set - }) - } - - struct FakeTool { - name: String, - source: SharedString, - } - - impl FakeTool { - fn new(name: impl Into, source: impl Into) -> Self { - Self { - name: name.into(), - source: source.into(), - } - } - } - - impl Tool for FakeTool { - fn name(&self) -> String { - self.name.clone() - } - - fn source(&self) -> ToolSource { - ToolSource::ContextServer { - id: self.source.clone(), - } - } - - fn description(&self) -> String { - unimplemented!() - } - - fn icon(&self) -> icons::IconName { - unimplemented!() - } - - fn needs_confirmation( - &self, - _input: &serde_json::Value, - _project: &Entity, - _cx: &App, - ) -> bool { - unimplemented!() - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - unimplemented!() - } - - fn run( - self: Arc, - _input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - _cx: &mut App, - ) -> assistant_tool::ToolResult { - unimplemented!() - } - - fn may_perform_edits(&self) -> bool { - unimplemented!() - } - } -} diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs deleted file mode 100644 index 696c569356bca36adf54bc84ec52fa7295048b75..0000000000000000000000000000000000000000 --- a/crates/agent/src/context_server_tool.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::sync::Arc; - -use action_log::ActionLog; -use anyhow::{Result, anyhow, bail}; -use assistant_tool::{Tool, ToolResult, ToolSource}; -use context_server::{ContextServerId, types}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use icons::IconName; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::{Project, context_server_store::ContextServerStore}; - -pub struct ContextServerTool { - store: Entity, - server_id: ContextServerId, - tool: types::Tool, -} - -impl ContextServerTool { - pub fn new( - store: Entity, - server_id: ContextServerId, - tool: types::Tool, - ) -> Self { - Self { - store, - server_id, - tool, - } - } -} - -impl Tool for ContextServerTool { - fn name(&self) -> String { - self.tool.name.clone() - } - - fn description(&self) -> String { - self.tool.description.clone().unwrap_or_default() - } - - fn icon(&self) -> IconName { - IconName::ToolHammer - } - - fn source(&self) -> ToolSource { - ToolSource::ContextServer { - id: self.server_id.clone().0.into(), - } - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true - } - - fn may_perform_edits(&self) -> bool { - true - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - let mut schema = self.tool.input_schema.clone(); - assistant_tool::adapt_schema_to_format(&mut schema, format)?; - Ok(match schema { - serde_json::Value::Null => { - serde_json::json!({ "type": "object", "properties": [] }) - } - serde_json::Value::Object(map) if map.is_empty() => { - serde_json::json!({ "type": "object", "properties": [] }) - } - _ => schema, - }) - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - format!("Run MCP tool `{}`", self.tool.name) - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - if let Some(server) = self.store.read(cx).get_running_server(&self.server_id) { - let tool_name = self.tool.name.clone(); - - cx.spawn(async move |_cx| { - let Some(protocol) = server.client() else { - bail!("Context server not initialized"); - }; - - let arguments = if let serde_json::Value::Object(map) = input { - Some(map.into_iter().collect()) - } else { - None - }; - - log::trace!( - "Running tool: {} with arguments: {:?}", - tool_name, - arguments - ); - let response = protocol - .request::( - context_server::types::CallToolParams { - name: tool_name, - arguments, - meta: None, - }, - ) - .await?; - - let mut result = String::new(); - for content in response.content { - match content { - types::ToolResponseContent::Text { text } => { - result.push_str(&text); - } - types::ToolResponseContent::Image { .. } => { - log::warn!("Ignoring image content from tool response"); - } - types::ToolResponseContent::Audio { .. } => { - log::warn!("Ignoring audio content from tool response"); - } - types::ToolResponseContent::Resource { .. } => { - log::warn!("Ignoring resource content from tool response"); - } - } - } - Ok(result.into()) - }) - .into() - } else { - Task::ready(Err(anyhow!("Context server not found"))).into() - } - } -} diff --git a/crates/agent2/src/db.rs b/crates/agent/src/db.rs similarity index 78% rename from crates/agent2/src/db.rs rename to crates/agent/src/db.rs index 563ccdd7ca5b2c2cc63a8c7f30c59b9443f8a0bd..c72e20571e2761788157a5fd10df147c2b414e4a 100644 --- a/crates/agent2/src/db.rs +++ b/crates/agent/src/db.rs @@ -1,6 +1,5 @@ use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; use acp_thread::UserMessageId; -use agent::{thread::DetailedSummaryState, thread_store}; use agent_client_protocol as acp; use agent_settings::{AgentProfileId, CompletionMode}; use anyhow::{Result, anyhow}; @@ -21,8 +20,8 @@ use ui::{App, SharedString}; use zed_env_vars::ZED_STATELESS; pub type DbMessage = crate::Message; -pub type DbSummary = DetailedSummaryState; -pub type DbLanguageModel = thread_store::SerializedLanguageModel; +pub type DbSummary = crate::legacy_thread::DetailedSummaryState; +pub type DbLanguageModel = crate::legacy_thread::SerializedLanguageModel; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DbThreadMetadata { @@ -40,7 +39,7 @@ pub struct DbThread { #[serde(default)] pub detailed_summary: Option, #[serde(default)] - pub initial_project_snapshot: Option>, + pub initial_project_snapshot: Option>, #[serde(default)] pub cumulative_token_usage: language_model::TokenUsage, #[serde(default)] @@ -61,13 +60,17 @@ impl DbThread { match saved_thread_json.get("version") { Some(serde_json::Value::String(version)) => match version.as_str() { Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?), - _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), + _ => Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json( + json, + )?), }, - _ => Self::upgrade_from_agent_1(agent::SerializedThread::from_json(json)?), + _ => { + Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(json)?) + } } } - fn upgrade_from_agent_1(thread: agent::SerializedThread) -> Result { + fn upgrade_from_agent_1(thread: crate::legacy_thread::SerializedThread) -> Result { let mut messages = Vec::new(); let mut request_token_usage = HashMap::default(); @@ -80,14 +83,19 @@ impl DbThread { // Convert segments to content for segment in msg.segments { match segment { - thread_store::SerializedMessageSegment::Text { text } => { + crate::legacy_thread::SerializedMessageSegment::Text { text } => { content.push(UserMessageContent::Text(text)); } - thread_store::SerializedMessageSegment::Thinking { text, .. } => { + crate::legacy_thread::SerializedMessageSegment::Thinking { + text, + .. + } => { // User messages don't have thinking segments, but handle gracefully content.push(UserMessageContent::Text(text)); } - thread_store::SerializedMessageSegment::RedactedThinking { .. } => { + crate::legacy_thread::SerializedMessageSegment::RedactedThinking { + .. + } => { // User messages don't have redacted thinking, skip. } } @@ -113,16 +121,18 @@ impl DbThread { // Convert segments to content for segment in msg.segments { match segment { - thread_store::SerializedMessageSegment::Text { text } => { + crate::legacy_thread::SerializedMessageSegment::Text { text } => { content.push(AgentMessageContent::Text(text)); } - thread_store::SerializedMessageSegment::Thinking { + crate::legacy_thread::SerializedMessageSegment::Thinking { text, signature, } => { content.push(AgentMessageContent::Thinking { text, signature }); } - thread_store::SerializedMessageSegment::RedactedThinking { data } => { + crate::legacy_thread::SerializedMessageSegment::RedactedThinking { + data, + } => { content.push(AgentMessageContent::RedactedThinking(data)); } } @@ -187,10 +197,9 @@ impl DbThread { messages, updated_at: thread.updated_at, detailed_summary: match thread.detailed_summary_state { - DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => { - None - } - DetailedSummaryState::Generated { text, .. } => Some(text), + crate::legacy_thread::DetailedSummaryState::NotGenerated + | crate::legacy_thread::DetailedSummaryState::Generating => None, + crate::legacy_thread::DetailedSummaryState::Generated { text, .. } => Some(text), }, initial_project_snapshot: thread.initial_project_snapshot, cumulative_token_usage: thread.cumulative_token_usage, @@ -414,84 +423,3 @@ impl ThreadsDatabase { }) } } - -#[cfg(test)] -mod tests { - - use super::*; - use agent::MessageSegment; - use agent::context::LoadedContext; - use client::Client; - use fs::{FakeFs, Fs}; - use gpui::AppContext; - use gpui::TestAppContext; - use http_client::FakeHttpClient; - use language_model::Role; - use project::Project; - use settings::SettingsStore; - - fn init_test(fs: Arc, cx: &mut TestAppContext) { - env_logger::try_init().ok(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - Project::init_settings(cx); - language::init(cx); - - let http_client = FakeHttpClient::with_404_response(); - let clock = Arc::new(clock::FakeSystemClock::new()); - let client = Client::new(clock, http_client, cx); - agent::init(fs, cx); - agent_settings::init(cx); - language_model::init(client, cx); - }); - } - - #[gpui::test] - async fn test_retrieving_old_thread(cx: &mut TestAppContext) { - let fs = FakeFs::new(cx.executor()); - init_test(fs.clone(), cx); - let project = Project::test(fs, [], cx).await; - - // Save a thread using the old agent. - let thread_store = cx.new(|cx| agent::ThreadStore::fake(project, cx)); - let thread = thread_store.update(cx, |thread_store, cx| thread_store.create_thread(cx)); - thread.update(cx, |thread, cx| { - thread.insert_message( - Role::User, - vec![MessageSegment::Text("Hey!".into())], - LoadedContext::default(), - vec![], - false, - cx, - ); - thread.insert_message( - Role::Assistant, - vec![MessageSegment::Text("How're you doing?".into())], - LoadedContext::default(), - vec![], - false, - cx, - ) - }); - thread_store - .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) - .await - .unwrap(); - - // Open that same thread using the new agent. - let db = cx.update(ThreadsDatabase::connect).await.unwrap(); - let threads = db.list_threads().await.unwrap(); - assert_eq!(threads.len(), 1); - let thread = db - .load_thread(threads[0].id.clone()) - .await - .unwrap() - .unwrap(); - assert_eq!(thread.messages[0].to_markdown(), "## User\n\nHey!\n"); - assert_eq!( - thread.messages[1].to_markdown(), - "## Assistant\n\nHow're you doing?\n" - ); - } -} diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/agent/src/edit_agent.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent.rs rename to crates/agent/src/edit_agent.rs diff --git a/crates/assistant_tools/src/edit_agent/create_file_parser.rs b/crates/agent/src/edit_agent/create_file_parser.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/create_file_parser.rs rename to crates/agent/src/edit_agent/create_file_parser.rs diff --git a/crates/assistant_tools/src/edit_agent/edit_parser.rs b/crates/agent/src/edit_agent/edit_parser.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/edit_parser.rs rename to crates/agent/src/edit_agent/edit_parser.rs diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/agent/src/edit_agent/evals.rs similarity index 97% rename from crates/assistant_tools/src/edit_agent/evals.rs rename to crates/agent/src/edit_agent/evals.rs index 515e22d5f8b184a875cd91038d7bfa0a7d8127a7..b3043f0a81256568338f5d4be22bfe02de277076 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/agent/src/edit_agent/evals.rs @@ -1,12 +1,8 @@ use super::*; use crate::{ - ReadFileToolInput, - edit_file_tool::{EditFileMode, EditFileToolInput}, - grep_tool::GrepToolInput, - list_directory_tool::ListDirectoryToolInput, + EditFileMode, EditFileToolInput, GrepToolInput, ListDirectoryToolInput, ReadFileToolInput, }; use Role::*; -use assistant_tool::ToolRegistry; use client::{Client, UserStore}; use collections::HashMap; use fs::FakeFs; @@ -15,11 +11,11 @@ use gpui::{AppContext, TestAppContext, Timer}; use http_client::StatusCode; use indoc::{formatdoc, indoc}; use language_model::{ - LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, - LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel, + LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolResultContent, + LanguageModelToolUse, LanguageModelToolUseId, SelectedModel, }; use project::Project; -use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext}; +use prompt_store::{ProjectContext, WorktreeContext}; use rand::prelude::*; use reqwest_client::ReqwestClient; use serde_json::json; @@ -121,6 +117,7 @@ fn eval_delete_run_git_blame() { // gemini-2.5-pro-06-05 | 1.0 (2025-06-16) // gemini-2.5-flash | // gpt-4.1 | + let input_file_path = "root/blame.rs"; let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); @@ -184,6 +181,7 @@ fn eval_translate_doc_comments() { // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) // gemini-2.5-flash-preview-04-17 | // gpt-4.1 | + let input_file_path = "root/canvas.rs"; let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs"); let edit_description = "Translate all doc comments to Italian"; @@ -246,6 +244,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { // gemini-2.5-pro-preview-latest | 0.99 (2025-06-16) // gemini-2.5-flash-preview-04-17 | // gpt-4.1 | + let input_file_path = "root/lib.rs"; let input_file_content = include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs"); @@ -371,6 +370,7 @@ fn eval_disable_cursor_blinking() { // gemini-2.5-pro | 0.95 (2025-07-14) // gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14) // gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally) + let input_file_path = "root/editor.rs"; let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs"); let edit_description = "Comment out the call to `BlinkManager::enable`"; @@ -463,6 +463,7 @@ fn eval_from_pixels_constructor() { // claude-3.7-sonnet | 2025-06-14 | 0.88 // gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98 // gpt-4.1 | + let input_file_path = "root/canvas.rs"; let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); let edit_description = "Implement from_pixels constructor and add tests."; @@ -665,6 +666,7 @@ fn eval_zode() { // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22) // gpt-4.1 | 1.0 (2025-05-22) + let input_file_path = "root/zode.py"; let input_content = None; let edit_description = "Create the main Zode CLI script"; @@ -771,6 +773,7 @@ fn eval_add_overwrite_test() { // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22) // gemini-2.5-flash-preview-04-17 | // gpt-4.1 | + let input_file_path = "root/action_log.rs"; let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs"); let edit_description = "Add a new test for overwriting a file in action_log.rs"; @@ -1010,7 +1013,7 @@ fn eval_create_empty_file() { // // TODO: gpt-4.1-mini errored 38 times: // "data did not match any variant of untagged enum ResponseStreamResult" - // + let input_file_content = None; let expected_output_content = String::new(); eval( @@ -1475,19 +1478,16 @@ impl EditAgentTest { language::init(cx); language_model::init(client.clone(), cx); language_models::init(user_store, client.clone(), cx); - crate::init(client.http_client(), cx); }); fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let agent_model = SelectedModel::from_str( - &std::env::var("ZED_AGENT_MODEL") - .unwrap_or("anthropic/claude-3-7-sonnet-latest".into()), + &std::env::var("ZED_AGENT_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()), ) .unwrap(); let judge_model = SelectedModel::from_str( - &std::env::var("ZED_JUDGE_MODEL") - .unwrap_or("anthropic/claude-3-7-sonnet-latest".into()), + &std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()), ) .unwrap(); let (agent_model, judge_model) = cx @@ -1553,39 +1553,27 @@ impl EditAgentTest { .update(cx, |project, cx| project.open_buffer(path, cx)) .await .unwrap(); - let tools = cx.update(|cx| { - ToolRegistry::default_global(cx) - .tools() - .into_iter() - .filter_map(|tool| { - let input_schema = tool - .input_schema(self.agent.model.tool_input_format()) - .ok()?; - Some(LanguageModelRequestTool { - name: tool.name(), - description: tool.description(), - input_schema, - }) - }) - .collect::>() - }); - let tool_names = tools - .iter() - .map(|tool| tool.name.clone()) - .collect::>(); - let worktrees = vec![WorktreeContext { - root_name: "root".to_string(), - abs_path: Path::new("/path/to/root").into(), - rules_file: None, - }]; - let prompt_builder = PromptBuilder::new(None)?; - let project_context = ProjectContext::new(worktrees, Vec::default()); - let system_prompt = prompt_builder.generate_assistant_system_prompt( - &project_context, - &ModelContext { + + let tools = crate::built_in_tools().collect::>(); + + let system_prompt = { + let worktrees = vec![WorktreeContext { + root_name: "root".to_string(), + abs_path: Path::new("/path/to/root").into(), + rules_file: None, + }]; + let project_context = ProjectContext::new(worktrees, Vec::default()); + let tool_names = tools + .iter() + .map(|tool| tool.name.clone().into()) + .collect::>(); + let template = crate::SystemPromptTemplate { + project: &project_context, available_tools: tool_names, - }, - )?; + }; + let templates = Templates::new(); + template.render(&templates).unwrap() + }; let has_system_prompt = eval .conversation diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs b/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs b/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs rename to crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs b/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff rename to crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff rename to crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff rename to crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff rename to crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff rename to crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs b/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs b/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs b/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs rename to crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/zode/prompt.md b/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/zode/prompt.md rename to crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/zode/react.py b/crates/agent/src/edit_agent/evals/fixtures/zode/react.py similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/zode/react.py rename to crates/agent/src/edit_agent/evals/fixtures/zode/react.py diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/zode/react_test.py b/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py similarity index 100% rename from crates/assistant_tools/src/edit_agent/evals/fixtures/zode/react_test.py rename to crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py diff --git a/crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs similarity index 100% rename from crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs rename to crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs diff --git a/crates/agent2/src/history_store.rs b/crates/agent/src/history_store.rs similarity index 80% rename from crates/agent2/src/history_store.rs rename to crates/agent/src/history_store.rs index ff6caacc78e5dba4ee38f160fa6ded7fcb45a845..c342110f3ee289b6e84241517b69fe9a86efcf16 100644 --- a/crates/agent2/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -1,4 +1,4 @@ -use crate::{DbThreadMetadata, ThreadsDatabase}; +use crate::{DbThread, DbThreadMetadata, ThreadsDatabase}; use acp_thread::MentionUri; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; @@ -8,8 +8,9 @@ use db::kvp::KEY_VALUE_STORE; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; use itertools::Itertools; use paths::contexts_dir; +use project::Project; use serde::{Deserialize, Serialize}; -use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration}; +use std::{collections::VecDeque, path::Path, rc::Rc, sync::Arc, time::Duration}; use ui::ElementId; use util::ResultExt as _; @@ -19,6 +20,33 @@ const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50 const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); +//todo: We should remove this function once we support loading all acp thread +pub fn load_agent_thread( + session_id: acp::SessionId, + history_store: Entity, + project: Entity, + cx: &mut App, +) -> Task>> { + use agent_servers::{AgentServer, AgentServerDelegate}; + + let server = Rc::new(crate::NativeAgentServer::new( + project.read(cx).fs().clone(), + history_store, + )); + let delegate = AgentServerDelegate::new( + project.read(cx).agent_server_store().clone(), + project.clone(), + None, + None, + ); + let connection = server.connect(None, delegate, cx); + cx.spawn(async move |cx| { + let (agent, _) = connection.await?; + let agent = agent.downcast::().unwrap(); + cx.update(|cx| agent.load_thread(session_id, cx))?.await + }) +} + #[derive(Clone, Debug)] pub enum HistoryEntry { AcpThread(DbThreadMetadata), @@ -55,8 +83,13 @@ impl HistoryEntry { pub fn title(&self) -> &SharedString { match self { - HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE, - HistoryEntry::AcpThread(thread) => &thread.title, + HistoryEntry::AcpThread(thread) => { + if thread.title.is_empty() { + DEFAULT_TITLE + } else { + &thread.title + } + } HistoryEntry::TextThread(context) => &context.title, } } @@ -87,7 +120,7 @@ enum SerializedRecentOpen { pub struct HistoryStore { threads: Vec, entries: Vec, - context_store: Entity, + text_thread_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, _save_recently_opened_entries_task: Task<()>, @@ -95,10 +128,11 @@ pub struct HistoryStore { impl HistoryStore { pub fn new( - context_store: Entity, + text_thread_store: Entity, cx: &mut Context, ) -> Self { - let subscriptions = vec![cx.observe(&context_store, |this, _, cx| this.update_entries(cx))]; + let subscriptions = + vec![cx.observe(&text_thread_store, |this, _, cx| this.update_entries(cx))]; cx.spawn(async move |this, cx| { let entries = Self::load_recently_opened_entries(cx).await; @@ -114,7 +148,7 @@ impl HistoryStore { .detach(); Self { - context_store, + text_thread_store, recently_opened_entries: VecDeque::default(), threads: Vec::default(), entries: Vec::default(), @@ -127,6 +161,18 @@ impl HistoryStore { self.threads.iter().find(|thread| &thread.id == session_id) } + pub fn load_thread( + &mut self, + id: acp::SessionId, + cx: &mut Context, + ) -> Task>> { + let database_future = ThreadsDatabase::connect(cx); + cx.background_spawn(async move { + let database = database_future.await.map_err(|err| anyhow!(err))?; + database.load_thread(id).await + }) + } + pub fn delete_thread( &mut self, id: acp::SessionId, @@ -145,9 +191,8 @@ impl HistoryStore { path: Arc, cx: &mut Context, ) -> Task> { - self.context_store.update(cx, |context_store, cx| { - context_store.delete_local_context(path, cx) - }) + self.text_thread_store + .update(cx, |store, cx| store.delete_local_context(path, cx)) } pub fn load_text_thread( @@ -155,9 +200,8 @@ impl HistoryStore { path: Arc, cx: &mut Context, ) -> Task>> { - self.context_store.update(cx, |context_store, cx| { - context_store.open_local_context(path, cx) - }) + self.text_thread_store + .update(cx, |store, cx| store.open_local_context(path, cx)) } pub fn reload(&self, cx: &mut Context) { @@ -197,7 +241,7 @@ impl HistoryStore { let mut history_entries = Vec::new(); history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); history_entries.extend( - self.context_store + self.text_thread_store .read(cx) .unordered_contexts() .cloned() @@ -231,21 +275,21 @@ impl HistoryStore { }) }); - let context_entries = - self.context_store - .read(cx) - .unordered_contexts() - .flat_map(|context| { - self.recently_opened_entries - .iter() - .enumerate() - .flat_map(|(index, entry)| match entry { - HistoryEntryId::TextThread(path) if &context.path == path => { - Some((index, HistoryEntry::TextThread(context.clone()))) - } - _ => None, - }) - }); + let context_entries = self + .text_thread_store + .read(cx) + .unordered_contexts() + .flat_map(|context| { + self.recently_opened_entries + .iter() + .enumerate() + .flat_map(|(index, entry)| match entry { + HistoryEntryId::TextThread(path) if &context.path == path => { + Some((index, HistoryEntry::TextThread(context.clone()))) + } + _ => None, + }) + }); thread_entries .chain(context_entries) diff --git a/crates/agent/src/legacy_thread.rs b/crates/agent/src/legacy_thread.rs new file mode 100644 index 0000000000000000000000000000000000000000..34babb800616e7a3d5390abdaccc0cafa24ff386 --- /dev/null +++ b/crates/agent/src/legacy_thread.rs @@ -0,0 +1,402 @@ +use crate::ProjectSnapshot; +use agent_settings::{AgentProfileId, CompletionMode}; +use anyhow::Result; +use chrono::{DateTime, Utc}; +use gpui::SharedString; +use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub enum DetailedSummaryState { + #[default] + NotGenerated, + Generating, + Generated { + text: SharedString, + }, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] +pub struct MessageId(pub usize); + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SerializedThread { + pub version: String, + pub summary: SharedString, + pub updated_at: DateTime, + pub messages: Vec, + #[serde(default)] + pub initial_project_snapshot: Option>, + #[serde(default)] + pub cumulative_token_usage: TokenUsage, + #[serde(default)] + pub request_token_usage: Vec, + #[serde(default)] + pub detailed_summary_state: DetailedSummaryState, + #[serde(default)] + pub model: Option, + #[serde(default)] + pub completion_mode: Option, + #[serde(default)] + pub tool_use_limit_reached: bool, + #[serde(default)] + pub profile: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SerializedLanguageModel { + pub provider: String, + pub model: String, +} + +impl SerializedThread { + pub const VERSION: &'static str = "0.2.0"; + + pub fn from_json(json: &[u8]) -> Result { + let saved_thread_json = serde_json::from_slice::(json)?; + match saved_thread_json.get("version") { + Some(serde_json::Value::String(version)) => match version.as_str() { + SerializedThreadV0_1_0::VERSION => { + let saved_thread = + serde_json::from_value::(saved_thread_json)?; + Ok(saved_thread.upgrade()) + } + SerializedThread::VERSION => Ok(serde_json::from_value::( + saved_thread_json, + )?), + _ => anyhow::bail!("unrecognized serialized thread version: {version:?}"), + }, + None => { + let saved_thread = + serde_json::from_value::(saved_thread_json)?; + Ok(saved_thread.upgrade()) + } + version => anyhow::bail!("unrecognized serialized thread version: {version:?}"), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SerializedThreadV0_1_0( + // The structure did not change, so we are reusing the latest SerializedThread. + // When making the next version, make sure this points to SerializedThreadV0_2_0 + SerializedThread, +); + +impl SerializedThreadV0_1_0 { + pub const VERSION: &'static str = "0.1.0"; + + pub fn upgrade(self) -> SerializedThread { + debug_assert_eq!(SerializedThread::VERSION, "0.2.0"); + + let mut messages: Vec = Vec::with_capacity(self.0.messages.len()); + + for message in self.0.messages { + if message.role == Role::User + && !message.tool_results.is_empty() + && let Some(last_message) = messages.last_mut() + { + debug_assert!(last_message.role == Role::Assistant); + + last_message.tool_results = message.tool_results; + continue; + } + + messages.push(message); + } + + SerializedThread { + messages, + version: SerializedThread::VERSION.to_string(), + ..self.0 + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct SerializedMessage { + pub id: MessageId, + pub role: Role, + #[serde(default)] + pub segments: Vec, + #[serde(default)] + pub tool_uses: Vec, + #[serde(default)] + pub tool_results: Vec, + #[serde(default)] + pub context: String, + #[serde(default)] + pub creases: Vec, + #[serde(default)] + pub is_hidden: bool, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type")] +pub enum SerializedMessageSegment { + #[serde(rename = "text")] + Text { + text: String, + }, + #[serde(rename = "thinking")] + Thinking { + text: String, + #[serde(skip_serializing_if = "Option::is_none")] + signature: Option, + }, + RedactedThinking { + data: String, + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct SerializedToolUse { + pub id: LanguageModelToolUseId, + pub name: SharedString, + pub input: serde_json::Value, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct SerializedToolResult { + pub tool_use_id: LanguageModelToolUseId, + pub is_error: bool, + pub content: LanguageModelToolResultContent, + pub output: Option, +} + +#[derive(Serialize, Deserialize)] +struct LegacySerializedThread { + pub summary: SharedString, + pub updated_at: DateTime, + pub messages: Vec, + #[serde(default)] + pub initial_project_snapshot: Option>, +} + +impl LegacySerializedThread { + pub fn upgrade(self) -> SerializedThread { + SerializedThread { + version: SerializedThread::VERSION.to_string(), + summary: self.summary, + updated_at: self.updated_at, + messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(), + initial_project_snapshot: self.initial_project_snapshot, + cumulative_token_usage: TokenUsage::default(), + request_token_usage: Vec::new(), + detailed_summary_state: DetailedSummaryState::default(), + model: None, + completion_mode: None, + tool_use_limit_reached: false, + profile: None, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +struct LegacySerializedMessage { + pub id: MessageId, + pub role: Role, + pub text: String, + #[serde(default)] + pub tool_uses: Vec, + #[serde(default)] + pub tool_results: Vec, +} + +impl LegacySerializedMessage { + fn upgrade(self) -> SerializedMessage { + SerializedMessage { + id: self.id, + role: self.role, + segments: vec![SerializedMessageSegment::Text { text: self.text }], + tool_uses: self.tool_uses, + tool_results: self.tool_results, + context: String::new(), + creases: Vec::new(), + is_hidden: false, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +pub struct SerializedCrease { + pub start: usize, + pub end: usize, + pub icon_path: SharedString, + pub label: SharedString, +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + use language_model::{Role, TokenUsage}; + use pretty_assertions::assert_eq; + + #[test] + fn test_legacy_serialized_thread_upgrade() { + let updated_at = Utc::now(); + let legacy_thread = LegacySerializedThread { + summary: "Test conversation".into(), + updated_at, + messages: vec![LegacySerializedMessage { + id: MessageId(1), + role: Role::User, + text: "Hello, world!".to_string(), + tool_uses: vec![], + tool_results: vec![], + }], + initial_project_snapshot: None, + }; + + let upgraded = legacy_thread.upgrade(); + + assert_eq!( + upgraded, + SerializedThread { + summary: "Test conversation".into(), + updated_at, + messages: vec![SerializedMessage { + id: MessageId(1), + role: Role::User, + segments: vec![SerializedMessageSegment::Text { + text: "Hello, world!".to_string() + }], + tool_uses: vec![], + tool_results: vec![], + context: "".to_string(), + creases: vec![], + is_hidden: false + }], + version: SerializedThread::VERSION.to_string(), + initial_project_snapshot: None, + cumulative_token_usage: TokenUsage::default(), + request_token_usage: vec![], + detailed_summary_state: DetailedSummaryState::default(), + model: None, + completion_mode: None, + tool_use_limit_reached: false, + profile: None + } + ) + } + + #[test] + fn test_serialized_threadv0_1_0_upgrade() { + let updated_at = Utc::now(); + let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread { + summary: "Test conversation".into(), + updated_at, + messages: vec![ + SerializedMessage { + id: MessageId(1), + role: Role::User, + segments: vec![SerializedMessageSegment::Text { + text: "Use tool_1".to_string(), + }], + tool_uses: vec![], + tool_results: vec![], + context: "".to_string(), + creases: vec![], + is_hidden: false, + }, + SerializedMessage { + id: MessageId(2), + role: Role::Assistant, + segments: vec![SerializedMessageSegment::Text { + text: "I want to use a tool".to_string(), + }], + tool_uses: vec![SerializedToolUse { + id: "abc".into(), + name: "tool_1".into(), + input: serde_json::Value::Null, + }], + tool_results: vec![], + context: "".to_string(), + creases: vec![], + is_hidden: false, + }, + SerializedMessage { + id: MessageId(1), + role: Role::User, + segments: vec![SerializedMessageSegment::Text { + text: "Here is the tool result".to_string(), + }], + tool_uses: vec![], + tool_results: vec![SerializedToolResult { + tool_use_id: "abc".into(), + is_error: false, + content: LanguageModelToolResultContent::Text("abcdef".into()), + output: Some(serde_json::Value::Null), + }], + context: "".to_string(), + creases: vec![], + is_hidden: false, + }, + ], + version: SerializedThreadV0_1_0::VERSION.to_string(), + initial_project_snapshot: None, + cumulative_token_usage: TokenUsage::default(), + request_token_usage: vec![], + detailed_summary_state: DetailedSummaryState::default(), + model: None, + completion_mode: None, + tool_use_limit_reached: false, + profile: None, + }); + let upgraded = thread_v0_1_0.upgrade(); + + assert_eq!( + upgraded, + SerializedThread { + summary: "Test conversation".into(), + updated_at, + messages: vec![ + SerializedMessage { + id: MessageId(1), + role: Role::User, + segments: vec![SerializedMessageSegment::Text { + text: "Use tool_1".to_string() + }], + tool_uses: vec![], + tool_results: vec![], + context: "".to_string(), + creases: vec![], + is_hidden: false + }, + SerializedMessage { + id: MessageId(2), + role: Role::Assistant, + segments: vec![SerializedMessageSegment::Text { + text: "I want to use a tool".to_string(), + }], + tool_uses: vec![SerializedToolUse { + id: "abc".into(), + name: "tool_1".into(), + input: serde_json::Value::Null, + }], + tool_results: vec![SerializedToolResult { + tool_use_id: "abc".into(), + is_error: false, + content: LanguageModelToolResultContent::Text("abcdef".into()), + output: Some(serde_json::Value::Null), + }], + context: "".to_string(), + creases: vec![], + is_hidden: false, + }, + ], + version: SerializedThread::VERSION.to_string(), + initial_project_snapshot: None, + cumulative_token_usage: TokenUsage::default(), + request_token_usage: vec![], + detailed_summary_state: DetailedSummaryState::default(), + model: None, + completion_mode: None, + tool_use_limit_reached: false, + profile: None + } + ) + } +} diff --git a/crates/agent2/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs similarity index 100% rename from crates/agent2/src/native_agent_server.rs rename to crates/agent/src/native_agent_server.rs diff --git a/crates/assistant_tool/src/outline.rs b/crates/agent/src/outline.rs similarity index 76% rename from crates/assistant_tool/src/outline.rs rename to crates/agent/src/outline.rs index 4c8e2efefd67e25c630d38e16bda8a8dff34fb16..bc78290fb52ae208742b9dea0e6dbbe560022419 100644 --- a/crates/assistant_tool/src/outline.rs +++ b/crates/agent/src/outline.rs @@ -1,8 +1,6 @@ -use action_log::ActionLog; -use anyhow::{Context as _, Result}; +use anyhow::Result; use gpui::{AsyncApp, Entity}; use language::{Buffer, OutlineItem, ParseStatus}; -use project::Project; use regex::Regex; use std::fmt::Write; use text::Point; @@ -11,51 +9,66 @@ use text::Point; /// we automatically provide the file's symbol outline instead, with line numbers. pub const AUTO_OUTLINE_SIZE: usize = 16384; -pub async fn file_outline( - project: Entity, - path: String, - action_log: Entity, - regex: Option, - cx: &mut AsyncApp, -) -> anyhow::Result { - let buffer = { - let project_path = project.read_with(cx, |project, cx| { - project - .find_project_path(&path, cx) - .with_context(|| format!("Path {path} not found in project")) - })??; - - project - .update(cx, |project, cx| project.open_buffer(project_path, cx))? - .await? - }; +/// Result of getting buffer content, which can be either full content or an outline. +pub struct BufferContent { + /// The actual content (either full text or outline) + pub text: String, + /// Whether this is an outline (true) or full content (false) + pub is_outline: bool, +} - action_log.update(cx, |action_log, cx| { - action_log.buffer_read(buffer.clone(), cx); - })?; +/// Returns either the full content of a buffer or its outline, depending on size. +/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header. +/// For smaller files, returns the full content. +pub async fn get_buffer_content_or_outline( + buffer: Entity, + path: Option<&str>, + cx: &AsyncApp, +) -> Result { + let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?; - // Wait until the buffer has been fully parsed, so that we can read its outline. - let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?; - while *parse_status.borrow() != ParseStatus::Idle { - parse_status.changed().await?; - } + if file_size > AUTO_OUTLINE_SIZE { + // For large files, use outline instead of full content + // Wait until the buffer has been fully parsed, so we can read its outline + let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?; + while *parse_status.borrow() != ParseStatus::Idle { + parse_status.changed().await?; + } + + let outline_items = buffer.read_with(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + snapshot + .outline(None) + .items + .into_iter() + .map(|item| item.to_point(&snapshot)) + .collect::>() + })?; - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let outline = snapshot.outline(None); - - render_outline( - outline - .items - .into_iter() - .map(|item| item.to_point(&snapshot)), - regex, - 0, - usize::MAX, - ) - .await + let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?; + + let text = if let Some(path) = path { + format!( + "# File outline for {path} (file too large to show full content)\n\n{outline_text}", + ) + } else { + format!("# File outline (file too large to show full content)\n\n{outline_text}",) + }; + Ok(BufferContent { + text, + is_outline: true, + }) + } else { + // File is small enough, return full content + let text = buffer.read_with(cx, |buffer, _| buffer.text())?; + Ok(BufferContent { + text, + is_outline: false, + }) + } } -pub async fn render_outline( +async fn render_outline( items: impl IntoIterator>, regex: Option, offset: usize, @@ -128,62 +141,3 @@ fn render_entries( entries_rendered } - -/// Result of getting buffer content, which can be either full content or an outline. -pub struct BufferContent { - /// The actual content (either full text or outline) - pub text: String, - /// Whether this is an outline (true) or full content (false) - pub is_outline: bool, -} - -/// Returns either the full content of a buffer or its outline, depending on size. -/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header. -/// For smaller files, returns the full content. -pub async fn get_buffer_content_or_outline( - buffer: Entity, - path: Option<&str>, - cx: &AsyncApp, -) -> Result { - let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?; - - if file_size > AUTO_OUTLINE_SIZE { - // For large files, use outline instead of full content - // Wait until the buffer has been fully parsed, so we can read its outline - let mut parse_status = buffer.read_with(cx, |buffer, _| buffer.parse_status())?; - while *parse_status.borrow() != ParseStatus::Idle { - parse_status.changed().await?; - } - - let outline_items = buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot - .outline(None) - .items - .into_iter() - .map(|item| item.to_point(&snapshot)) - .collect::>() - })?; - - let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?; - - let text = if let Some(path) = path { - format!( - "# File outline for {path} (file too large to show full content)\n\n{outline_text}", - ) - } else { - format!("# File outline (file too large to show full content)\n\n{outline_text}",) - }; - Ok(BufferContent { - text, - is_outline: true, - }) - } else { - // File is small enough, return full content - let text = buffer.read_with(cx, |buffer, _| buffer.text())?; - Ok(BufferContent { - text, - is_outline: false, - }) - } -} diff --git a/crates/agent/src/prompts/stale_files_prompt_header.txt b/crates/agent/src/prompts/stale_files_prompt_header.txt deleted file mode 100644 index f743e239c883c7456f7bdc6e089185c6b994cb44..0000000000000000000000000000000000000000 --- a/crates/agent/src/prompts/stale_files_prompt_header.txt +++ /dev/null @@ -1,3 +0,0 @@ -[The following is an auto-generated notification; do not reply] - -These files have changed since the last read: diff --git a/crates/agent2/src/templates.rs b/crates/agent/src/templates.rs similarity index 100% rename from crates/agent2/src/templates.rs rename to crates/agent/src/templates.rs diff --git a/crates/assistant_tools/src/templates/create_file_prompt.hbs b/crates/agent/src/templates/create_file_prompt.hbs similarity index 100% rename from crates/assistant_tools/src/templates/create_file_prompt.hbs rename to crates/agent/src/templates/create_file_prompt.hbs diff --git a/crates/assistant_tools/src/templates/diff_judge.hbs b/crates/agent/src/templates/diff_judge.hbs similarity index 100% rename from crates/assistant_tools/src/templates/diff_judge.hbs rename to crates/agent/src/templates/diff_judge.hbs diff --git a/crates/assistant_tools/src/templates/edit_file_prompt_diff_fenced.hbs b/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs similarity index 100% rename from crates/assistant_tools/src/templates/edit_file_prompt_diff_fenced.hbs rename to crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs diff --git a/crates/assistant_tools/src/templates/edit_file_prompt_xml.hbs b/crates/agent/src/templates/edit_file_prompt_xml.hbs similarity index 100% rename from crates/assistant_tools/src/templates/edit_file_prompt_xml.hbs rename to crates/agent/src/templates/edit_file_prompt_xml.hbs diff --git a/crates/agent2/src/templates/system_prompt.hbs b/crates/agent/src/templates/system_prompt.hbs similarity index 100% rename from crates/agent2/src/templates/system_prompt.hbs rename to crates/agent/src/templates/system_prompt.hbs diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent/src/tests/mod.rs similarity index 99% rename from crates/agent2/src/tests/mod.rs rename to crates/agent/src/tests/mod.rs index 2e63aa5856501f880fec94f7659b13be321b03b3..6b7d30b37f825bf664ee270bee9f965ee194291c 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -975,9 +975,9 @@ async fn test_mcp_tools(cx: &mut TestAppContext) { vec![context_server::types::Tool { name: "echo".into(), description: None, - input_schema: serde_json::to_value( - EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), - ) + input_schema: serde_json::to_value(EchoTool::input_schema( + LanguageModelToolSchemaFormat::JsonSchema, + )) .unwrap(), output_schema: None, annotations: None, @@ -1149,9 +1149,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) { context_server::types::Tool { name: "echo".into(), // Conflicts with native EchoTool description: None, - input_schema: serde_json::to_value( - EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), - ) + input_schema: serde_json::to_value(EchoTool::input_schema( + LanguageModelToolSchemaFormat::JsonSchema, + )) .unwrap(), output_schema: None, annotations: None, @@ -1174,9 +1174,9 @@ async fn test_mcp_tool_truncation(cx: &mut TestAppContext) { context_server::types::Tool { name: "echo".into(), // Also conflicts with native EchoTool description: None, - input_schema: serde_json::to_value( - EchoTool.input_schema(LanguageModelToolSchemaFormat::JsonSchema), - ) + input_schema: serde_json::to_value(EchoTool::input_schema( + LanguageModelToolSchemaFormat::JsonSchema, + )) .unwrap(), output_schema: None, annotations: None, @@ -1864,7 +1864,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let selector_opt = connection.model_selector(&session_id); assert!( selector_opt.is_some(), - "agent2 should always support ModelSelector" + "agent should always support ModelSelector" ); let selector = selector_opt.unwrap(); diff --git a/crates/agent2/src/tests/test_tools.rs b/crates/agent/src/tests/test_tools.rs similarity index 100% rename from crates/agent2/src/tests/test_tools.rs rename to crates/agent/src/tests/test_tools.rs diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index d189b7611209d2fbea5c882ea548318f73ddbfb3..ec9d50ff2f62c5602dd91e5da47593764ea01c85 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1,95 +1,65 @@ use crate::{ - agent_profile::AgentProfile, - context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext}, - thread_store::{ - SerializedCrease, SerializedLanguageModel, SerializedMessage, SerializedMessageSegment, - SerializedThread, SerializedToolResult, SerializedToolUse, SharedProjectContext, - ThreadStore, - }, - tool_use::{PendingToolUse, ToolUse, ToolUseMetadata, ToolUseState}, + ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, + DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GitState, GrepTool, + ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool, + SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, + WorktreeSnapshot, }; +use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; + +use agent_client_protocol as acp; use agent_settings::{ - AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT, - SUMMARIZE_THREAD_PROMPT, + AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, + SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, }; -use anyhow::{Result, anyhow}; -use assistant_tool::{AnyToolCard, Tool, ToolWorkingSet}; +use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; -use client::{ModelRequestUsage, RequestUsage}; +use client::{ModelRequestUsage, RequestUsage, UserStore}; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; -use collections::HashMap; -use futures::{FutureExt, StreamExt as _, future::Shared}; +use collections::{HashMap, HashSet, IndexMap}; +use fs::Fs; +use futures::stream; +use futures::{ + FutureExt, + channel::{mpsc, oneshot}, + future::Shared, + stream::FuturesUnordered, +}; use git::repository::DiffType; use gpui::{ - AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, - WeakEntity, Window, + App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, }; -use http_client::StatusCode; use language_model::{ - ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, - LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest, + LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, + LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, - LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, - ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason, - TokenUsage, + LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, + LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID, }; -use postage::stream::Stream as _; use project::{ Project, - git_store::{GitStore, GitStoreCheckpoint, RepositoryState}, + git_store::{GitStore, RepositoryState}, }; -use prompt_store::{ModelContext, PromptBuilder}; -use schemars::JsonSchema; +use prompt_store::ProjectContext; +use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; -use settings::Settings; +use settings::{Settings, update_settings_file}; +use smol::stream::StreamExt; use std::{ - io::Write, - ops::Range, + collections::BTreeMap, + ops::RangeInclusive, + path::Path, + rc::Rc, sync::Arc, time::{Duration, Instant}, }; -use thiserror::Error; -use util::{ResultExt as _, post_inc}; +use std::{fmt::Write, path::PathBuf}; +use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; use uuid::Uuid; -const MAX_RETRY_ATTEMPTS: u8 = 4; -const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); - -#[derive(Debug, Clone)] -enum RetryStrategy { - ExponentialBackoff { - initial_delay: Duration, - max_attempts: u8, - }, - Fixed { - delay: Duration, - max_attempts: u8, - }, -} - -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, -)] -pub struct ThreadId(Arc); - -impl ThreadId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} - -impl std::fmt::Display for ThreadId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From<&str> for ThreadId { - fn from(value: &str) -> Self { - Self(value.into()) - } -} +const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; +pub const MAX_TOOL_NAME_LENGTH: usize = 64; /// The ID of the user prompt that initiated a request. /// @@ -109,2014 +79,1958 @@ impl std::fmt::Display for PromptId { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(pub usize); - -impl MessageId { - fn post_inc(&mut self) -> Self { - Self(post_inc(&mut self.0)) - } - - pub fn as_usize(&self) -> usize { - self.0 - } -} +pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4; +pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); -/// Stored information that can be used to resurrect a context crease when creating an editor for a past message. -#[derive(Clone, Debug)] -pub struct MessageCrease { - pub range: Range, - pub icon_path: SharedString, - pub label: SharedString, - /// None for a deserialized message, Some otherwise. - pub context: Option, +#[derive(Debug, Clone)] +enum RetryStrategy { + ExponentialBackoff { + initial_delay: Duration, + max_attempts: u8, + }, + Fixed { + delay: Duration, + max_attempts: u8, + }, } -/// A message in a [`Thread`]. -#[derive(Debug, Clone)] -pub struct Message { - pub id: MessageId, - pub role: Role, - pub segments: Vec, - pub loaded_context: LoadedContext, - pub creases: Vec, - pub is_hidden: bool, - pub ui_only: bool, +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum Message { + User(UserMessage), + Agent(AgentMessage), + Resume, } impl Message { - /// Returns whether the message contains any meaningful text that should be displayed - /// The model sometimes runs tool without producing any text or just a marker ([`USING_TOOL_MARKER`]) - pub fn should_display_content(&self) -> bool { - self.segments.iter().all(|segment| segment.should_display()) + pub fn as_agent_message(&self) -> Option<&AgentMessage> { + match self { + Message::Agent(agent_message) => Some(agent_message), + _ => None, + } } - pub fn push_thinking(&mut self, text: &str, signature: Option) { - if let Some(MessageSegment::Thinking { - text: segment, - signature: current_signature, - }) = self.segments.last_mut() - { - if let Some(signature) = signature { - *current_signature = Some(signature); - } - segment.push_str(text); - } else { - self.segments.push(MessageSegment::Thinking { - text: text.to_string(), - signature, - }); + pub fn to_request(&self) -> Vec { + match self { + Message::User(message) => vec![message.to_request()], + Message::Agent(message) => message.to_request(), + Message::Resume => vec![LanguageModelRequestMessage { + role: Role::User, + content: vec!["Continue where you left off".into()], + cache: false, + }], } } - pub fn push_redacted_thinking(&mut self, data: String) { - self.segments.push(MessageSegment::RedactedThinking(data)); + pub fn to_markdown(&self) -> String { + match self { + Message::User(message) => message.to_markdown(), + Message::Agent(message) => message.to_markdown(), + Message::Resume => "[resume]\n".into(), + } } - pub fn push_text(&mut self, text: &str) { - if let Some(MessageSegment::Text(segment)) = self.segments.last_mut() { - segment.push_str(text); - } else { - self.segments.push(MessageSegment::Text(text.to_string())); + pub fn role(&self) -> Role { + match self { + Message::User(_) | Message::Resume => Role::User, + Message::Agent(_) => Role::Assistant, } } +} - pub fn to_message_content(&self) -> String { - let mut result = String::new(); +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserMessage { + pub id: UserMessageId, + pub content: Vec, +} - if !self.loaded_context.text.is_empty() { - result.push_str(&self.loaded_context.text); - } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum UserMessageContent { + Text(String), + Mention { uri: MentionUri, content: String }, + Image(LanguageModelImage), +} + +impl UserMessage { + pub fn to_markdown(&self) -> String { + let mut markdown = String::from("## User\n\n"); - for segment in &self.segments { - match segment { - MessageSegment::Text(text) => result.push_str(text), - MessageSegment::Thinking { text, .. } => { - result.push_str("\n"); - result.push_str(text); - result.push_str("\n"); + for content in &self.content { + match content { + UserMessageContent::Text(text) => { + markdown.push_str(text); + markdown.push('\n'); + } + UserMessageContent::Image(_) => { + markdown.push_str("\n"); + } + UserMessageContent::Mention { uri, content } => { + if !content.is_empty() { + let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); + } else { + let _ = writeln!(&mut markdown, "{}", uri.as_link()); + } } - MessageSegment::RedactedThinking(_) => {} } } - result + markdown } -} -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MessageSegment { - Text(String), - Thinking { - text: String, - signature: Option, - }, - RedactedThinking(String), -} + fn to_request(&self) -> LanguageModelRequestMessage { + let mut message = LanguageModelRequestMessage { + role: Role::User, + content: Vec::with_capacity(self.content.len()), + cache: false, + }; -impl MessageSegment { - pub fn should_display(&self) -> bool { - match self { - Self::Text(text) => text.is_empty(), - Self::Thinking { text, .. } => text.is_empty(), - Self::RedactedThinking(_) => false, + const OPEN_CONTEXT: &str = "\n\ + The following items were attached by the user. \ + They are up-to-date and don't need to be re-read.\n\n"; + + const OPEN_FILES_TAG: &str = ""; + const OPEN_DIRECTORIES_TAG: &str = ""; + const OPEN_SYMBOLS_TAG: &str = ""; + const OPEN_SELECTIONS_TAG: &str = ""; + const OPEN_THREADS_TAG: &str = ""; + const OPEN_FETCH_TAG: &str = ""; + const OPEN_RULES_TAG: &str = + "\nThe user has specified the following rules that should be applied:\n"; + + let mut file_context = OPEN_FILES_TAG.to_string(); + let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); + let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); + let mut selection_context = OPEN_SELECTIONS_TAG.to_string(); + let mut thread_context = OPEN_THREADS_TAG.to_string(); + let mut fetch_context = OPEN_FETCH_TAG.to_string(); + let mut rules_context = OPEN_RULES_TAG.to_string(); + + for chunk in &self.content { + let chunk = match chunk { + UserMessageContent::Text(text) => { + language_model::MessageContent::Text(text.clone()) + } + UserMessageContent::Image(value) => { + language_model::MessageContent::Image(value.clone()) + } + UserMessageContent::Mention { uri, content } => { + match uri { + MentionUri::File { abs_path } => { + write!( + &mut file_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag(abs_path, None), + text: &content.to_string(), + } + ) + .ok(); + } + MentionUri::PastedImage => { + debug_panic!("pasted image URI should not be used in mention content") + } + MentionUri::Directory { .. } => { + write!(&mut directory_context, "\n{}\n", content).ok(); + } + MentionUri::Symbol { + abs_path: path, + line_range, + .. + } => { + write!( + &mut symbol_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag(path, Some(line_range)), + text: content + } + ) + .ok(); + } + MentionUri::Selection { + abs_path: path, + line_range, + .. + } => { + write!( + &mut selection_context, + "\n{}", + MarkdownCodeBlock { + tag: &codeblock_tag( + path.as_deref().unwrap_or("Untitled".as_ref()), + Some(line_range) + ), + text: content + } + ) + .ok(); + } + MentionUri::Thread { .. } => { + write!(&mut thread_context, "\n{}\n", content).ok(); + } + MentionUri::TextThread { .. } => { + write!(&mut thread_context, "\n{}\n", content).ok(); + } + MentionUri::Rule { .. } => { + write!( + &mut rules_context, + "\n{}", + MarkdownCodeBlock { + tag: "", + text: content + } + ) + .ok(); + } + MentionUri::Fetch { url } => { + write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok(); + } + } + + language_model::MessageContent::Text(uri.as_link().to_string()) + } + }; + + message.content.push(chunk); } - } - pub fn text(&self) -> Option<&str> { - match self { - MessageSegment::Text(text) => Some(text), - _ => None, + let len_before_context = message.content.len(); + + if file_context.len() > OPEN_FILES_TAG.len() { + file_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(file_context)); } - } -} -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ProjectSnapshot { - pub worktree_snapshots: Vec, - pub timestamp: DateTime, -} + if directory_context.len() > OPEN_DIRECTORIES_TAG.len() { + directory_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(directory_context)); + } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct WorktreeSnapshot { - pub worktree_path: String, - pub git_state: Option, -} + if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { + symbol_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(symbol_context)); + } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct GitState { - pub remote_url: Option, - pub head_sha: Option, - pub current_branch: Option, - pub diff: Option, -} + if selection_context.len() > OPEN_SELECTIONS_TAG.len() { + selection_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(selection_context)); + } -#[derive(Clone, Debug)] -pub struct ThreadCheckpoint { - message_id: MessageId, - git_checkpoint: GitStoreCheckpoint, -} + if thread_context.len() > OPEN_THREADS_TAG.len() { + thread_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(thread_context)); + } -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ThreadFeedback { - Positive, - Negative, -} + if fetch_context.len() > OPEN_FETCH_TAG.len() { + fetch_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(fetch_context)); + } -pub enum LastRestoreCheckpoint { - Pending { - message_id: MessageId, - }, - Error { - message_id: MessageId, - error: String, - }, -} + if rules_context.len() > OPEN_RULES_TAG.len() { + rules_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(rules_context)); + } -impl LastRestoreCheckpoint { - pub fn message_id(&self) -> MessageId { - match self { - LastRestoreCheckpoint::Pending { message_id } => *message_id, - LastRestoreCheckpoint::Error { message_id, .. } => *message_id, + if message.content.len() > len_before_context { + message.content.insert( + len_before_context, + language_model::MessageContent::Text(OPEN_CONTEXT.into()), + ); + message + .content + .push(language_model::MessageContent::Text("".into())); } + + message } } -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub enum DetailedSummaryState { - #[default] - NotGenerated, - Generating { - message_id: MessageId, - }, - Generated { - text: SharedString, - message_id: MessageId, - }, -} +fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { + let mut result = String::new(); -impl DetailedSummaryState { - fn text(&self) -> Option { - if let Self::Generated { text, .. } = self { - Some(text.clone()) + if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { + let _ = write!(result, "{} ", extension); + } + + let _ = write!(result, "{}", full_path.display()); + + if let Some(range) = line_range { + if range.start() == range.end() { + let _ = write!(result, ":{}", range.start() + 1); } else { - None + let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); } } -} -#[derive(Default, Debug)] -pub struct TotalTokenUsage { - pub total: u64, - pub max: u64, + result } -impl TotalTokenUsage { - pub fn ratio(&self) -> TokenUsageRatio { - #[cfg(debug_assertions)] - let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD") - .unwrap_or("0.8".to_string()) - .parse() - .unwrap(); - #[cfg(not(debug_assertions))] - let warning_threshold: f32 = 0.8; - - // When the maximum is unknown because there is no selected model, - // avoid showing the token limit warning. - if self.max == 0 { - TokenUsageRatio::Normal - } else if self.total >= self.max { - TokenUsageRatio::Exceeded - } else if self.total as f32 / self.max as f32 >= warning_threshold { - TokenUsageRatio::Warning - } else { - TokenUsageRatio::Normal +impl AgentMessage { + pub fn to_markdown(&self) -> String { + let mut markdown = String::from("## Assistant\n\n"); + + for content in &self.content { + match content { + AgentMessageContent::Text(text) => { + markdown.push_str(text); + markdown.push('\n'); + } + AgentMessageContent::Thinking { text, .. } => { + markdown.push_str(""); + markdown.push_str(text); + markdown.push_str("\n"); + } + AgentMessageContent::RedactedThinking(_) => { + markdown.push_str("\n") + } + AgentMessageContent::ToolUse(tool_use) => { + markdown.push_str(&format!( + "**Tool Use**: {} (ID: {})\n", + tool_use.name, tool_use.id + )); + markdown.push_str(&format!( + "{}\n", + MarkdownCodeBlock { + tag: "json", + text: &format!("{:#}", tool_use.input) + } + )); + } + } + } + + for tool_result in self.tool_results.values() { + markdown.push_str(&format!( + "**Tool Result**: {} (ID: {})\n\n", + tool_result.tool_name, tool_result.tool_use_id + )); + if tool_result.is_error { + markdown.push_str("**ERROR:**\n"); + } + + match &tool_result.content { + LanguageModelToolResultContent::Text(text) => { + writeln!(markdown, "{text}\n").ok(); + } + LanguageModelToolResultContent::Image(_) => { + writeln!(markdown, "\n").ok(); + } + } + + if let Some(output) = tool_result.output.as_ref() { + writeln!( + markdown, + "**Debug Output**:\n\n```json\n{}\n```\n", + serde_json::to_string_pretty(output).unwrap() + ) + .unwrap(); + } } + + markdown } - pub fn add(&self, tokens: u64) -> TotalTokenUsage { - TotalTokenUsage { - total: self.total + tokens, - max: self.max, + pub fn to_request(&self) -> Vec { + let mut assistant_message = LanguageModelRequestMessage { + role: Role::Assistant, + content: Vec::with_capacity(self.content.len()), + cache: false, + }; + for chunk in &self.content { + match chunk { + AgentMessageContent::Text(text) => { + assistant_message + .content + .push(language_model::MessageContent::Text(text.clone())); + } + AgentMessageContent::Thinking { text, signature } => { + assistant_message + .content + .push(language_model::MessageContent::Thinking { + text: text.clone(), + signature: signature.clone(), + }); + } + AgentMessageContent::RedactedThinking(value) => { + assistant_message.content.push( + language_model::MessageContent::RedactedThinking(value.clone()), + ); + } + AgentMessageContent::ToolUse(tool_use) => { + if self.tool_results.contains_key(&tool_use.id) { + assistant_message + .content + .push(language_model::MessageContent::ToolUse(tool_use.clone())); + } + } + }; + } + + let mut user_message = LanguageModelRequestMessage { + role: Role::User, + content: Vec::new(), + cache: false, + }; + + for tool_result in self.tool_results.values() { + let mut tool_result = tool_result.clone(); + // Surprisingly, the API fails if we return an empty string here. + // It thinks we are sending a tool use without a tool result. + if tool_result.content.is_empty() { + tool_result.content = "".into(); + } + user_message + .content + .push(language_model::MessageContent::ToolResult(tool_result)); + } + + let mut messages = Vec::new(); + if !assistant_message.content.is_empty() { + messages.push(assistant_message); + } + if !user_message.content.is_empty() { + messages.push(user_message); } + messages } } -#[derive(Debug, Default, PartialEq, Eq)] -pub enum TokenUsageRatio { - #[default] - Normal, - Warning, - Exceeded, +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AgentMessage { + pub content: Vec, + pub tool_results: IndexMap, } -#[derive(Debug, Clone, Copy)] -pub enum QueueState { - Sending, - Queued { position: usize }, - Started, +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum AgentMessageContent { + Text(String), + Thinking { + text: String, + signature: Option, + }, + RedactedThinking(String), + ToolUse(LanguageModelToolUse), } -/// A thread of conversation with the LLM. -pub struct Thread { - id: ThreadId, - updated_at: DateTime, - summary: ThreadSummary, - pending_summary: Task>, - detailed_summary_task: Task>, - detailed_summary_tx: postage::watch::Sender, - detailed_summary_rx: postage::watch::Receiver, - completion_mode: agent_settings::CompletionMode, - messages: Vec, - next_message_id: MessageId, - last_prompt_id: PromptId, - project_context: SharedProjectContext, - checkpoints_by_message: HashMap, - completion_count: usize, - pending_completions: Vec, - project: Entity, - prompt_builder: Arc, - tools: Entity, - tool_use: ToolUseState, - action_log: Entity, - last_restore_checkpoint: Option, - pending_checkpoint: Option, - initial_project_snapshot: Shared>>>, - request_token_usage: Vec, - cumulative_token_usage: TokenUsage, - exceeded_window_error: Option, - tool_use_limit_reached: bool, - retry_state: Option, - message_feedback: HashMap, - last_received_chunk_at: Option, - request_callback: Option< - Box])>, - >, - remaining_turns: u32, - configured_model: Option, - profile: AgentProfile, - last_error_context: Option<(Arc, CompletionIntent)>, +pub trait TerminalHandle { + fn id(&self, cx: &AsyncApp) -> Result; + fn current_output(&self, cx: &AsyncApp) -> Result; + fn wait_for_exit(&self, cx: &AsyncApp) -> Result>>; } -#[derive(Clone, Debug)] -struct RetryState { - attempt: u8, - max_attempts: u8, - intent: CompletionIntent, +pub trait ThreadEnvironment { + fn create_terminal( + &self, + command: String, + cwd: Option, + output_byte_limit: Option, + cx: &mut AsyncApp, + ) -> Task>>; } -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ThreadSummary { - Pending, - Generating, - Ready(SharedString), - Error, +#[derive(Debug)] +pub enum ThreadEvent { + UserMessage(UserMessage), + AgentText(String), + AgentThinking(String), + ToolCall(acp::ToolCall), + ToolCallUpdate(acp_thread::ToolCallUpdate), + ToolCallAuthorization(ToolCallAuthorization), + Retry(acp_thread::RetryStatus), + Stop(acp::StopReason), } -impl ThreadSummary { - pub const DEFAULT: SharedString = SharedString::new_static("New Thread"); - - pub fn or_default(&self) -> SharedString { - self.unwrap_or(Self::DEFAULT) - } +#[derive(Debug)] +pub struct NewTerminal { + pub command: String, + pub output_byte_limit: Option, + pub cwd: Option, + pub response: oneshot::Sender>>, +} - pub fn unwrap_or(&self, message: impl Into) -> SharedString { - self.ready().unwrap_or_else(|| message.into()) - } +#[derive(Debug)] +pub struct ToolCallAuthorization { + pub tool_call: acp::ToolCallUpdate, + pub options: Vec, + pub response: oneshot::Sender, +} - pub fn ready(&self) -> Option { - match self { - ThreadSummary::Ready(summary) => Some(summary.clone()), - ThreadSummary::Pending | ThreadSummary::Generating | ThreadSummary::Error => None, - } - } +#[derive(Debug, thiserror::Error)] +enum CompletionError { + #[error("max tokens")] + MaxTokens, + #[error("refusal")] + Refusal, + #[error(transparent)] + Other(#[from] anyhow::Error), } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ExceededWindowError { - /// Model used when last message exceeded context window - model_id: LanguageModelId, - /// Token count including last message - token_count: u64, +pub struct Thread { + id: acp::SessionId, + prompt_id: PromptId, + updated_at: DateTime, + title: Option, + pending_title_generation: Option>, + pending_summary_generation: Option>>>, + summary: Option, + messages: Vec, + user_store: Entity, + completion_mode: CompletionMode, + /// Holds the task that handles agent interaction until the end of the turn. + /// Survives across multiple requests as the model performs tool calls and + /// we run tools, report their results. + running_turn: Option, + pending_message: Option, + tools: BTreeMap>, + tool_use_limit_reached: bool, + request_token_usage: HashMap, + #[allow(unused)] + cumulative_token_usage: TokenUsage, + #[allow(unused)] + initial_project_snapshot: Shared>>>, + context_server_registry: Entity, + profile_id: AgentProfileId, + project_context: Entity, + templates: Arc, + model: Option>, + summarization_model: Option>, + prompt_capabilities_tx: watch::Sender, + pub(crate) prompt_capabilities_rx: watch::Receiver, + pub(crate) project: Entity, + pub(crate) action_log: Entity, } impl Thread { + fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { + let image = model.map_or(true, |model| model.supports_images()); + acp::PromptCapabilities { + meta: None, + image, + audio: false, + embedded_context: true, + } + } + pub fn new( project: Entity, - tools: Entity, - prompt_builder: Arc, - system_prompt: SharedProjectContext, + project_context: Entity, + context_server_registry: Entity, + templates: Arc, + model: Option>, cx: &mut Context, ) -> Self { - let (detailed_summary_tx, detailed_summary_rx) = postage::watch::channel(); - let configured_model = LanguageModelRegistry::read_global(cx).default_model(); let profile_id = AgentSettings::get_global(cx).default_profile.clone(); - + let action_log = cx.new(|_cx| ActionLog::new(project.clone())); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); Self { - id: ThreadId::new(), + id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), + prompt_id: PromptId::new(), updated_at: Utc::now(), - summary: ThreadSummary::Pending, - pending_summary: Task::ready(None), - detailed_summary_task: Task::ready(None), - detailed_summary_tx, - detailed_summary_rx, - completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, + title: None, + pending_title_generation: None, + pending_summary_generation: None, + summary: None, messages: Vec::new(), - next_message_id: MessageId(0), - last_prompt_id: PromptId::new(), - project_context: system_prompt, - checkpoints_by_message: HashMap::default(), - completion_count: 0, - pending_completions: Vec::new(), - project: project.clone(), - prompt_builder, - tools: tools.clone(), - last_restore_checkpoint: None, - pending_checkpoint: None, - tool_use: ToolUseState::new(tools.clone()), - action_log: cx.new(|_| ActionLog::new(project.clone())), + user_store: project.read(cx).user_store(), + completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, + running_turn: None, + pending_message: None, + tools: BTreeMap::default(), + tool_use_limit_reached: false, + request_token_usage: HashMap::default(), + cumulative_token_usage: TokenUsage::default(), initial_project_snapshot: { - let project_snapshot = Self::project_snapshot(project, cx); + let project_snapshot = Self::project_snapshot(project.clone(), cx); cx.foreground_executor() .spawn(async move { Some(project_snapshot.await) }) .shared() }, - request_token_usage: Vec::new(), - cumulative_token_usage: TokenUsage::default(), - exceeded_window_error: None, - tool_use_limit_reached: false, - retry_state: None, - message_feedback: HashMap::default(), - last_error_context: None, - last_received_chunk_at: None, - request_callback: None, - remaining_turns: u32::MAX, - configured_model, - profile: AgentProfile::new(profile_id, tools), + context_server_registry, + profile_id, + project_context, + templates, + model, + summarization_model: None, + prompt_capabilities_tx, + prompt_capabilities_rx, + project, + action_log, } } - pub fn deserialize( - id: ThreadId, - serialized: SerializedThread, - project: Entity, - tools: Entity, - prompt_builder: Arc, - project_context: SharedProjectContext, - window: Option<&mut Window>, // None in headless mode - cx: &mut Context, - ) -> Self { - let next_message_id = MessageId( - serialized - .messages - .last() - .map(|message| message.id.0 + 1) - .unwrap_or(0), - ); - let tool_use = ToolUseState::from_serialized_messages( - tools.clone(), - &serialized.messages, - project.clone(), - window, - cx, - ); - let (detailed_summary_tx, detailed_summary_rx) = - postage::watch::channel_with(serialized.detailed_summary_state); - - let configured_model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - serialized - .model - .and_then(|model| { - let model = SelectedModel { - provider: model.provider.clone().into(), - model: model.model.into(), - }; - registry.select_model(&model, cx) - }) - .or_else(|| registry.default_model()) - }); - - let completion_mode = serialized - .completion_mode - .unwrap_or_else(|| AgentSettings::get_global(cx).preferred_completion_mode); - let profile_id = serialized - .profile - .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); - - Self { - id, - updated_at: serialized.updated_at, - summary: ThreadSummary::Ready(serialized.summary), - pending_summary: Task::ready(None), - detailed_summary_task: Task::ready(None), - detailed_summary_tx, - detailed_summary_rx, - completion_mode, - retry_state: None, - messages: serialized - .messages - .into_iter() - .map(|message| Message { - id: message.id, - role: message.role, - segments: message - .segments - .into_iter() - .map(|segment| match segment { - SerializedMessageSegment::Text { text } => MessageSegment::Text(text), - SerializedMessageSegment::Thinking { text, signature } => { - MessageSegment::Thinking { text, signature } - } - SerializedMessageSegment::RedactedThinking { data } => { - MessageSegment::RedactedThinking(data) - } - }) - .collect(), - loaded_context: LoadedContext { - contexts: Vec::new(), - text: message.context, - images: Vec::new(), - }, - creases: message - .creases - .into_iter() - .map(|crease| MessageCrease { - range: crease.start..crease.end, - icon_path: crease.icon_path, - label: crease.label, - context: None, - }) - .collect(), - is_hidden: message.is_hidden, - ui_only: false, // UI-only messages are not persisted - }) - .collect(), - next_message_id, - last_prompt_id: PromptId::new(), - project_context, - checkpoints_by_message: HashMap::default(), - completion_count: 0, - pending_completions: Vec::new(), - last_restore_checkpoint: None, - pending_checkpoint: None, - project: project.clone(), - prompt_builder, - tools: tools.clone(), - tool_use, - action_log: cx.new(|_| ActionLog::new(project)), - initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(), - request_token_usage: serialized.request_token_usage, - cumulative_token_usage: serialized.cumulative_token_usage, - exceeded_window_error: None, - tool_use_limit_reached: serialized.tool_use_limit_reached, - message_feedback: HashMap::default(), - last_error_context: None, - last_received_chunk_at: None, - request_callback: None, - remaining_turns: u32::MAX, - configured_model, - profile: AgentProfile::new(profile_id, tools), - } + pub fn id(&self) -> &acp::SessionId { + &self.id } - pub fn set_request_callback( + pub fn replay( &mut self, - callback: impl 'static - + FnMut(&LanguageModelRequest, &[Result]), - ) { - self.request_callback = Some(Box::new(callback)); - } - - pub fn id(&self) -> &ThreadId { - &self.id - } - - pub fn profile(&self) -> &AgentProfile { - &self.profile - } - - pub fn set_profile(&mut self, id: AgentProfileId, cx: &mut Context) { - if &id != self.profile.id() { - self.profile = AgentProfile::new(id, self.tools.clone()); - cx.emit(ThreadEvent::ProfileChanged); + cx: &mut Context, + ) -> mpsc::UnboundedReceiver> { + let (tx, rx) = mpsc::unbounded(); + let stream = ThreadEventStream(tx); + for message in &self.messages { + match message { + Message::User(user_message) => stream.send_user_message(user_message), + Message::Agent(assistant_message) => { + for content in &assistant_message.content { + match content { + AgentMessageContent::Text(text) => stream.send_text(text), + AgentMessageContent::Thinking { text, .. } => { + stream.send_thinking(text) + } + AgentMessageContent::RedactedThinking(_) => {} + AgentMessageContent::ToolUse(tool_use) => { + self.replay_tool_call( + tool_use, + assistant_message.tool_results.get(&tool_use.id), + &stream, + cx, + ); + } + } + } + } + Message::Resume => {} + } } + rx } - pub fn is_empty(&self) -> bool { - self.messages.is_empty() - } + fn replay_tool_call( + &self, + tool_use: &LanguageModelToolUse, + tool_result: Option<&LanguageModelToolResult>, + stream: &ThreadEventStream, + cx: &mut Context, + ) { + let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| { + self.context_server_registry + .read(cx) + .servers() + .find_map(|(_, tools)| { + if let Some(tool) = tools.get(tool_use.name.as_ref()) { + Some(tool.clone()) + } else { + None + } + }) + }); - pub fn updated_at(&self) -> DateTime { - self.updated_at - } + let Some(tool) = tool else { + stream + .0 + .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { + meta: None, + id: acp::ToolCallId(tool_use.id.to_string().into()), + title: tool_use.name.to_string(), + kind: acp::ToolKind::Other, + status: acp::ToolCallStatus::Failed, + content: Vec::new(), + locations: Vec::new(), + raw_input: Some(tool_use.input.clone()), + raw_output: None, + }))) + .ok(); + return; + }; - pub fn touch_updated_at(&mut self) { - self.updated_at = Utc::now(); + let title = tool.initial_title(tool_use.input.clone(), cx); + let kind = tool.kind(); + stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); + + let output = tool_result + .as_ref() + .and_then(|result| result.output.clone()); + if let Some(output) = output.clone() { + let tool_event_stream = ToolCallEventStream::new( + tool_use.id.clone(), + stream.clone(), + Some(self.project.read(cx).fs().clone()), + ); + tool.replay(tool_use.input.clone(), output, tool_event_stream, cx) + .log_err(); + } + + stream.update_tool_call_fields( + &tool_use.id, + acp::ToolCallUpdateFields { + status: Some( + tool_result + .as_ref() + .map_or(acp::ToolCallStatus::Failed, |result| { + if result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + } + }), + ), + raw_output: output, + ..Default::default() + }, + ); } - pub fn advance_prompt_id(&mut self) { - self.last_prompt_id = PromptId::new(); - } + pub fn from_db( + id: acp::SessionId, + db_thread: DbThread, + project: Entity, + project_context: Entity, + context_server_registry: Entity, + templates: Arc, + cx: &mut Context, + ) -> Self { + let profile_id = db_thread + .profile + .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); + let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + db_thread + .model + .and_then(|model| { + let model = SelectedModel { + provider: model.provider.clone().into(), + model: model.model.into(), + }; + registry.select_model(&model, cx) + }) + .or_else(|| registry.default_model()) + .map(|model| model.model) + }); + let (prompt_capabilities_tx, prompt_capabilities_rx) = + watch::channel(Self::prompt_capabilities(model.as_deref())); - pub fn project_context(&self) -> SharedProjectContext { - self.project_context.clone() - } + let action_log = cx.new(|_| ActionLog::new(project.clone())); - pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() { - self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); + Self { + id, + prompt_id: PromptId::new(), + title: if db_thread.title.is_empty() { + None + } else { + Some(db_thread.title.clone()) + }, + pending_title_generation: None, + pending_summary_generation: None, + summary: db_thread.detailed_summary, + messages: db_thread.messages, + user_store: project.read(cx).user_store(), + completion_mode: db_thread.completion_mode.unwrap_or_default(), + running_turn: None, + pending_message: None, + tools: BTreeMap::default(), + tool_use_limit_reached: false, + request_token_usage: db_thread.request_token_usage.clone(), + cumulative_token_usage: db_thread.cumulative_token_usage, + initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(), + context_server_registry, + profile_id, + project_context, + templates, + model, + summarization_model: None, + project, + action_log, + updated_at: db_thread.updated_at, + prompt_capabilities_tx, + prompt_capabilities_rx, } - self.configured_model.clone() - } - - pub fn configured_model(&self) -> Option { - self.configured_model.clone() - } - - pub fn set_configured_model(&mut self, model: Option, cx: &mut Context) { - self.configured_model = model; - cx.notify(); } - pub fn summary(&self) -> &ThreadSummary { - &self.summary - } - - pub fn set_summary(&mut self, new_summary: impl Into, cx: &mut Context) { - let current_summary = match &self.summary { - ThreadSummary::Pending | ThreadSummary::Generating => return, - ThreadSummary::Ready(summary) => summary, - ThreadSummary::Error => &ThreadSummary::DEFAULT, + pub fn to_db(&self, cx: &App) -> Task { + let initial_project_snapshot = self.initial_project_snapshot.clone(); + let mut thread = DbThread { + title: self.title(), + messages: self.messages.clone(), + updated_at: self.updated_at, + detailed_summary: self.summary.clone(), + initial_project_snapshot: None, + cumulative_token_usage: self.cumulative_token_usage, + request_token_usage: self.request_token_usage.clone(), + model: self.model.as_ref().map(|model| DbLanguageModel { + provider: model.provider_id().to_string(), + model: model.name().0.to_string(), + }), + completion_mode: Some(self.completion_mode), + profile: Some(self.profile_id.clone()), }; - let mut new_summary = new_summary.into(); + cx.background_spawn(async move { + let initial_project_snapshot = initial_project_snapshot.await; + thread.initial_project_snapshot = initial_project_snapshot; + thread + }) + } - if new_summary.is_empty() { - new_summary = ThreadSummary::DEFAULT; - } + /// Create a snapshot of the current project state including git information and unsaved buffers. + fn project_snapshot( + project: Entity, + cx: &mut Context, + ) -> Task> { + let git_store = project.read(cx).git_store().clone(); + let worktree_snapshots: Vec<_> = project + .read(cx) + .visible_worktrees(cx) + .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx)) + .collect(); - if current_summary != &new_summary { - self.summary = ThreadSummary::Ready(new_summary); - cx.emit(ThreadEvent::SummaryChanged); - } - } + cx.spawn(async move |_, _| { + let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; - pub fn completion_mode(&self) -> CompletionMode { - self.completion_mode + Arc::new(ProjectSnapshot { + worktree_snapshots, + timestamp: Utc::now(), + }) + }) } - pub fn set_completion_mode(&mut self, mode: CompletionMode) { - self.completion_mode = mode; - } + fn worktree_snapshot( + worktree: Entity, + git_store: Entity, + cx: &App, + ) -> Task { + cx.spawn(async move |cx| { + // Get worktree path and snapshot + let worktree_info = cx.update(|app_cx| { + let worktree = worktree.read(app_cx); + let path = worktree.abs_path().to_string_lossy().into_owned(); + let snapshot = worktree.snapshot(); + (path, snapshot) + }); - pub fn message(&self, id: MessageId) -> Option<&Message> { - let index = self - .messages - .binary_search_by(|message| message.id.cmp(&id)) - .ok()?; + let Ok((worktree_path, _snapshot)) = worktree_info else { + return WorktreeSnapshot { + worktree_path: String::new(), + git_state: None, + }; + }; - self.messages.get(index) - } + let git_state = git_store + .update(cx, |git_store, cx| { + git_store + .repositories() + .values() + .find(|repo| { + repo.read(cx) + .abs_path_to_repo_path(&worktree.read(cx).abs_path()) + .is_some() + }) + .cloned() + }) + .ok() + .flatten() + .map(|repo| { + repo.update(cx, |repo, _| { + let current_branch = + repo.branch.as_ref().map(|branch| branch.name().to_owned()); + repo.send_job(None, |state, _| async move { + let RepositoryState::Local { backend, .. } = state else { + return GitState { + remote_url: None, + head_sha: None, + current_branch, + diff: None, + }; + }; - pub fn messages(&self) -> impl ExactSizeIterator { - self.messages.iter() - } + let remote_url = backend.remote_url("origin"); + let head_sha = backend.head_sha().await; + let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); - pub fn is_generating(&self) -> bool { - !self.pending_completions.is_empty() || !self.all_tools_finished() - } + GitState { + remote_url, + head_sha, + current_branch, + diff, + } + }) + }) + }); - /// Indicates whether streaming of language model events is stale. - /// When `is_generating()` is false, this method returns `None`. - pub fn is_generation_stale(&self) -> Option { - const STALE_THRESHOLD: u128 = 250; + let git_state = match git_state { + Some(git_state) => match git_state.ok() { + Some(git_state) => git_state.await.ok(), + None => None, + }, + None => None, + }; - self.last_received_chunk_at - .map(|instant| instant.elapsed().as_millis() > STALE_THRESHOLD) + WorktreeSnapshot { + worktree_path, + git_state, + } + }) } - fn received_chunk(&mut self) { - self.last_received_chunk_at = Some(Instant::now()); + pub fn project_context(&self) -> &Entity { + &self.project_context } - pub fn queue_state(&self) -> Option { - self.pending_completions - .first() - .map(|pending_completion| pending_completion.queue_state) + pub fn project(&self) -> &Entity { + &self.project } - pub fn tools(&self) -> &Entity { - &self.tools + pub fn action_log(&self) -> &Entity { + &self.action_log } - pub fn pending_tool(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> { - self.tool_use - .pending_tool_uses() - .into_iter() - .find(|tool_use| &tool_use.id == id) + pub fn is_empty(&self) -> bool { + self.messages.is_empty() && self.title.is_none() } - pub fn tools_needing_confirmation(&self) -> impl Iterator { - self.tool_use - .pending_tool_uses() - .into_iter() - .filter(|tool_use| tool_use.status.needs_confirmation()) + pub fn model(&self) -> Option<&Arc> { + self.model.as_ref() } - pub fn has_pending_tool_uses(&self) -> bool { - !self.tool_use.pending_tool_uses().is_empty() + pub fn set_model(&mut self, model: Arc, cx: &mut Context) { + let old_usage = self.latest_token_usage(); + self.model = Some(model); + let new_caps = Self::prompt_capabilities(self.model.as_deref()); + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } + self.prompt_capabilities_tx.send(new_caps).log_err(); + cx.notify() } - pub fn checkpoint_for_message(&self, id: MessageId) -> Option { - self.checkpoints_by_message.get(&id).cloned() + pub fn summarization_model(&self) -> Option<&Arc> { + self.summarization_model.as_ref() } - pub fn restore_checkpoint( + pub fn set_summarization_model( &mut self, - checkpoint: ThreadCheckpoint, + model: Option>, cx: &mut Context, - ) -> Task> { - self.last_restore_checkpoint = Some(LastRestoreCheckpoint::Pending { - message_id: checkpoint.message_id, - }); - cx.emit(ThreadEvent::CheckpointChanged); - cx.notify(); + ) { + self.summarization_model = model; + cx.notify() + } - let git_store = self.project().read(cx).git_store().clone(); - let restore = git_store.update(cx, |git_store, cx| { - git_store.restore_checkpoint(checkpoint.git_checkpoint.clone(), cx) - }); + pub fn completion_mode(&self) -> CompletionMode { + self.completion_mode + } - cx.spawn(async move |this, cx| { - let result = restore.await; - this.update(cx, |this, cx| { - if let Err(err) = result.as_ref() { - this.last_restore_checkpoint = Some(LastRestoreCheckpoint::Error { - message_id: checkpoint.message_id, - error: err.to_string(), - }); - } else { - this.truncate(checkpoint.message_id, cx); - this.last_restore_checkpoint = None; - } - this.pending_checkpoint = None; - cx.emit(ThreadEvent::CheckpointChanged); - cx.notify(); - })?; - result - }) + pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context) { + let old_usage = self.latest_token_usage(); + self.completion_mode = mode; + let new_usage = self.latest_token_usage(); + if old_usage != new_usage { + cx.emit(TokenUsageUpdated(new_usage)); + } + cx.notify() } - fn finalize_pending_checkpoint(&mut self, cx: &mut Context) { - let pending_checkpoint = if self.is_generating() { - return; - } else if let Some(checkpoint) = self.pending_checkpoint.take() { - checkpoint + #[cfg(any(test, feature = "test-support"))] + pub fn last_message(&self) -> Option { + if let Some(message) = self.pending_message.clone() { + Some(Message::Agent(message)) } else { - return; - }; - - self.finalize_checkpoint(pending_checkpoint, cx); + self.messages.last().cloned() + } } - fn finalize_checkpoint( + pub fn add_default_tools( &mut self, - pending_checkpoint: ThreadCheckpoint, + environment: Rc, cx: &mut Context, ) { - let git_store = self.project.read(cx).git_store().clone(); - let final_checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx)); - cx.spawn(async move |this, cx| match final_checkpoint.await { - Ok(final_checkpoint) => { - let equal = git_store - .update(cx, |store, cx| { - store.compare_checkpoints( - pending_checkpoint.git_checkpoint.clone(), - final_checkpoint.clone(), - cx, - ) - })? - .await - .unwrap_or(false); - - 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(()) - } - Err(_) => this.update(cx, |this, cx| { - this.insert_checkpoint(pending_checkpoint, cx) - }), - }) - .detach(); + let language_registry = self.project.read(cx).languages().clone(); + self.add_tool(CopyPathTool::new(self.project.clone())); + self.add_tool(CreateDirectoryTool::new(self.project.clone())); + self.add_tool(DeletePathTool::new( + self.project.clone(), + self.action_log.clone(), + )); + self.add_tool(DiagnosticsTool::new(self.project.clone())); + self.add_tool(EditFileTool::new( + self.project.clone(), + cx.weak_entity(), + language_registry, + Templates::new(), + )); + self.add_tool(FetchTool::new(self.project.read(cx).client().http_client())); + self.add_tool(FindPathTool::new(self.project.clone())); + self.add_tool(GrepTool::new(self.project.clone())); + self.add_tool(ListDirectoryTool::new(self.project.clone())); + self.add_tool(MovePathTool::new(self.project.clone())); + self.add_tool(NowTool); + self.add_tool(OpenTool::new(self.project.clone())); + self.add_tool(ReadFileTool::new( + self.project.clone(), + self.action_log.clone(), + )); + self.add_tool(TerminalTool::new(self.project.clone(), environment)); + self.add_tool(ThinkingTool); + self.add_tool(WebSearchTool); } - fn insert_checkpoint(&mut self, checkpoint: ThreadCheckpoint, cx: &mut Context) { - self.checkpoints_by_message - .insert(checkpoint.message_id, checkpoint); - cx.emit(ThreadEvent::CheckpointChanged); - cx.notify(); + pub fn add_tool(&mut self, tool: T) { + self.tools.insert(T::name().into(), tool.erase()); } - pub fn last_restore_checkpoint(&self) -> Option<&LastRestoreCheckpoint> { - self.last_restore_checkpoint.as_ref() + pub fn remove_tool(&mut self, name: &str) -> bool { + self.tools.remove(name).is_some() } - pub fn truncate(&mut self, message_id: MessageId, cx: &mut Context) { - let Some(message_ix) = self - .messages - .iter() - .rposition(|message| message.id == message_id) - else { - return; - }; - for deleted_message in self.messages.drain(message_ix..) { - self.checkpoints_by_message.remove(&deleted_message.id); - } - cx.notify(); + pub fn profile(&self) -> &AgentProfileId { + &self.profile_id } - pub fn context_for_message(&self, id: MessageId) -> impl Iterator { - self.messages - .iter() - .find(|message| message.id == id) - .into_iter() - .flat_map(|message| message.loaded_context.contexts.iter()) + pub fn set_profile(&mut self, profile_id: AgentProfileId) { + self.profile_id = profile_id; } - pub fn is_turn_end(&self, ix: usize) -> bool { - if self.messages.is_empty() { - return false; + pub fn cancel(&mut self, cx: &mut Context) { + if let Some(running_turn) = self.running_turn.take() { + running_turn.cancel(); } + self.flush_pending_message(cx); + } - if !self.is_generating() && ix == self.messages.len() - 1 { - return true; - } + fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context) { + let Some(last_user_message) = self.last_user_message() else { + return; + }; - let Some(message) = self.messages.get(ix) else { - return false; + self.request_token_usage + .insert(last_user_message.id.clone(), update); + cx.emit(TokenUsageUpdated(self.latest_token_usage())); + cx.notify(); + } + + pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context) -> Result<()> { + self.cancel(cx); + let Some(position) = self.messages.iter().position( + |msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id), + ) else { + return Err(anyhow!("Message not found")); }; - if message.role != Role::Assistant { - return false; + for message in self.messages.drain(position..) { + match message { + Message::User(message) => { + self.request_token_usage.remove(&message.id); + } + Message::Agent(_) | Message::Resume => {} + } } - - self.messages - .get(ix + 1) - .and_then(|message| { - self.message(message.id) - .map(|next_message| next_message.role == Role::User && !next_message.is_hidden) - }) - .unwrap_or(false) + self.clear_summary(); + cx.notify(); + Ok(()) } - pub fn tool_use_limit_reached(&self) -> bool { - self.tool_use_limit_reached - } + pub fn latest_token_usage(&self) -> Option { + let last_user_message = self.last_user_message()?; + let tokens = self.request_token_usage.get(&last_user_message.id)?; + let model = self.model.clone()?; - /// Returns whether all of the tool uses have finished running. - pub fn all_tools_finished(&self) -> bool { - // If the only pending tool uses left are the ones with errors, then - // that means that we've finished running all of the pending tools. - self.tool_use - .pending_tool_uses() - .iter() - .all(|pending_tool_use| pending_tool_use.status.is_error()) + Some(acp_thread::TokenUsage { + max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), + used_tokens: tokens.total_tokens(), + }) } - /// Returns whether any pending tool uses may perform edits - pub fn has_pending_edit_tool_uses(&self) -> bool { - self.tool_use - .pending_tool_uses() - .iter() - .filter(|pending_tool_use| !pending_tool_use.status.is_error()) - .any(|pending_tool_use| pending_tool_use.may_perform_edits) - } + pub fn resume( + &mut self, + cx: &mut Context, + ) -> Result>> { + self.messages.push(Message::Resume); + cx.notify(); - pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec { - self.tool_use.tool_uses_for_message(id, &self.project, cx) + log::debug!("Total messages in thread: {}", self.messages.len()); + self.run_turn(cx) } - pub fn tool_results_for_message( - &self, - assistant_message_id: MessageId, - ) -> Vec<&LanguageModelToolResult> { - self.tool_use.tool_results_for_message(assistant_message_id) - } + /// Sending a message results in the model streaming a response, which could include tool calls. + /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. + /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. + pub fn send( + &mut self, + id: UserMessageId, + content: impl IntoIterator, + cx: &mut Context, + ) -> Result>> + where + T: Into, + { + let model = self.model().context("No language model configured")?; - pub fn tool_result(&self, id: &LanguageModelToolUseId) -> Option<&LanguageModelToolResult> { - self.tool_use.tool_result(id) - } + log::info!("Thread::send called with model: {}", model.name().0); + self.advance_prompt_id(); - pub fn output_for_tool(&self, id: &LanguageModelToolUseId) -> Option<&Arc> { - match &self.tool_use.tool_result(id)?.content { - LanguageModelToolResultContent::Text(text) => Some(text), - LanguageModelToolResultContent::Image(_) => { - // TODO: We should display image - None - } - } - } + let content = content.into_iter().map(Into::into).collect::>(); + log::debug!("Thread::send content: {:?}", content); - pub fn card_for_tool(&self, id: &LanguageModelToolUseId) -> Option { - self.tool_use.tool_result_card(id).cloned() - } + self.messages + .push(Message::User(UserMessage { id, content })); + cx.notify(); - /// Return tools that are both enabled and supported by the model - pub fn available_tools( - &self, - cx: &App, - model: Arc, - ) -> Vec { - if model.supports_tools() { - self.profile - .enabled_tools(cx) - .into_iter() - .filter_map(|(name, tool)| { - // Skip tools that cannot be supported - let input_schema = tool.input_schema(model.tool_input_format()).ok()?; - Some(LanguageModelRequestTool { - name: name.into(), - description: tool.description(), - input_schema, - }) - }) - .collect() - } else { - Vec::default() - } + log::debug!("Total messages in thread: {}", self.messages.len()); + self.run_turn(cx) } - pub fn insert_user_message( + fn run_turn( &mut self, - text: impl Into, - loaded_context: ContextLoadResult, - git_checkpoint: Option, - creases: Vec, cx: &mut Context, - ) -> MessageId { - if !loaded_context.referenced_buffers.is_empty() { - self.action_log.update(cx, |log, cx| { - for buffer in loaded_context.referenced_buffers { - log.buffer_read(buffer, cx); + ) -> Result>> { + self.cancel(cx); + + let model = self.model.clone().context("No language model configured")?; + let profile = AgentSettings::get_global(cx) + .profiles + .get(&self.profile_id) + .context("Profile not found")?; + let (events_tx, events_rx) = mpsc::unbounded::>(); + let event_stream = ThreadEventStream(events_tx); + let message_ix = self.messages.len().saturating_sub(1); + self.tool_use_limit_reached = false; + self.clear_summary(); + self.running_turn = Some(RunningTurn { + event_stream: event_stream.clone(), + tools: self.enabled_tools(profile, &model, cx), + _task: cx.spawn(async move |this, cx| { + log::debug!("Starting agent turn execution"); + + let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; + _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); + + match turn_result { + Ok(()) => { + log::debug!("Turn execution completed"); + event_stream.send_stop(acp::StopReason::EndTurn); + } + Err(error) => { + log::error!("Turn execution failed: {:?}", error); + match error.downcast::() { + Ok(CompletionError::Refusal) => { + event_stream.send_stop(acp::StopReason::Refusal); + _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); + } + Ok(CompletionError::MaxTokens) => { + event_stream.send_stop(acp::StopReason::MaxTokens); + } + Ok(CompletionError::Other(error)) | Err(error) => { + event_stream.send_error(error); + } + } + } } - }); - } - let message_id = self.insert_message( - Role::User, - vec![MessageSegment::Text(text.into())], - loaded_context.loaded_context, - creases, - false, - cx, - ); + _ = this.update(cx, |this, _| this.running_turn.take()); + }), + }); + Ok(events_rx) + } - if let Some(git_checkpoint) = git_checkpoint { - self.pending_checkpoint = Some(ThreadCheckpoint { - message_id, - git_checkpoint, - }); - } + async fn run_turn_internal( + this: &WeakEntity, + model: Arc, + event_stream: &ThreadEventStream, + cx: &mut AsyncApp, + ) -> Result<()> { + let mut attempt = 0; + let mut intent = CompletionIntent::UserPrompt; + loop { + let request = + this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; - message_id - } + telemetry::event!( + "Agent Thread Completion", + thread_id = this.read_with(cx, |this, _| this.id.to_string())?, + prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, + model = model.telemetry_id(), + model_provider = model.provider_id().to_string(), + attempt + ); - pub fn insert_invisible_continue_message(&mut self, cx: &mut Context) -> MessageId { - let id = self.insert_message( - Role::User, - vec![MessageSegment::Text("Continue where you left off".into())], - LoadedContext::default(), - vec![], - true, - cx, - ); - self.pending_checkpoint = None; + log::debug!("Calling model.stream_completion, attempt {}", attempt); - id - } + let (mut events, mut error) = match model.stream_completion(request, cx).await { + Ok(events) => (events, None), + Err(err) => (stream::empty().boxed(), Some(err)), + }; + let mut tool_results = FuturesUnordered::new(); + while let Some(event) = events.next().await { + log::trace!("Received completion event: {:?}", event); + match event { + Ok(event) => { + tool_results.extend(this.update(cx, |this, cx| { + this.handle_completion_event(event, event_stream, cx) + })??); + } + Err(err) => { + error = Some(err); + break; + } + } + } - pub fn insert_assistant_message( - &mut self, - segments: Vec, - cx: &mut Context, - ) -> MessageId { - self.insert_message( - Role::Assistant, - segments, - LoadedContext::default(), - Vec::new(), - false, - cx, - ) - } + let end_turn = tool_results.is_empty(); + while let Some(tool_result) = tool_results.next().await { + log::debug!("Tool finished {:?}", tool_result); - pub fn insert_message( - &mut self, - role: Role, - segments: Vec, - loaded_context: LoadedContext, - creases: Vec, - is_hidden: bool, - cx: &mut Context, - ) -> MessageId { - let id = self.next_message_id.post_inc(); - self.messages.push(Message { - id, - role, - segments, - loaded_context, - creases, - is_hidden, - ui_only: false, - }); - self.touch_updated_at(); - cx.emit(ThreadEvent::MessageAdded(id)); - id + event_stream.update_tool_call_fields( + &tool_result.tool_use_id, + acp::ToolCallUpdateFields { + status: Some(if tool_result.is_error { + acp::ToolCallStatus::Failed + } else { + acp::ToolCallStatus::Completed + }), + raw_output: tool_result.output.clone(), + ..Default::default() + }, + ); + this.update(cx, |this, _cx| { + this.pending_message() + .tool_results + .insert(tool_result.tool_use_id.clone(), tool_result); + })?; + } + + this.update(cx, |this, cx| { + this.flush_pending_message(cx); + if this.title.is_none() && this.pending_title_generation.is_none() { + this.generate_title(cx); + } + })?; + + if let Some(error) = error { + attempt += 1; + let retry = this.update(cx, |this, cx| { + let user_store = this.user_store.read(cx); + this.handle_completion_error(error, attempt, user_store.plan()) + })??; + let timer = cx.background_executor().timer(retry.duration); + event_stream.send_retry(retry); + timer.await; + this.update(cx, |this, _cx| { + if let Some(Message::Agent(message)) = this.messages.last() { + if message.tool_results.is_empty() { + intent = CompletionIntent::UserPrompt; + this.messages.push(Message::Resume); + } + } + })?; + } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { + return Err(language_model::ToolUseLimitReachedError.into()); + } else if end_turn { + return Ok(()); + } else { + intent = CompletionIntent::ToolResults; + attempt = 0; + } + } } - pub fn edit_message( + fn handle_completion_error( &mut self, - id: MessageId, - new_role: Role, - new_segments: Vec, - creases: Vec, - loaded_context: Option, - checkpoint: Option, - cx: &mut Context, - ) -> bool { - let Some(message) = self.messages.iter_mut().find(|message| message.id == id) else { - return false; + error: LanguageModelCompletionError, + attempt: u8, + plan: Option, + ) -> Result { + let Some(model) = self.model.as_ref() else { + return Err(anyhow!(error)); + }; + + let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID { + match plan { + Some(Plan::V2(_)) => true, + Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn, + None => false, + } + } else { + true }; - message.role = new_role; - message.segments = new_segments; - message.creases = creases; - if let Some(context) = loaded_context { - message.loaded_context = context; + + if !auto_retry { + return Err(anyhow!(error)); } - if let Some(git_checkpoint) = checkpoint { - self.checkpoints_by_message.insert( - id, - ThreadCheckpoint { - message_id: id, - git_checkpoint, - }, - ); + + let Some(strategy) = Self::retry_strategy_for(&error) else { + return Err(anyhow!(error)); + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + + if attempt > max_attempts { + return Err(anyhow!(error)); } - self.touch_updated_at(); - cx.emit(ThreadEvent::MessageEdited(id)); - true - } - pub fn delete_message(&mut self, id: MessageId, cx: &mut Context) -> bool { - let Some(index) = self.messages.iter().position(|message| message.id == id) else { - return false; + let delay = match &strategy { + RetryStrategy::ExponentialBackoff { initial_delay, .. } => { + let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); + Duration::from_secs(delay_secs) + } + RetryStrategy::Fixed { delay, .. } => *delay, }; - self.messages.remove(index); - self.touch_updated_at(); - cx.emit(ThreadEvent::MessageDeleted(id)); - true + log::debug!("Retry attempt {attempt} with delay {delay:?}"); + + Ok(acp_thread::RetryStatus { + last_error: error.to_string().into(), + attempt: attempt as usize, + max_attempts: max_attempts as usize, + started_at: Instant::now(), + duration: delay, + }) } - /// Returns the representation of this [`Thread`] in a textual form. - /// - /// This is the representation we use when attaching a thread as context to another thread. - pub fn text(&self) -> String { - let mut text = String::new(); - - for message in &self.messages { - text.push_str(match message.role { - language_model::Role::User => "User:", - language_model::Role::Assistant => "Agent:", - language_model::Role::System => "System:", - }); - text.push('\n'); + /// A helper method that's called on every streamed completion event. + /// Returns an optional tool result task, which the main agentic loop will + /// send back to the model when it resolves. + fn handle_completion_event( + &mut self, + event: LanguageModelCompletionEvent, + event_stream: &ThreadEventStream, + cx: &mut Context, + ) -> Result>> { + log::trace!("Handling streamed completion event: {:?}", event); + use LanguageModelCompletionEvent::*; - for segment in &message.segments { - match segment { - MessageSegment::Text(content) => text.push_str(content), - MessageSegment::Thinking { text: content, .. } => { - text.push_str(&format!("{}", content)) - } - MessageSegment::RedactedThinking(_) => {} - } + match event { + StartMessage { .. } => { + self.flush_pending_message(cx); + self.pending_message = Some(AgentMessage::default()); + } + Text(new_text) => self.handle_text_event(new_text, event_stream, cx), + Thinking { text, signature } => { + self.handle_thinking_event(text, signature, event_stream, cx) + } + RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), + ToolUse(tool_use) => { + return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); + } + ToolUseJsonParseError { + id, + tool_name, + raw_input, + json_parse_error, + } => { + return Ok(Some(Task::ready( + self.handle_tool_use_json_parse_error_event( + id, + tool_name, + raw_input, + json_parse_error, + ), + ))); + } + UsageUpdate(usage) => { + telemetry::event!( + "Agent Thread Completion Usage Updated", + thread_id = self.id.to_string(), + prompt_id = self.prompt_id.to_string(), + model = self.model.as_ref().map(|m| m.telemetry_id()), + model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), + input_tokens = usage.input_tokens, + output_tokens = usage.output_tokens, + cache_creation_input_tokens = usage.cache_creation_input_tokens, + cache_read_input_tokens = usage.cache_read_input_tokens, + ); + self.update_token_usage(usage, cx); + } + StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => { + self.update_model_request_usage(amount, limit, cx); } - text.push('\n'); + StatusUpdate( + CompletionRequestStatus::Started + | CompletionRequestStatus::Queued { .. } + | CompletionRequestStatus::Failed { .. }, + ) => {} + StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => { + self.tool_use_limit_reached = true; + } + Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), + Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), + Stop(StopReason::ToolUse | StopReason::EndTurn) => {} } - text + Ok(None) } - /// Serializes this thread into a format for storage or telemetry. - pub fn serialize(&self, cx: &mut Context) -> Task> { - let initial_project_snapshot = self.initial_project_snapshot.clone(); - cx.spawn(async move |this, cx| { - let initial_project_snapshot = initial_project_snapshot.await; - this.read_with(cx, |this, cx| SerializedThread { - version: SerializedThread::VERSION.to_string(), - summary: this.summary().or_default(), - updated_at: this.updated_at(), - messages: this - .messages() - .filter(|message| !message.ui_only) - .map(|message| SerializedMessage { - id: message.id, - role: message.role, - segments: message - .segments - .iter() - .map(|segment| match segment { - MessageSegment::Text(text) => { - SerializedMessageSegment::Text { text: text.clone() } - } - MessageSegment::Thinking { text, signature } => { - SerializedMessageSegment::Thinking { - text: text.clone(), - signature: signature.clone(), - } - } - MessageSegment::RedactedThinking(data) => { - SerializedMessageSegment::RedactedThinking { - data: data.clone(), - } - } - }) - .collect(), - tool_uses: this - .tool_uses_for_message(message.id, cx) - .into_iter() - .map(|tool_use| SerializedToolUse { - id: tool_use.id, - name: tool_use.name, - input: tool_use.input, - }) - .collect(), - tool_results: this - .tool_results_for_message(message.id) - .into_iter() - .map(|tool_result| SerializedToolResult { - tool_use_id: tool_result.tool_use_id.clone(), - is_error: tool_result.is_error, - content: tool_result.content.clone(), - output: tool_result.output.clone(), - }) - .collect(), - context: message.loaded_context.text.clone(), - creases: message - .creases - .iter() - .map(|crease| SerializedCrease { - start: crease.range.start, - end: crease.range.end, - icon_path: crease.icon_path.clone(), - label: crease.label.clone(), - }) - .collect(), - is_hidden: message.is_hidden, - }) - .collect(), - initial_project_snapshot, - cumulative_token_usage: this.cumulative_token_usage, - request_token_usage: this.request_token_usage.clone(), - detailed_summary_state: this.detailed_summary_rx.borrow().clone(), - exceeded_window_error: this.exceeded_window_error.clone(), - model: this - .configured_model - .as_ref() - .map(|model| SerializedLanguageModel { - provider: model.provider.id().0.to_string(), - model: model.model.id().0.to_string(), - }), - completion_mode: Some(this.completion_mode), - tool_use_limit_reached: this.tool_use_limit_reached, - profile: Some(this.profile.id().clone()), - }) - }) - } + fn handle_text_event( + &mut self, + new_text: String, + event_stream: &ThreadEventStream, + cx: &mut Context, + ) { + event_stream.send_text(&new_text); - pub fn remaining_turns(&self) -> u32 { - self.remaining_turns - } + let last_message = self.pending_message(); + if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { + text.push_str(&new_text); + } else { + last_message + .content + .push(AgentMessageContent::Text(new_text)); + } - pub fn set_remaining_turns(&mut self, remaining_turns: u32) { - self.remaining_turns = remaining_turns; + cx.notify(); } - pub fn send_to_model( + fn handle_thinking_event( &mut self, - model: Arc, - intent: CompletionIntent, - window: Option, + new_text: String, + new_signature: Option, + event_stream: &ThreadEventStream, cx: &mut Context, ) { - if self.remaining_turns == 0 { - return; - } + event_stream.send_thinking(&new_text); - self.remaining_turns -= 1; + let last_message = self.pending_message(); + if let Some(AgentMessageContent::Thinking { text, signature }) = + last_message.content.last_mut() + { + text.push_str(&new_text); + *signature = new_signature.or(signature.take()); + } else { + last_message.content.push(AgentMessageContent::Thinking { + text: new_text, + signature: new_signature, + }); + } - self.flush_notifications(model.clone(), intent, cx); + cx.notify(); + } - let _checkpoint = self.finalize_pending_checkpoint(cx); - self.stream_completion( - self.to_completion_request(model.clone(), intent, cx), - model, - intent, - window, - cx, - ); + fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { + let last_message = self.pending_message(); + last_message + .content + .push(AgentMessageContent::RedactedThinking(data)); + cx.notify(); } - pub fn to_completion_request( - &self, - model: Arc, - intent: CompletionIntent, + fn handle_tool_use_event( + &mut self, + tool_use: LanguageModelToolUse, + event_stream: &ThreadEventStream, cx: &mut Context, - ) -> LanguageModelRequest { - let mut request = LanguageModelRequest { - thread_id: Some(self.id.to_string()), - prompt_id: Some(self.last_prompt_id.to_string()), - intent: Some(intent), - mode: None, - messages: vec![], - tools: Vec::new(), - tool_choice: None, - stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(&model, cx), - thinking_allowed: true, - }; - - let available_tools = self.available_tools(cx, model.clone()); - let available_tool_names = available_tools - .iter() - .map(|tool| tool.name.clone()) - .collect(); - - let model_context = &ModelContext { - available_tools: available_tool_names, - }; + ) -> Option> { + cx.notify(); - if let Some(project_context) = self.project_context.borrow().as_ref() { - match self - .prompt_builder - .generate_assistant_system_prompt(project_context, model_context) - { - Err(err) => { - let message = format!("{err:?}").into(); - log::error!("{message}"); - cx.emit(ThreadEvent::ShowError(ThreadError::Message { - header: "Error generating system prompt".into(), - message, - })); - } - Ok(system_prompt) => { - request.messages.push(LanguageModelRequestMessage { - role: Role::System, - content: vec![MessageContent::Text(system_prompt)], - cache: true, - }); + let tool = self.tool(tool_use.name.as_ref()); + let mut title = SharedString::from(&tool_use.name); + let mut kind = acp::ToolKind::Other; + if let Some(tool) = tool.as_ref() { + title = tool.initial_title(tool_use.input.clone(), cx); + kind = tool.kind(); + } + + // Ensure the last message ends in the current tool use + let last_message = self.pending_message(); + let push_new_tool_use = last_message.content.last_mut().is_none_or(|content| { + if let AgentMessageContent::ToolUse(last_tool_use) = content { + if last_tool_use.id == tool_use.id { + *last_tool_use = tool_use.clone(); + false + } else { + true } + } else { + true } + }); + + if push_new_tool_use { + event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); + last_message + .content + .push(AgentMessageContent::ToolUse(tool_use.clone())); } else { - let message = "Context for system prompt unexpectedly not ready.".into(); - log::error!("{message}"); - cx.emit(ThreadEvent::ShowError(ThreadError::Message { - header: "Error generating system prompt".into(), - message, - })); + event_stream.update_tool_call_fields( + &tool_use.id, + acp::ToolCallUpdateFields { + title: Some(title.into()), + kind: Some(kind), + raw_input: Some(tool_use.input.clone()), + ..Default::default() + }, + ); } - let mut message_ix_to_cache = None; - for message in &self.messages { - // ui_only messages are for the UI only, not for the model - if message.ui_only { - continue; - } - - let mut request_message = LanguageModelRequestMessage { - role: message.role, - content: Vec::new(), - cache: false, - }; + if !tool_use.is_input_complete { + return None; + } - message - .loaded_context - .add_to_request_message(&mut request_message); - - for segment in &message.segments { - match segment { - MessageSegment::Text(text) => { - let text = text.trim_end(); - if !text.is_empty() { - request_message - .content - .push(MessageContent::Text(text.into())); - } - } - MessageSegment::Thinking { text, signature } => { - if !text.is_empty() { - request_message.content.push(MessageContent::Thinking { - text: text.into(), - signature: signature.clone(), - }); - } - } - MessageSegment::RedactedThinking(data) => { - request_message - .content - .push(MessageContent::RedactedThinking(data.clone())); - } - }; - } + let Some(tool) = tool else { + let content = format!("No tool named {} exists", tool_use.name); + return Some(Task::ready(LanguageModelToolResult { + content: LanguageModelToolResultContent::Text(Arc::from(content)), + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: true, + output: None, + })); + }; - let mut cache_message = true; - let mut tool_results_message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::new(), - cache: false, - }; - for (tool_use, tool_result) in self.tool_use.tool_results(message.id) { - if let Some(tool_result) = tool_result { - request_message - .content - .push(MessageContent::ToolUse(tool_use.clone())); - tool_results_message - .content - .push(MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: tool_use.id.clone(), - tool_name: tool_result.tool_name.clone(), - is_error: tool_result.is_error, - content: if tool_result.content.is_empty() { - // Surprisingly, the API fails if we return an empty string here. - // It thinks we are sending a tool use without a tool result. - "".into() - } else { - tool_result.content.clone() - }, - output: None, - })); - } else { - cache_message = false; - log::debug!( - "skipped tool use {:?} because it is still pending", - tool_use - ); + let fs = self.project.read(cx).fs().clone(); + let tool_event_stream = + ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs)); + tool_event_stream.update_fields(acp::ToolCallUpdateFields { + status: Some(acp::ToolCallStatus::InProgress), + ..Default::default() + }); + let supports_images = self.model().is_some_and(|model| model.supports_images()); + let tool_result = tool.run(tool_use.input, tool_event_stream, cx); + log::debug!("Running tool {}", tool_use.name); + Some(cx.foreground_executor().spawn(async move { + let tool_result = tool_result.await.and_then(|output| { + if let LanguageModelToolResultContent::Image(_) = &output.llm_output + && !supports_images + { + return Err(anyhow!( + "Attempted to read an image, but this model doesn't support it.", + )); } - } + Ok(output) + }); - if cache_message { - message_ix_to_cache = Some(request.messages.len()); + match tool_result { + Ok(output) => LanguageModelToolResult { + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: false, + content: output.llm_output, + output: Some(output.raw_output), + }, + Err(error) => LanguageModelToolResult { + tool_use_id: tool_use.id, + tool_name: tool_use.name, + is_error: true, + content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), + output: Some(error.to_string().into()), + }, } - request.messages.push(request_message); + })) + } - if !tool_results_message.content.is_empty() { - if cache_message { - message_ix_to_cache = Some(request.messages.len()); - } - request.messages.push(tool_results_message); - } + fn handle_tool_use_json_parse_error_event( + &mut self, + tool_use_id: LanguageModelToolUseId, + tool_name: Arc, + raw_input: Arc, + json_parse_error: String, + ) -> LanguageModelToolResult { + let tool_output = format!("Error parsing input JSON: {json_parse_error}"); + LanguageModelToolResult { + tool_use_id, + tool_name, + is_error: true, + content: LanguageModelToolResultContent::Text(tool_output.into()), + output: Some(serde_json::Value::String(raw_input.to_string())), } + } - // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching - if let Some(message_ix_to_cache) = message_ix_to_cache { - request.messages[message_ix_to_cache].cache = true; - } + fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context) { + self.project + .read(cx) + .user_store() + .update(cx, |user_store, cx| { + user_store.update_model_request_usage( + ModelRequestUsage(RequestUsage { + amount: amount as i32, + limit, + }), + cx, + ) + }); + } - request.tools = available_tools; - request.mode = if model.supports_burn_mode() { - Some(self.completion_mode.into()) - } else { - Some(CompletionMode::Normal.into()) - }; + pub fn title(&self) -> SharedString { + self.title.clone().unwrap_or("New Thread".into()) + } - request + pub fn is_generating_summary(&self) -> bool { + self.pending_summary_generation.is_some() } - fn to_summarize_request( - &self, - model: &Arc, - intent: CompletionIntent, - added_user_message: String, - cx: &App, - ) -> LanguageModelRequest { + pub fn summary(&mut self, cx: &mut Context) -> Shared>> { + if let Some(summary) = self.summary.as_ref() { + return Task::ready(Some(summary.clone())).shared(); + } + if let Some(task) = self.pending_summary_generation.clone() { + return task; + } + let Some(model) = self.summarization_model.clone() else { + log::error!("No summarization model available"); + return Task::ready(None).shared(); + }; let mut request = LanguageModelRequest { - thread_id: None, - prompt_id: None, - intent: Some(intent), - mode: None, - messages: vec![], - tools: Vec::new(), - tool_choice: None, - stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(model, cx), - thinking_allowed: false, + intent: Some(CompletionIntent::ThreadContextSummarization), + temperature: AgentSettings::temperature_for_model(&model, cx), + ..Default::default() }; for message in &self.messages { - let mut request_message = LanguageModelRequestMessage { - role: message.role, - content: Vec::new(), - cache: false, - }; - - for segment in &message.segments { - match segment { - MessageSegment::Text(text) => request_message - .content - .push(MessageContent::Text(text.clone())), - MessageSegment::Thinking { .. } => {} - MessageSegment::RedactedThinking(_) => {} - } - } - - if request_message.content.is_empty() { - continue; - } - - request.messages.push(request_message); + request.messages.extend(message.to_request()); } request.messages.push(LanguageModelRequestMessage { role: Role::User, - content: vec![MessageContent::Text(added_user_message)], + content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], cache: false, }); - request - } + let task = cx + .spawn(async move |this, cx| { + let mut summary = String::new(); + let mut messages = model.stream_completion(request, cx).await.log_err()?; + while let Some(event) = messages.next().await { + let event = event.log_err()?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + ) => { + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + }) + .ok()?; + continue; + } + _ => continue, + }; - /// Insert auto-generated notifications (if any) to the thread - fn flush_notifications( - &mut self, - model: Arc, - intent: CompletionIntent, - cx: &mut Context, - ) { - match intent { - CompletionIntent::UserPrompt | CompletionIntent::ToolResults => { - if let Some(pending_tool_use) = self.attach_tracked_files_state(model, cx) { - cx.emit(ThreadEvent::ToolFinished { - tool_use_id: pending_tool_use.id.clone(), - pending_tool_use: Some(pending_tool_use), - }); + let mut lines = text.lines(); + summary.extend(lines.next()); } - } - CompletionIntent::ThreadSummarization - | CompletionIntent::ThreadContextSummarization - | CompletionIntent::CreateFile - | CompletionIntent::EditFile - | CompletionIntent::InlineAssist - | CompletionIntent::TerminalInlineAssist - | CompletionIntent::GenerateGitCommitMessage => {} - }; - } - - fn attach_tracked_files_state( - &mut self, - model: Arc, - cx: &mut App, - ) -> Option { - // Represent notification as a simulated `project_notifications` tool call - let tool_name = Arc::from("project_notifications"); - let tool = self.tools.read(cx).tool(&tool_name, cx)?; - - if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) { - return None; - } - if self - .action_log - .update(cx, |log, cx| log.unnotified_user_edits(cx).is_none()) - { - return None; - } + log::debug!("Setting summary: {}", summary); + let summary = SharedString::from(summary); - let input = serde_json::json!({}); - let request = Arc::new(LanguageModelRequest::default()); // unused - let window = None; - let tool_result = tool.run( - input, - request, - self.project.clone(), - self.action_log.clone(), - model.clone(), - window, - cx, - ); + this.update(cx, |this, cx| { + this.summary = Some(summary.clone()); + this.pending_summary_generation = None; + cx.notify() + }) + .ok()?; - let tool_use_id = - LanguageModelToolUseId::from(format!("project_notifications_{}", self.messages.len())); + Some(summary) + }) + .shared(); + self.pending_summary_generation = Some(task.clone()); + task + } - let tool_use = LanguageModelToolUse { - id: tool_use_id.clone(), - name: tool_name.clone(), - raw_input: "{}".to_string(), - input: serde_json::json!({}), - is_input_complete: true, + fn generate_title(&mut self, cx: &mut Context) { + let Some(model) = self.summarization_model.clone() else { + return; }; - let tool_output = cx.background_executor().block(tool_result.output); - - // Attach a project_notification tool call to the latest existing - // Assistant message. We cannot create a new Assistant message - // because thinking models require a `thinking` block that we - // cannot mock. We cannot send a notification as a normal - // (non-tool-use) User message because this distracts Agent - // too much. - let tool_message_id = self - .messages - .iter() - .enumerate() - .rfind(|(_, message)| message.role == Role::Assistant) - .map(|(_, message)| message.id)?; - - let tool_use_metadata = ToolUseMetadata { - model: model.clone(), - thread_id: self.id.clone(), - prompt_id: self.last_prompt_id.clone(), + log::debug!( + "Generating title with model: {:?}", + self.summarization_model.as_ref().map(|model| model.name()) + ); + let mut request = LanguageModelRequest { + intent: Some(CompletionIntent::ThreadSummarization), + temperature: AgentSettings::temperature_for_model(&model, cx), + ..Default::default() }; - self.tool_use - .request_tool_use(tool_message_id, tool_use, tool_use_metadata, cx); - - self.tool_use.insert_tool_output( - tool_use_id, - tool_name, - tool_output, - self.configured_model.as_ref(), - self.completion_mode, - ) - } - - pub fn stream_completion( - &mut self, - request: LanguageModelRequest, - model: Arc, - intent: CompletionIntent, - window: Option, - cx: &mut Context, - ) { - self.tool_use_limit_reached = false; - - let pending_completion_id = post_inc(&mut self.completion_count); - let mut request_callback_parameters = if self.request_callback.is_some() { - Some((request.clone(), Vec::new())) - } else { - None - }; - let prompt_id = self.last_prompt_id.clone(); - let tool_use_metadata = ToolUseMetadata { - model: model.clone(), - thread_id: self.id.clone(), - prompt_id: prompt_id.clone(), - }; + for message in &self.messages { + request.messages.extend(message.to_request()); + } - let completion_mode = request - .mode - .unwrap_or(cloud_llm_client::CompletionMode::Normal); + request.messages.push(LanguageModelRequestMessage { + role: Role::User, + content: vec![SUMMARIZE_THREAD_PROMPT.into()], + cache: false, + }); + self.pending_title_generation = Some(cx.spawn(async move |this, cx| { + let mut title = String::new(); - self.last_received_chunk_at = Some(Instant::now()); + let generate = async { + let mut messages = model.stream_completion(request, cx).await?; + while let Some(event) = messages.next().await { + let event = event?; + let text = match event { + LanguageModelCompletionEvent::Text(text) => text, + LanguageModelCompletionEvent::StatusUpdate( + CompletionRequestStatus::UsageUpdated { amount, limit }, + ) => { + this.update(cx, |thread, cx| { + thread.update_model_request_usage(amount, limit, cx); + })?; + continue; + } + _ => continue, + }; - let task = cx.spawn(async move |thread, cx| { - let stream_completion_future = model.stream_completion(request, cx); - let initial_token_usage = - thread.read_with(cx, |thread, _cx| thread.cumulative_token_usage); - let stream_completion = async { - let mut events = stream_completion_future.await?; + let mut lines = text.lines(); + title.extend(lines.next()); - let mut stop_reason = StopReason::EndTurn; - let mut current_token_usage = TokenUsage::default(); + // Stop if the LLM generated multiple lines. + if lines.next().is_some() { + break; + } + } + anyhow::Ok(()) + }; - thread - .update(cx, |_thread, cx| { - cx.emit(ThreadEvent::NewRequest); - }) - .ok(); + if generate.await.context("failed to generate title").is_ok() { + _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); + } + _ = this.update(cx, |this, _| this.pending_title_generation = None); + })); + } - let mut request_assistant_message_id = None; + pub fn set_title(&mut self, title: SharedString, cx: &mut Context) { + self.pending_title_generation = None; + if Some(&title) != self.title.as_ref() { + self.title = Some(title); + cx.emit(TitleUpdated); + cx.notify(); + } + } - while let Some(event) = events.next().await { - if let Some((_, response_events)) = request_callback_parameters.as_mut() { - response_events - .push(event.as_ref().map_err(|error| error.to_string()).cloned()); - } + fn clear_summary(&mut self) { + self.summary = None; + self.pending_summary_generation = None; + } - thread.update(cx, |thread, cx| { - match event? { - LanguageModelCompletionEvent::StartMessage { .. } => { - request_assistant_message_id = - Some(thread.insert_assistant_message( - vec![MessageSegment::Text(String::new())], - cx, - )); - } - LanguageModelCompletionEvent::Stop(reason) => { - stop_reason = reason; - } - LanguageModelCompletionEvent::UsageUpdate(token_usage) => { - thread.update_token_usage_at_last_message(token_usage); - thread.cumulative_token_usage = thread.cumulative_token_usage - + token_usage - - current_token_usage; - current_token_usage = token_usage; - } - LanguageModelCompletionEvent::Text(chunk) => { - thread.received_chunk(); - - cx.emit(ThreadEvent::ReceivedTextChunk); - if let Some(last_message) = thread.messages.last_mut() { - if last_message.role == Role::Assistant - && !thread.tool_use.has_tool_results(last_message.id) - { - last_message.push_text(&chunk); - cx.emit(ThreadEvent::StreamedAssistantText( - last_message.id, - chunk, - )); - } else { - // If we won't have an Assistant message yet, assume this chunk marks the beginning - // of a new Assistant response. - // - // Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it - // will result in duplicating the text of the chunk in the rendered Markdown. - request_assistant_message_id = - Some(thread.insert_assistant_message( - vec![MessageSegment::Text(chunk.to_string())], - cx, - )); - }; - } - } - LanguageModelCompletionEvent::Thinking { - text: chunk, - signature, - } => { - thread.received_chunk(); - - if let Some(last_message) = thread.messages.last_mut() { - if last_message.role == Role::Assistant - && !thread.tool_use.has_tool_results(last_message.id) - { - last_message.push_thinking(&chunk, signature); - cx.emit(ThreadEvent::StreamedAssistantThinking( - last_message.id, - chunk, - )); - } else { - // If we won't have an Assistant message yet, assume this chunk marks the beginning - // of a new Assistant response. - // - // Importantly: We do *not* want to emit a `StreamedAssistantText` event here, as it - // will result in duplicating the text of the chunk in the rendered Markdown. - request_assistant_message_id = - Some(thread.insert_assistant_message( - vec![MessageSegment::Thinking { - text: chunk.to_string(), - signature, - }], - cx, - )); - }; - } - } - LanguageModelCompletionEvent::RedactedThinking { data } => { - thread.received_chunk(); - - if let Some(last_message) = thread.messages.last_mut() { - if last_message.role == Role::Assistant - && !thread.tool_use.has_tool_results(last_message.id) - { - last_message.push_redacted_thinking(data); - } else { - request_assistant_message_id = - Some(thread.insert_assistant_message( - vec![MessageSegment::RedactedThinking(data)], - cx, - )); - }; - } - } - LanguageModelCompletionEvent::ToolUse(tool_use) => { - let last_assistant_message_id = request_assistant_message_id - .unwrap_or_else(|| { - let new_assistant_message_id = - thread.insert_assistant_message(vec![], cx); - request_assistant_message_id = - Some(new_assistant_message_id); - new_assistant_message_id - }); - - let tool_use_id = tool_use.id.clone(); - let streamed_input = if tool_use.is_input_complete { - None - } else { - Some(tool_use.input.clone()) - }; + fn last_user_message(&self) -> Option<&UserMessage> { + self.messages + .iter() + .rev() + .find_map(|message| match message { + Message::User(user_message) => Some(user_message), + Message::Agent(_) => None, + Message::Resume => None, + }) + } - let ui_text = thread.tool_use.request_tool_use( - last_assistant_message_id, - tool_use, - tool_use_metadata.clone(), - cx, - ); + fn pending_message(&mut self) -> &mut AgentMessage { + self.pending_message.get_or_insert_default() + } - if let Some(input) = streamed_input { - cx.emit(ThreadEvent::StreamedToolUse { - tool_use_id, - ui_text, - input, - }); - } - } - LanguageModelCompletionEvent::ToolUseJsonParseError { - id, - tool_name, - raw_input: invalid_input_json, - json_parse_error, - } => { - thread.receive_invalid_tool_json( - id, - tool_name, - invalid_input_json, - json_parse_error, - window, - cx, - ); - } - LanguageModelCompletionEvent::StatusUpdate(status_update) => { - if let Some(completion) = thread - .pending_completions - .iter_mut() - .find(|completion| completion.id == pending_completion_id) - { - match status_update { - CompletionRequestStatus::Queued { position } => { - completion.queue_state = - QueueState::Queued { position }; - } - CompletionRequestStatus::Started => { - completion.queue_state = QueueState::Started; - } - CompletionRequestStatus::Failed { - code, - message, - request_id: _, - retry_after, - } => { - return Err( - LanguageModelCompletionError::from_cloud_failure( - model.upstream_provider_name(), - code, - message, - retry_after.map(Duration::from_secs_f64), - ), - ); - } - CompletionRequestStatus::UsageUpdated { amount, limit } => { - thread.update_model_request_usage( - amount as u32, - limit, - cx, - ); - } - CompletionRequestStatus::ToolUseLimitReached => { - thread.tool_use_limit_reached = true; - cx.emit(ThreadEvent::ToolUseLimitReached); - } - } - } - } - } + fn flush_pending_message(&mut self, cx: &mut Context) { + let Some(mut message) = self.pending_message.take() else { + return; + }; - thread.touch_updated_at(); - cx.emit(ThreadEvent::StreamedCompletion); - cx.notify(); + if message.content.is_empty() { + return; + } - Ok(()) - })??; + for content in &message.content { + let AgentMessageContent::ToolUse(tool_use) = content else { + continue; + }; - smol::future::yield_now().await; - } + if !message.tool_results.contains_key(&tool_use.id) { + message.tool_results.insert( + tool_use.id.clone(), + LanguageModelToolResult { + tool_use_id: tool_use.id.clone(), + tool_name: tool_use.name.clone(), + is_error: true, + content: LanguageModelToolResultContent::Text(TOOL_CANCELED_MESSAGE.into()), + output: None, + }, + ); + } + } - thread.update(cx, |thread, cx| { - thread.last_received_chunk_at = None; - thread - .pending_completions - .retain(|completion| completion.id != pending_completion_id); - - // If there is a response without tool use, summarize the message. Otherwise, - // allow two tool uses before summarizing. - if matches!(thread.summary, ThreadSummary::Pending) - && thread.messages.len() >= 2 - && (!thread.has_pending_tool_uses() || thread.messages.len() >= 6) - { - thread.summarize(cx); - } - })?; + self.messages.push(Message::Agent(message)); + self.updated_at = Utc::now(); + self.clear_summary(); + cx.notify() + } - anyhow::Ok(stop_reason) - }; + pub(crate) fn build_completion_request( + &self, + completion_intent: CompletionIntent, + cx: &App, + ) -> Result { + let model = self.model().context("No language model configured")?; + let tools = if let Some(turn) = self.running_turn.as_ref() { + turn.tools + .iter() + .filter_map(|(tool_name, tool)| { + log::trace!("Including tool: {}", tool_name); + Some(LanguageModelRequestTool { + name: tool_name.to_string(), + description: tool.description().to_string(), + input_schema: tool.input_schema(model.tool_input_format()).log_err()?, + }) + }) + .collect::>() + } else { + Vec::new() + }; - let result = stream_completion.await; - let mut retry_scheduled = false; + log::debug!("Building completion request"); + log::debug!("Completion intent: {:?}", completion_intent); + log::debug!("Completion mode: {:?}", self.completion_mode); - thread - .update(cx, |thread, cx| { - thread.finalize_pending_checkpoint(cx); - match result.as_ref() { - Ok(stop_reason) => { - match stop_reason { - StopReason::ToolUse => { - let tool_uses = - thread.use_pending_tools(window, model.clone(), cx); - cx.emit(ThreadEvent::UsePendingTools { tool_uses }); - } - StopReason::EndTurn | StopReason::MaxTokens => { - thread.project.update(cx, |project, cx| { - project.set_agent_location(None, cx); - }); - } - StopReason::Refusal => { - thread.project.update(cx, |project, cx| { - project.set_agent_location(None, cx); - }); - - // Remove the turn that was refused. - // - // https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/handle-streaming-refusals#reset-context-after-refusal - { - let mut messages_to_remove = Vec::new(); - - for (ix, message) in - thread.messages.iter().enumerate().rev() - { - messages_to_remove.push(message.id); - - if message.role == Role::User { - if ix == 0 { - break; - } - - if let Some(prev_message) = - thread.messages.get(ix - 1) - && prev_message.role == Role::Assistant { - break; - } - } - } - - for message_id in messages_to_remove { - thread.delete_message(message_id, cx); - } - } - - cx.emit(ThreadEvent::ShowError(ThreadError::Message { - header: "Language model refusal".into(), - message: - "Model refused to generate content for safety reasons." - .into(), - })); - } - } + let messages = self.build_request_messages(cx); + log::debug!("Request will include {} messages", messages.len()); + log::debug!("Request includes {} tools", tools.len()); - // We successfully completed, so cancel any remaining retries. - thread.retry_state = None; - } - Err(error) => { - thread.project.update(cx, |project, cx| { - project.set_agent_location(None, cx); - }); - - if error.is::() { - cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired)); - } else if let Some(error) = - error.downcast_ref::() - { - cx.emit(ThreadEvent::ShowError( - ThreadError::ModelRequestLimitReached { plan: error.plan }, - )); - } else if let Some(completion_error) = - error.downcast_ref::() - { - match &completion_error { - LanguageModelCompletionError::PromptTooLarge { - tokens, .. - } => { - let tokens = tokens.unwrap_or_else(|| { - // We didn't get an exact token count from the API, so fall back on our estimate. - thread - .total_token_usage() - .map(|usage| usage.total) - .unwrap_or(0) - // We know the context window was exceeded in practice, so if our estimate was - // lower than max tokens, the estimate was wrong; return that we exceeded by 1. - .max( - model - .max_token_count_for_mode(completion_mode) - .saturating_add(1), - ) - }); - thread.exceeded_window_error = Some(ExceededWindowError { - model_id: model.id(), - token_count: tokens, - }); - cx.notify(); - } - _ => { - if let Some(retry_strategy) = - Thread::get_retry_strategy(completion_error) - { - log::info!( - "Retrying with {:?} for language model completion error {:?}", - retry_strategy, - completion_error - ); - - retry_scheduled = thread - .handle_retryable_error_with_delay( - completion_error, - Some(retry_strategy), - model.clone(), - intent, - window, - cx, - ); - } - } - } - } + let request = LanguageModelRequest { + thread_id: Some(self.id.to_string()), + prompt_id: Some(self.prompt_id.to_string()), + intent: Some(completion_intent), + mode: Some(self.completion_mode.into()), + messages, + tools, + tool_choice: None, + stop: Vec::new(), + temperature: AgentSettings::temperature_for_model(model, cx), + thinking_allowed: true, + }; - if !retry_scheduled { - thread.cancel_last_completion(window, cx); - } - } - } + log::debug!("Completion request built successfully"); + Ok(request) + } - if !retry_scheduled { - cx.emit(ThreadEvent::Stopped(result.map_err(Arc::new))); - } + fn enabled_tools( + &self, + profile: &AgentProfileSettings, + model: &Arc, + cx: &App, + ) -> BTreeMap> { + fn truncate(tool_name: &SharedString) -> SharedString { + if tool_name.len() > MAX_TOOL_NAME_LENGTH { + let mut truncated = tool_name.to_string(); + truncated.truncate(MAX_TOOL_NAME_LENGTH); + truncated.into() + } else { + tool_name.clone() + } + } - if let Some((request_callback, (request, response_events))) = thread - .request_callback - .as_mut() - .zip(request_callback_parameters.as_ref()) - { - request_callback(request, response_events); + let mut tools = self + .tools + .iter() + .filter_map(|(tool_name, tool)| { + if tool.supported_provider(&model.provider_id()) + && profile.is_tool_enabled(tool_name) + { + Some((truncate(tool_name), tool.clone())) + } else { + None + } + }) + .collect::>(); + + let mut context_server_tools = Vec::new(); + let mut seen_tools = tools.keys().cloned().collect::>(); + let mut duplicate_tool_names = HashSet::default(); + for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { + for (tool_name, tool) in server_tools { + if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { + let tool_name = truncate(tool_name); + if !seen_tools.insert(tool_name.clone()) { + duplicate_tool_names.insert(tool_name.clone()); } + context_server_tools.push((server_id.clone(), tool_name, tool.clone())); + } + } + } - if let Ok(initial_usage) = initial_token_usage { - let usage = thread.cumulative_token_usage - initial_usage; - - telemetry::event!( - "Assistant Thread Completion", - thread_id = thread.id().to_string(), - prompt_id = prompt_id, - model = model.telemetry_id(), - model_provider = model.provider_id().to_string(), - input_tokens = usage.input_tokens, - output_tokens = usage.output_tokens, - cache_creation_input_tokens = usage.cache_creation_input_tokens, - cache_read_input_tokens = usage.cache_read_input_tokens, - ); - } - }) - .ok(); - }); + // When there are duplicate tool names, disambiguate by prefixing them + // with the server ID. In the rare case there isn't enough space for the + // disambiguated tool name, keep only the last tool with this name. + for (server_id, tool_name, tool) in context_server_tools { + if duplicate_tool_names.contains(&tool_name) { + let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); + if available >= 2 { + let mut disambiguated = server_id.0.to_string(); + disambiguated.truncate(available - 1); + disambiguated.push('_'); + disambiguated.push_str(&tool_name); + tools.insert(disambiguated.into(), tool.clone()); + } else { + tools.insert(tool_name, tool.clone()); + } + } else { + tools.insert(tool_name, tool.clone()); + } + } - self.pending_completions.push(PendingCompletion { - id: pending_completion_id, - queue_state: QueueState::Sending, - _task: task, - }); + tools } - pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { - println!("No thread summary model"); - return; - }; - - if !model.provider.is_authenticated(cx) { - return; - } + fn tool(&self, name: &str) -> Option> { + self.running_turn.as_ref()?.tools.get(name).cloned() + } - let request = self.to_summarize_request( - &model.model, - CompletionIntent::ThreadSummarization, - SUMMARIZE_THREAD_PROMPT.into(), - cx, + fn build_request_messages(&self, cx: &App) -> Vec { + log::trace!( + "Building request messages from {} thread messages", + self.messages.len() ); - self.summary = ThreadSummary::Generating; - - self.pending_summary = cx.spawn(async move |this, cx| { - let result = async { - let mut messages = model.model.stream_completion(request, cx).await?; + let system_prompt = SystemPromptTemplate { + project: self.project_context.read(cx), + available_tools: self.tools.keys().cloned().collect(), + } + .render(&self.templates) + .context("failed to build system prompt") + .expect("Invalid template"); + let mut messages = vec![LanguageModelRequestMessage { + role: Role::System, + content: vec![system_prompt.into()], + cache: false, + }]; + for message in &self.messages { + messages.extend(message.to_request()); + } - let mut new_summary = String::new(); - while let Some(event) = messages.next().await { - let Ok(event) = event else { - continue; - }; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount as u32, limit, cx); - })?; - continue; - } - _ => continue, - }; + if let Some(last_message) = messages.last_mut() { + last_message.cache = true; + } - let mut lines = text.lines(); - new_summary.extend(lines.next()); + if let Some(message) = self.pending_message.as_ref() { + messages.extend(message.to_request()); + } - // Stop if the LLM generated multiple lines. - if lines.next().is_some() { - break; - } - } + messages + } - anyhow::Ok(new_summary) + pub fn to_markdown(&self) -> String { + let mut markdown = String::new(); + for (ix, message) in self.messages.iter().enumerate() { + if ix > 0 { + markdown.push('\n'); } - .await; + markdown.push_str(&message.to_markdown()); + } - this.update(cx, |this, cx| { - match result { - Ok(new_summary) => { - if new_summary.is_empty() { - this.summary = ThreadSummary::Error; - } else { - this.summary = ThreadSummary::Ready(new_summary.into()); - } - } - Err(err) => { - this.summary = ThreadSummary::Error; - log::error!("Failed to generate thread summary: {}", err); - } - } - cx.emit(ThreadEvent::SummaryGenerated); - }) - .log_err()?; + if let Some(message) = self.pending_message.as_ref() { + markdown.push('\n'); + markdown.push_str(&message.to_markdown()); + } - Some(()) - }); + markdown } - fn get_retry_strategy(error: &LanguageModelCompletionError) -> Option { + fn advance_prompt_id(&mut self) { + self.prompt_id = PromptId::new(); + } + + fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option { use LanguageModelCompletionError::*; + use http_client::StatusCode; // General strategy here: // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all. @@ -2205,8 +2119,8 @@ impl Thread { }) } Other(err) - if err.is::() - || err.is::() => + if err.is::() + || err.is::() => { // Retrying won't help for Payment Required or Model Request Limit errors (where // the user must upgrade to usage-based billing to get more requests, or else wait @@ -2220,3166 +2134,556 @@ impl Thread { }), } } +} - fn handle_retryable_error_with_delay( - &mut self, - error: &LanguageModelCompletionError, - strategy: Option, - model: Arc, - intent: CompletionIntent, - window: Option, - cx: &mut Context, - ) -> bool { - // Store context for the Retry button - self.last_error_context = Some((model.clone(), intent)); - - // Only auto-retry if Burn Mode is enabled - if self.completion_mode != CompletionMode::Burn { - // Show error with retry options - cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError { - message: format!( - "{}\n\nTo automatically retry when similar errors happen, enable Burn Mode.", - error - ) - .into(), - can_enable_burn_mode: true, - })); - return false; - } +struct RunningTurn { + /// Holds the task that handles agent interaction until the end of the turn. + /// Survives across multiple requests as the model performs tool calls and + /// we run tools, report their results. + _task: Task<()>, + /// The current event stream for the running turn. Used to report a final + /// cancellation event if we cancel the turn. + event_stream: ThreadEventStream, + /// The tools that were enabled for this turn. + tools: BTreeMap>, +} - let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else { - return false; - }; +impl RunningTurn { + fn cancel(self) { + log::debug!("Cancelling in progress turn"); + self.event_stream.send_canceled(); + } +} - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; +pub struct TokenUsageUpdated(pub Option); - let retry_state = self.retry_state.get_or_insert(RetryState { - attempt: 0, - max_attempts, - intent, - }); +impl EventEmitter for Thread {} - retry_state.attempt += 1; - let attempt = retry_state.attempt; - let max_attempts = retry_state.max_attempts; - let intent = retry_state.intent; +pub struct TitleUpdated; - if attempt <= max_attempts { - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; +impl EventEmitter for Thread {} - // Add a transient message to inform the user - let delay_secs = delay.as_secs(); - let retry_message = if max_attempts == 1 { - format!("{error}. Retrying in {delay_secs} seconds...") - } else { - format!( - "{error}. Retrying (attempt {attempt} of {max_attempts}) \ - in {delay_secs} seconds..." - ) - }; - log::warn!( - "Retrying completion request (attempt {attempt} of {max_attempts}) \ - in {delay_secs} seconds: {error:?}", - ); +pub trait AgentTool +where + Self: 'static + Sized, +{ + type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; + type Output: for<'de> Deserialize<'de> + Serialize + Into; - // Add a UI-only message instead of a regular message - let id = self.next_message_id.post_inc(); - self.messages.push(Message { - id, - role: Role::System, - segments: vec![MessageSegment::Text(retry_message)], - loaded_context: LoadedContext::default(), - creases: Vec::new(), - is_hidden: false, - ui_only: true, - }); - cx.emit(ThreadEvent::MessageAdded(id)); + fn name() -> &'static str; - // Schedule the retry - let thread_handle = cx.entity().downgrade(); + fn description() -> SharedString { + let schema = schemars::schema_for!(Self::Input); + SharedString::new( + schema + .get("description") + .and_then(|description| description.as_str()) + .unwrap_or_default(), + ) + } - cx.spawn(async move |_thread, cx| { - cx.background_executor().timer(delay).await; + fn kind() -> acp::ToolKind; - thread_handle - .update(cx, |thread, cx| { - // Retry the completion - thread.send_to_model(model, intent, window, cx); - }) - .log_err(); - }) - .detach(); + /// The initial tool title to display. Can be updated during the tool run. + fn initial_title( + &self, + input: Result, + cx: &mut App, + ) -> SharedString; - true - } else { - // Max retries exceeded - self.retry_state = None; + /// Returns the JSON schema that describes the tool's input. + fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema { + crate::tool_schema::root_schema_for::(format) + } - // Stop generating since we're giving up on retrying. - self.pending_completions.clear(); + /// Some tools rely on a provider for the underlying billing or other reasons. + /// Allow the tool to check if they are compatible, or should be filtered out. + fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { + true + } - // Show error alongside a Retry button, but no - // Enable Burn Mode button (since it's already enabled) - cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError { - message: format!("Failed after retrying: {}", error).into(), - can_enable_burn_mode: false, - })); + /// Runs the tool with the provided input. + fn run( + self: Arc, + input: Self::Input, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task>; - false - } + /// Emits events for a previous execution of the tool. + fn replay( + &self, + _input: Self::Input, + _output: Self::Output, + _event_stream: ToolCallEventStream, + _cx: &mut App, + ) -> Result<()> { + Ok(()) } - pub fn start_generating_detailed_summary_if_needed( - &mut self, - thread_store: WeakEntity, - cx: &mut Context, - ) { - let Some(last_message_id) = self.messages.last().map(|message| message.id) else { - return; - }; - - match &*self.detailed_summary_rx.borrow() { - DetailedSummaryState::Generating { message_id, .. } - | DetailedSummaryState::Generated { message_id, .. } - if *message_id == last_message_id => - { - // Already up-to-date - return; - } - _ => {} - } - - let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model() - else { - return; - }; + fn erase(self) -> Arc { + Arc::new(Erased(Arc::new(self))) + } +} - if !provider.is_authenticated(cx) { - return; - } +pub struct Erased(T); - let request = self.to_summarize_request( - &model, - CompletionIntent::ThreadContextSummarization, - SUMMARIZE_THREAD_DETAILED_PROMPT.into(), - cx, - ); +pub struct AgentToolOutput { + pub llm_output: LanguageModelToolResultContent, + pub raw_output: serde_json::Value, +} - *self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating { - message_id: last_message_id, - }; +pub trait AnyAgentTool { + fn name(&self) -> SharedString; + fn description(&self) -> SharedString; + fn kind(&self) -> acp::ToolKind; + fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString; + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; + fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { + true + } + fn run( + self: Arc, + input: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task>; + fn replay( + &self, + input: serde_json::Value, + output: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()>; +} - // Replace the detailed summarization task if there is one, cancelling it. It would probably - // be better to allow the old task to complete, but this would require logic for choosing - // which result to prefer (the old task could complete after the new one, resulting in a - // stale summary). - self.detailed_summary_task = cx.spawn(async move |thread, cx| { - let stream = model.stream_completion_text(request, cx); - let Some(mut messages) = stream.await.log_err() else { - thread - .update(cx, |thread, _cx| { - *thread.detailed_summary_tx.borrow_mut() = - DetailedSummaryState::NotGenerated; - }) - .ok()?; - return None; - }; +impl AnyAgentTool for Erased> +where + T: AgentTool, +{ + fn name(&self) -> SharedString { + T::name().into() + } - let mut new_detailed_summary = String::new(); + fn description(&self) -> SharedString { + T::description() + } - while let Some(chunk) = messages.stream.next().await { - if let Some(chunk) = chunk.log_err() { - new_detailed_summary.push_str(&chunk); - } - } + fn kind(&self) -> agent_client_protocol::ToolKind { + T::kind() + } - thread - .update(cx, |thread, _cx| { - *thread.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generated { - text: new_detailed_summary.into(), - message_id: last_message_id, - }; - }) - .ok()?; + fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { + let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); + self.0.initial_title(parsed_input, _cx) + } - // Save thread so its summary can be reused later - if let Some(thread) = thread.upgrade() - && let Ok(Ok(save_task)) = cx.update(|cx| { - thread_store - .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx)) - }) - { - save_task.await.log_err(); - } + fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { + let mut json = serde_json::to_value(T::input_schema(format))?; + crate::tool_schema::adapt_schema_to_format(&mut json, format)?; + Ok(json) + } - Some(()) - }); + fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { + self.0.supported_provider(provider) } - pub async fn wait_for_detailed_summary_or_text( - this: &Entity, - cx: &mut AsyncApp, - ) -> Option { - let mut detailed_summary_rx = this - .read_with(cx, |this, _cx| this.detailed_summary_rx.clone()) - .ok()?; - loop { - match detailed_summary_rx.recv().await? { - DetailedSummaryState::Generating { .. } => {} - DetailedSummaryState::NotGenerated => { - return this.read_with(cx, |this, _cx| this.text().into()).ok(); - } - DetailedSummaryState::Generated { text, .. } => return Some(text), - } - } + fn run( + self: Arc, + input: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Task> { + cx.spawn(async move |cx| { + let input = serde_json::from_value(input)?; + let output = cx + .update(|cx| self.0.clone().run(input, event_stream, cx))? + .await?; + let raw_output = serde_json::to_value(&output)?; + Ok(AgentToolOutput { + llm_output: output.into(), + raw_output, + }) + }) } - pub fn latest_detailed_summary_or_text(&self) -> SharedString { - self.detailed_summary_rx - .borrow() - .text() - .unwrap_or_else(|| self.text().into()) + fn replay( + &self, + input: serde_json::Value, + output: serde_json::Value, + event_stream: ToolCallEventStream, + cx: &mut App, + ) -> Result<()> { + let input = serde_json::from_value(input)?; + let output = serde_json::from_value(output)?; + self.0.replay(input, output, event_stream, cx) } +} - pub fn is_generating_detailed_summary(&self) -> bool { - matches!( - &*self.detailed_summary_rx.borrow(), - DetailedSummaryState::Generating { .. } - ) +#[derive(Clone)] +struct ThreadEventStream(mpsc::UnboundedSender>); + +impl ThreadEventStream { + fn send_user_message(&self, message: &UserMessage) { + self.0 + .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) + .ok(); } - pub fn use_pending_tools( - &mut self, - window: Option, - model: Arc, - cx: &mut Context, - ) -> Vec { - let request = - Arc::new(self.to_completion_request(model.clone(), CompletionIntent::ToolResults, cx)); - let pending_tool_uses = self - .tool_use - .pending_tool_uses() - .into_iter() - .filter(|tool_use| tool_use.status.is_idle()) - .cloned() - .collect::>(); - - for tool_use in pending_tool_uses.iter() { - self.use_pending_tool(tool_use.clone(), request.clone(), model.clone(), window, cx); - } + fn send_text(&self, text: &str) { + self.0 + .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string()))) + .ok(); + } - pending_tool_uses + fn send_thinking(&self, text: &str) { + self.0 + .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string()))) + .ok(); } - fn use_pending_tool( - &mut self, - tool_use: PendingToolUse, - request: Arc, - model: Arc, - window: Option, - cx: &mut Context, + fn send_tool_call( + &self, + id: &LanguageModelToolUseId, + title: SharedString, + kind: acp::ToolKind, + input: serde_json::Value, ) { - let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) else { - return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx); - }; - - if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) { - return self.handle_hallucinated_tool_use(tool_use.id, tool_use.name, window, cx); - } + self.0 + .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( + id, + title.to_string(), + kind, + input, + )))) + .ok(); + } - if tool.needs_confirmation(&tool_use.input, &self.project, cx) - && !AgentSettings::get_global(cx).always_allow_tool_actions - { - self.tool_use.confirm_tool_use( - tool_use.id, - tool_use.ui_text, - tool_use.input, - request, - tool, - ); - cx.emit(ThreadEvent::ToolConfirmationNeeded); - } else { - self.run_tool( - tool_use.id, - tool_use.ui_text, - tool_use.input, - request, - tool, - model, - window, - cx, - ); + fn initial_tool_call( + id: &LanguageModelToolUseId, + title: String, + kind: acp::ToolKind, + input: serde_json::Value, + ) -> acp::ToolCall { + acp::ToolCall { + meta: None, + id: acp::ToolCallId(id.to_string().into()), + title, + kind, + status: acp::ToolCallStatus::Pending, + content: vec![], + locations: vec![], + raw_input: Some(input), + raw_output: None, } } - pub fn handle_hallucinated_tool_use( - &mut self, - tool_use_id: LanguageModelToolUseId, - hallucinated_tool_name: Arc, - window: Option, - cx: &mut Context, + fn update_tool_call_fields( + &self, + tool_use_id: &LanguageModelToolUseId, + fields: acp::ToolCallUpdateFields, ) { - let available_tools = self.profile.enabled_tools(cx); - - let tool_list = available_tools - .iter() - .map(|(name, tool)| format!("- {}: {}", name, tool.description())) - .collect::>() - .join("\n"); - - let error_message = format!( - "The tool '{}' doesn't exist or is not enabled. Available tools:\n{}", - hallucinated_tool_name, tool_list - ); + self.0 + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( + acp::ToolCallUpdate { + meta: None, + id: acp::ToolCallId(tool_use_id.to_string().into()), + fields, + } + .into(), + ))) + .ok(); + } - let pending_tool_use = self.tool_use.insert_tool_output( - tool_use_id.clone(), - hallucinated_tool_name, - Err(anyhow!("Missing tool call: {error_message}")), - self.configured_model.as_ref(), - self.completion_mode, - ); + fn send_retry(&self, status: acp_thread::RetryStatus) { + self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); + } - cx.emit(ThreadEvent::MissingToolUse { - tool_use_id: tool_use_id.clone(), - ui_text: error_message.into(), - }); + fn send_stop(&self, reason: acp::StopReason) { + self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); + } - self.tool_finished(tool_use_id, pending_tool_use, false, window, cx); + fn send_canceled(&self) { + self.0 + .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) + .ok(); } - pub fn receive_invalid_tool_json( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - invalid_json: Arc, - error: String, - window: Option, - cx: &mut Context, - ) { - log::error!("The model returned invalid input JSON: {invalid_json}"); + fn send_error(&self, error: impl Into) { + self.0.unbounded_send(Err(error.into())).ok(); + } +} - let pending_tool_use = self.tool_use.insert_tool_output( - tool_use_id.clone(), - tool_name, - Err(anyhow!("Error parsing input JSON: {error}")), - self.configured_model.as_ref(), - self.completion_mode, - ); - let ui_text = if let Some(pending_tool_use) = &pending_tool_use { - pending_tool_use.ui_text.clone() - } else { - log::error!( - "There was no pending tool use for tool use {tool_use_id}, even though it finished (with invalid input JSON)." - ); - format!("Unknown tool {}", tool_use_id).into() - }; +#[derive(Clone)] +pub struct ToolCallEventStream { + tool_use_id: LanguageModelToolUseId, + stream: ThreadEventStream, + fs: Option>, +} - cx.emit(ThreadEvent::InvalidToolInput { - tool_use_id: tool_use_id.clone(), - ui_text, - invalid_input_json: invalid_json, - }); +impl ToolCallEventStream { + #[cfg(any(test, feature = "test-support"))] + pub fn test() -> (Self, ToolCallEventStreamReceiver) { + let (events_tx, events_rx) = mpsc::unbounded::>(); - self.tool_finished(tool_use_id, pending_tool_use, false, window, cx); - } + let stream = ToolCallEventStream::new("test_id".into(), ThreadEventStream(events_tx), None); - pub fn run_tool( - &mut self, - tool_use_id: LanguageModelToolUseId, - ui_text: impl Into, - input: serde_json::Value, - request: Arc, - tool: Arc, - model: Arc, - window: Option, - cx: &mut Context, - ) { - let task = - self.spawn_tool_use(tool_use_id.clone(), request, input, tool, model, window, cx); - self.tool_use - .run_pending_tool(tool_use_id, ui_text.into(), task); + (stream, ToolCallEventStreamReceiver(events_rx)) } - fn spawn_tool_use( - &mut self, + fn new( tool_use_id: LanguageModelToolUseId, - request: Arc, - input: serde_json::Value, - tool: Arc, - model: Arc, - window: Option, - cx: &mut Context, - ) -> Task<()> { - let tool_name: Arc = tool.name().into(); - - let tool_result = tool.run( - input, - request, - self.project.clone(), - self.action_log.clone(), - model, - window, - cx, - ); - - // Store the card separately if it exists - if let Some(card) = tool_result.card.clone() { - self.tool_use - .insert_tool_result_card(tool_use_id.clone(), card); + stream: ThreadEventStream, + fs: Option>, + ) -> Self { + Self { + tool_use_id, + stream, + fs, } - - cx.spawn({ - async move |thread: WeakEntity, cx| { - let output = tool_result.output.await; - - thread - .update(cx, |thread, cx| { - let pending_tool_use = thread.tool_use.insert_tool_output( - tool_use_id.clone(), - tool_name, - output, - thread.configured_model.as_ref(), - thread.completion_mode, - ); - thread.tool_finished(tool_use_id, pending_tool_use, false, window, cx); - }) - .ok(); - } - }) } - fn tool_finished( - &mut self, - tool_use_id: LanguageModelToolUseId, - pending_tool_use: Option, - canceled: bool, - window: Option, - cx: &mut Context, - ) { - if self.all_tools_finished() - && let Some(ConfiguredModel { model, .. }) = self.configured_model.as_ref() - && !canceled - { - self.send_to_model(model.clone(), CompletionIntent::ToolResults, window, cx); - } + pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) { + self.stream + .update_tool_call_fields(&self.tool_use_id, fields); + } - cx.emit(ThreadEvent::ToolFinished { - tool_use_id, - pending_tool_use, - }); + pub fn update_diff(&self, diff: Entity) { + self.stream + .0 + .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( + acp_thread::ToolCallUpdateDiff { + id: acp::ToolCallId(self.tool_use_id.to_string().into()), + diff, + } + .into(), + ))) + .ok(); } - /// Cancels the last pending completion, if there are any pending. - /// - /// Returns whether a completion was canceled. - pub fn cancel_last_completion( - &mut self, - window: Option, - cx: &mut Context, - ) -> bool { - let mut canceled = self.pending_completions.pop().is_some() || self.retry_state.is_some(); - - self.retry_state = None; - - for pending_tool_use in self.tool_use.cancel_pending() { - canceled = true; - self.tool_finished( - pending_tool_use.id.clone(), - Some(pending_tool_use), - true, - window, - cx, - ); + pub fn authorize(&self, title: impl Into, cx: &mut App) -> Task> { + if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { + return Task::ready(Ok(())); } - if canceled { - cx.emit(ThreadEvent::CompletionCanceled); + let (response_tx, response_rx) = oneshot::channel(); + self.stream + .0 + .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization( + ToolCallAuthorization { + tool_call: acp::ToolCallUpdate { + meta: None, + id: acp::ToolCallId(self.tool_use_id.to_string().into()), + fields: acp::ToolCallUpdateFields { + title: Some(title.into()), + ..Default::default() + }, + }, + options: vec![ + acp::PermissionOption { + id: acp::PermissionOptionId("always_allow".into()), + name: "Always Allow".into(), + kind: acp::PermissionOptionKind::AllowAlways, + meta: None, + }, + acp::PermissionOption { + id: acp::PermissionOptionId("allow".into()), + name: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + meta: None, + }, + acp::PermissionOption { + id: acp::PermissionOptionId("deny".into()), + name: "Deny".into(), + kind: acp::PermissionOptionKind::RejectOnce, + meta: None, + }, + ], + response: response_tx, + }, + ))) + .ok(); + let fs = self.fs.clone(); + cx.spawn(async move |cx| match response_rx.await?.0.as_ref() { + "always_allow" => { + if let Some(fs) = fs.clone() { + cx.update(|cx| { + update_settings_file(fs, cx, |settings, _| { + settings + .agent + .get_or_insert_default() + .set_always_allow_tool_actions(true); + }); + })?; + } - // When canceled, we always want to insert the checkpoint. - // (We skip over finalize_pending_checkpoint, because it - // would conclude we didn't have anything to insert here.) - if let Some(checkpoint) = self.pending_checkpoint.take() { - self.insert_checkpoint(checkpoint, cx); + Ok(()) } + "allow" => Ok(()), + _ => Err(anyhow!("Permission to run tool denied by user")), + }) + } +} + +#[cfg(any(test, feature = "test-support"))] +pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); + +#[cfg(any(test, feature = "test-support"))] +impl ToolCallEventStreamReceiver { + pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event { + auth } else { - self.finalize_pending_checkpoint(cx); + panic!("Expected ToolCallAuthorization but got: {:?}", event); } - - canceled } - /// Signals that any in-progress editing should be canceled. - /// - /// This method is used to notify listeners (like ActiveThread) that - /// they should cancel any editing operations. - pub fn cancel_editing(&mut self, cx: &mut Context) { - cx.emit(ThreadEvent::CancelEditing); + pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( + update, + )))) = event + { + update.fields + } else { + panic!("Expected update fields but got: {:?}", event); + } } - pub fn message_feedback(&self, message_id: MessageId) -> Option { - self.message_feedback.get(&message_id).copied() + pub async fn expect_diff(&mut self) -> Entity { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( + update, + )))) = event + { + update.diff + } else { + panic!("Expected diff but got: {:?}", event); + } } - pub fn report_message_feedback( - &mut self, - message_id: MessageId, - feedback: ThreadFeedback, - cx: &mut Context, - ) -> Task> { - if self.message_feedback.get(&message_id) == Some(&feedback) { - return Task::ready(Ok(())); + pub async fn expect_terminal(&mut self) -> Entity { + let event = self.0.next().await; + if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( + update, + )))) = event + { + update.terminal + } else { + panic!("Expected terminal but got: {:?}", event); } - - let final_project_snapshot = Self::project_snapshot(self.project.clone(), cx); - let serialized_thread = self.serialize(cx); - let thread_id = self.id().clone(); - let client = self.project.read(cx).client(); - - let enabled_tool_names: Vec = self - .profile - .enabled_tools(cx) - .iter() - .map(|(name, _)| name.clone().into()) - .collect(); - - self.message_feedback.insert(message_id, feedback); - - cx.notify(); - - let message_content = self - .message(message_id) - .map(|msg| msg.to_message_content()) - .unwrap_or_default(); - - cx.background_spawn(async move { - let final_project_snapshot = final_project_snapshot.await; - let serialized_thread = serialized_thread.await?; - let thread_data = - serde_json::to_value(serialized_thread).unwrap_or_else(|_| serde_json::Value::Null); - - let rating = match feedback { - ThreadFeedback::Positive => "positive", - ThreadFeedback::Negative => "negative", - }; - telemetry::event!( - "Assistant Thread Rated", - rating, - thread_id, - enabled_tool_names, - message_id = message_id.0, - message_content, - thread_data, - final_project_snapshot - ); - client.telemetry().flush_events().await; - - Ok(()) - }) } +} - /// Create a snapshot of the current project state including git information and unsaved buffers. - fn project_snapshot( - project: Entity, - cx: &mut Context, - ) -> Task> { - let git_store = project.read(cx).git_store().clone(); - let worktree_snapshots: Vec<_> = project - .read(cx) - .visible_worktrees(cx) - .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx)) - .collect(); +#[cfg(any(test, feature = "test-support"))] +impl std::ops::Deref for ToolCallEventStreamReceiver { + type Target = mpsc::UnboundedReceiver>; - cx.spawn(async move |_, _| { - let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; + fn deref(&self) -> &Self::Target { + &self.0 + } +} - Arc::new(ProjectSnapshot { - worktree_snapshots, - timestamp: Utc::now(), - }) - }) +#[cfg(any(test, feature = "test-support"))] +impl std::ops::DerefMut for ToolCallEventStreamReceiver { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 } +} - fn worktree_snapshot( - worktree: Entity, - git_store: Entity, - cx: &App, - ) -> Task { - cx.spawn(async move |cx| { - // Get worktree path and snapshot - let worktree_info = cx.update(|app_cx| { - let worktree = worktree.read(app_cx); - let path = worktree.abs_path().to_string_lossy().into_owned(); - let snapshot = worktree.snapshot(); - (path, snapshot) - }); +impl From<&str> for UserMessageContent { + fn from(text: &str) -> Self { + Self::Text(text.into()) + } +} - let Ok((worktree_path, _snapshot)) = worktree_info else { - return WorktreeSnapshot { - worktree_path: String::new(), - git_state: None, - }; - }; +impl From for UserMessageContent { + fn from(value: acp::ContentBlock) -> Self { + match value { + acp::ContentBlock::Text(text_content) => Self::Text(text_content.text), + acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)), + acp::ContentBlock::Audio(_) => { + // TODO + Self::Text("[audio]".to_string()) + } + acp::ContentBlock::ResourceLink(resource_link) => { + match MentionUri::parse(&resource_link.uri) { + Ok(uri) => Self::Mention { + uri, + content: String::new(), + }, + Err(err) => { + log::error!("Failed to parse mention link: {}", err); + Self::Text(format!("[{}]({})", resource_link.name, resource_link.uri)) + } + } + } + acp::ContentBlock::Resource(resource) => match resource.resource { + acp::EmbeddedResourceResource::TextResourceContents(resource) => { + match MentionUri::parse(&resource.uri) { + Ok(uri) => Self::Mention { + uri, + content: resource.text, + }, + Err(err) => { + log::error!("Failed to parse mention link: {}", err); + Self::Text( + MarkdownCodeBlock { + tag: &resource.uri, + text: &resource.text, + } + .to_string(), + ) + } + } + } + acp::EmbeddedResourceResource::BlobResourceContents(_) => { + // TODO + Self::Text("[blob]".to_string()) + } + }, + } + } +} - let git_state = git_store - .update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .find(|repo| { - repo.read(cx) - .abs_path_to_repo_path(&worktree.read(cx).abs_path()) - .is_some() - }) - .cloned() +impl From for acp::ContentBlock { + fn from(content: UserMessageContent) -> Self { + match content { + UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent { + text, + annotations: None, + meta: None, + }), + UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent { + data: image.source.to_string(), + mime_type: "image/png".to_string(), + meta: None, + annotations: None, + uri: None, + }), + UserMessageContent::Mention { uri, content } => { + acp::ContentBlock::Resource(acp::EmbeddedResource { + meta: None, + resource: acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents { + meta: None, + mime_type: None, + text: content, + uri: uri.to_uri().to_string(), + }, + ), + annotations: None, }) - .ok() - .flatten() - .map(|repo| { - repo.update(cx, |repo, _| { - let current_branch = - repo.branch.as_ref().map(|branch| branch.name().to_owned()); - repo.send_job(None, |state, _| async move { - let RepositoryState::Local { backend, .. } = state else { - return GitState { - remote_url: None, - head_sha: None, - current_branch, - diff: None, - }; - }; - - let remote_url = backend.remote_url("origin"); - let head_sha = backend.head_sha().await; - let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); - - GitState { - remote_url, - head_sha, - current_branch, - diff, - } - }) - }) - }); - - let git_state = match git_state { - Some(git_state) => match git_state.ok() { - Some(git_state) => git_state.await.ok(), - None => None, - }, - None => None, - }; - - WorktreeSnapshot { - worktree_path, - git_state, - } - }) - } - - pub fn to_markdown(&self, cx: &App) -> Result { - let mut markdown = Vec::new(); - - let summary = self.summary().or_default(); - writeln!(markdown, "# {summary}\n")?; - - for message in self.messages() { - writeln!( - markdown, - "## {role}\n", - role = match message.role { - Role::User => "User", - Role::Assistant => "Agent", - Role::System => "System", - } - )?; - - if !message.loaded_context.text.is_empty() { - writeln!(markdown, "{}", message.loaded_context.text)?; - } - - if !message.loaded_context.images.is_empty() { - writeln!( - markdown, - "\n{} images attached as context.\n", - message.loaded_context.images.len() - )?; - } - - for segment in &message.segments { - match segment { - MessageSegment::Text(text) => writeln!(markdown, "{}\n", text)?, - MessageSegment::Thinking { text, .. } => { - writeln!(markdown, "\n{}\n\n", text)? - } - MessageSegment::RedactedThinking(_) => {} - } - } - - for tool_use in self.tool_uses_for_message(message.id, cx) { - writeln!( - markdown, - "**Use Tool: {} ({})**", - tool_use.name, tool_use.id - )?; - writeln!(markdown, "```json")?; - writeln!( - markdown, - "{}", - serde_json::to_string_pretty(&tool_use.input)? - )?; - writeln!(markdown, "```")?; - } - - for tool_result in self.tool_results_for_message(message.id) { - write!(markdown, "\n**Tool Results: {}", tool_result.tool_use_id)?; - if tool_result.is_error { - write!(markdown, " (Error)")?; - } - - writeln!(markdown, "**\n")?; - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(markdown, "{text}")?; - } - LanguageModelToolResultContent::Image(image) => { - writeln!(markdown, "![Image](data:base64,{})", image.source)?; - } - } - - if let Some(output) = tool_result.output.as_ref() { - writeln!( - markdown, - "\n\nDebug Output:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output)? - )?; - } } } - - Ok(String::from_utf8_lossy(&markdown).to_string()) - } - - pub fn keep_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - cx: &mut Context, - ) { - self.action_log.update(cx, |action_log, cx| { - action_log.keep_edits_in_range(buffer, buffer_range, cx) - }); - } - - pub fn keep_all_edits(&mut self, cx: &mut Context) { - self.action_log - .update(cx, |action_log, cx| action_log.keep_all_edits(cx)); - } - - pub fn reject_edits_in_ranges( - &mut self, - buffer: Entity, - buffer_ranges: Vec>, - cx: &mut Context, - ) -> Task> { - self.action_log.update(cx, |action_log, cx| { - action_log.reject_edits_in_ranges(buffer, buffer_ranges, cx) - }) - } - - pub fn action_log(&self) -> &Entity { - &self.action_log - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn cumulative_token_usage(&self) -> TokenUsage { - self.cumulative_token_usage - } - - pub fn token_usage_up_to_message(&self, message_id: MessageId) -> TotalTokenUsage { - let Some(model) = self.configured_model.as_ref() else { - return TotalTokenUsage::default(); - }; - - let max = model - .model - .max_token_count_for_mode(self.completion_mode().into()); - - let index = self - .messages - .iter() - .position(|msg| msg.id == message_id) - .unwrap_or(0); - - if index == 0 { - return TotalTokenUsage { total: 0, max }; - } - - let token_usage = &self - .request_token_usage - .get(index - 1) - .cloned() - .unwrap_or_default(); - - TotalTokenUsage { - total: token_usage.total_tokens(), - max, - } - } - - pub fn total_token_usage(&self) -> Option { - let model = self.configured_model.as_ref()?; - - let max = model - .model - .max_token_count_for_mode(self.completion_mode().into()); - - if let Some(exceeded_error) = &self.exceeded_window_error - && model.model.id() == exceeded_error.model_id - { - return Some(TotalTokenUsage { - total: exceeded_error.token_count, - max, - }); - } - - let total = self - .token_usage_at_last_message() - .unwrap_or_default() - .total_tokens(); - - Some(TotalTokenUsage { total, max }) - } - - fn token_usage_at_last_message(&self) -> Option { - self.request_token_usage - .get(self.messages.len().saturating_sub(1)) - .or_else(|| self.request_token_usage.last()) - .cloned() - } - - fn update_token_usage_at_last_message(&mut self, token_usage: TokenUsage) { - let placeholder = self.token_usage_at_last_message().unwrap_or_default(); - self.request_token_usage - .resize(self.messages.len(), placeholder); - - if let Some(last) = self.request_token_usage.last_mut() { - *last = token_usage; - } - } - - fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context) { - self.project - .read(cx) - .user_store() - .update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); - } - - pub fn deny_tool_use( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - window: Option, - cx: &mut Context, - ) { - let err = Err(anyhow::anyhow!( - "Permission to run tool action denied by user" - )); - - self.tool_use.insert_tool_output( - tool_use_id.clone(), - tool_name, - err, - self.configured_model.as_ref(), - self.completion_mode, - ); - self.tool_finished(tool_use_id, None, true, window, cx); } } -#[derive(Debug, Clone, Error)] -pub enum ThreadError { - #[error("Payment required")] - PaymentRequired, - #[error("Model request limit reached")] - ModelRequestLimitReached { plan: Plan }, - #[error("Message {header}: {message}")] - Message { - header: SharedString, - message: SharedString, - }, - #[error("Retryable error: {message}")] - RetryableError { - message: SharedString, - can_enable_burn_mode: bool, - }, -} - -#[derive(Debug, Clone)] -pub enum ThreadEvent { - ShowError(ThreadError), - StreamedCompletion, - ReceivedTextChunk, - NewRequest, - StreamedAssistantText(MessageId, String), - StreamedAssistantThinking(MessageId, String), - StreamedToolUse { - tool_use_id: LanguageModelToolUseId, - ui_text: Arc, - input: serde_json::Value, - }, - MissingToolUse { - tool_use_id: LanguageModelToolUseId, - ui_text: Arc, - }, - InvalidToolInput { - tool_use_id: LanguageModelToolUseId, - ui_text: Arc, - invalid_input_json: Arc, - }, - Stopped(Result>), - MessageAdded(MessageId), - MessageEdited(MessageId), - MessageDeleted(MessageId), - SummaryGenerated, - SummaryChanged, - UsePendingTools { - tool_uses: Vec, - }, - ToolFinished { - #[allow(unused)] - tool_use_id: LanguageModelToolUseId, - /// The pending tool use that corresponds to this tool. - pending_tool_use: Option, - }, - CheckpointChanged, - ToolConfirmationNeeded, - ToolUseLimitReached, - CancelEditing, - CompletionCanceled, - ProfileChanged, -} - -impl EventEmitter for Thread {} - -struct PendingCompletion { - id: usize, - queue_state: QueueState, - _task: Task<()>, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - context::load_context, context_store::ContextStore, thread_store, thread_store::ThreadStore, - }; - - // Test-specific constants - const TEST_RATE_LIMIT_RETRY_SECS: u64 = 30; - use agent_settings::{AgentProfileId, AgentSettings}; - use assistant_tool::ToolRegistry; - use assistant_tools; - use fs::Fs; - use futures::StreamExt; - use futures::future::BoxFuture; - use futures::stream::BoxStream; - use gpui::TestAppContext; - use http_client; - use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}; - use language_model::{ - LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId, - LanguageModelProviderName, LanguageModelToolChoice, - }; - use parking_lot::Mutex; - use project::{FakeFs, Project}; - use prompt_store::PromptBuilder; - use serde_json::json; - use settings::{LanguageModelParameters, Settings, SettingsStore}; - use std::sync::Arc; - use std::time::Duration; - use util::path; - use workspace::Workspace; - - #[gpui::test] - async fn test_message_with_context(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (_workspace, _thread_store, thread, context_store, model) = - setup_test_environment(cx, project.clone()).await; - - add_file_to_context(&project, &context_store, "test/code.rs", cx) - .await - .unwrap(); - - let context = - context_store.read_with(cx, |store, _| store.context().next().cloned().unwrap()); - let loaded_context = cx - .update(|cx| load_context(vec![context], &project, &None, cx)) - .await; - - // Insert user message with context - let message_id = thread.update(cx, |thread, cx| { - thread.insert_user_message( - "Please explain this code", - loaded_context, - None, - Vec::new(), - cx, - ) - }); - - // Check content and context in message object - let message = thread.read_with(cx, |thread, _| thread.message(message_id).unwrap().clone()); - - // Use different path format strings based on platform for the test - #[cfg(windows)] - let path_part = r"test\code.rs"; - #[cfg(not(windows))] - let path_part = "test/code.rs"; - - let expected_context = format!( - r#" - -The following items were attached by the user. They are up-to-date and don't need to be re-read. - - -```rs {path_part} -fn main() {{ - println!("Hello, world!"); -}} -``` - - -"# - ); - - assert_eq!(message.role, Role::User); - assert_eq!(message.segments.len(), 1); - assert_eq!( - message.segments[0], - MessageSegment::Text("Please explain this code".to_string()) - ); - assert_eq!(message.loaded_context.text, expected_context); - - // Check message in request - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - - assert_eq!(request.messages.len(), 2); - let expected_full_message = format!("{}Please explain this code", expected_context); - assert_eq!(request.messages[1].string_contents(), expected_full_message); - } - - #[gpui::test] - async fn test_only_include_new_contexts(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({ - "file1.rs": "fn function1() {}\n", - "file2.rs": "fn function2() {}\n", - "file3.rs": "fn function3() {}\n", - "file4.rs": "fn function4() {}\n", - }), - ) - .await; - - let (_, _thread_store, thread, context_store, model) = - setup_test_environment(cx, project.clone()).await; - - // First message with context 1 - add_file_to_context(&project, &context_store, "test/file1.rs", cx) - .await - .unwrap(); - let new_contexts = context_store.update(cx, |store, cx| { - store.new_context_for_thread(thread.read(cx), None) - }); - assert_eq!(new_contexts.len(), 1); - let loaded_context = cx - .update(|cx| load_context(new_contexts, &project, &None, cx)) - .await; - let message1_id = thread.update(cx, |thread, cx| { - thread.insert_user_message("Message 1", loaded_context, None, Vec::new(), cx) - }); - - // Second message with contexts 1 and 2 (context 1 should be skipped as it's already included) - add_file_to_context(&project, &context_store, "test/file2.rs", cx) - .await - .unwrap(); - let new_contexts = context_store.update(cx, |store, cx| { - store.new_context_for_thread(thread.read(cx), None) - }); - assert_eq!(new_contexts.len(), 1); - let loaded_context = cx - .update(|cx| load_context(new_contexts, &project, &None, cx)) - .await; - let message2_id = thread.update(cx, |thread, cx| { - thread.insert_user_message("Message 2", loaded_context, None, Vec::new(), cx) - }); - - // Third message with all three contexts (contexts 1 and 2 should be skipped) - // - add_file_to_context(&project, &context_store, "test/file3.rs", cx) - .await - .unwrap(); - let new_contexts = context_store.update(cx, |store, cx| { - store.new_context_for_thread(thread.read(cx), None) - }); - assert_eq!(new_contexts.len(), 1); - let loaded_context = cx - .update(|cx| load_context(new_contexts, &project, &None, cx)) - .await; - let message3_id = thread.update(cx, |thread, cx| { - thread.insert_user_message("Message 3", loaded_context, None, Vec::new(), cx) - }); - - // Check what contexts are included in each message - let (message1, message2, message3) = thread.read_with(cx, |thread, _| { - ( - thread.message(message1_id).unwrap().clone(), - thread.message(message2_id).unwrap().clone(), - thread.message(message3_id).unwrap().clone(), - ) - }); - - // First message should include context 1 - assert!(message1.loaded_context.text.contains("file1.rs")); - - // Second message should include only context 2 (not 1) - assert!(!message2.loaded_context.text.contains("file1.rs")); - assert!(message2.loaded_context.text.contains("file2.rs")); - - // Third message should include only context 3 (not 1 or 2) - assert!(!message3.loaded_context.text.contains("file1.rs")); - assert!(!message3.loaded_context.text.contains("file2.rs")); - assert!(message3.loaded_context.text.contains("file3.rs")); - - // Check entire request to make sure all contexts are properly included - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - - // The request should contain all 3 messages - assert_eq!(request.messages.len(), 4); - - // Check that the contexts are properly formatted in each message - assert!(request.messages[1].string_contents().contains("file1.rs")); - assert!(!request.messages[1].string_contents().contains("file2.rs")); - assert!(!request.messages[1].string_contents().contains("file3.rs")); - - assert!(!request.messages[2].string_contents().contains("file1.rs")); - assert!(request.messages[2].string_contents().contains("file2.rs")); - assert!(!request.messages[2].string_contents().contains("file3.rs")); - - assert!(!request.messages[3].string_contents().contains("file1.rs")); - assert!(!request.messages[3].string_contents().contains("file2.rs")); - assert!(request.messages[3].string_contents().contains("file3.rs")); - - add_file_to_context(&project, &context_store, "test/file4.rs", cx) - .await - .unwrap(); - let new_contexts = context_store.update(cx, |store, cx| { - store.new_context_for_thread(thread.read(cx), Some(message2_id)) - }); - assert_eq!(new_contexts.len(), 3); - let loaded_context = cx - .update(|cx| load_context(new_contexts, &project, &None, cx)) - .await - .loaded_context; - - assert!(!loaded_context.text.contains("file1.rs")); - assert!(loaded_context.text.contains("file2.rs")); - assert!(loaded_context.text.contains("file3.rs")); - assert!(loaded_context.text.contains("file4.rs")); - - let new_contexts = context_store.update(cx, |store, cx| { - // Remove file4.rs - store.remove_context(&loaded_context.contexts[2].handle(), cx); - store.new_context_for_thread(thread.read(cx), Some(message2_id)) - }); - assert_eq!(new_contexts.len(), 2); - let loaded_context = cx - .update(|cx| load_context(new_contexts, &project, &None, cx)) - .await - .loaded_context; - - assert!(!loaded_context.text.contains("file1.rs")); - assert!(loaded_context.text.contains("file2.rs")); - assert!(loaded_context.text.contains("file3.rs")); - assert!(!loaded_context.text.contains("file4.rs")); - - let new_contexts = context_store.update(cx, |store, cx| { - // Remove file3.rs - store.remove_context(&loaded_context.contexts[1].handle(), cx); - store.new_context_for_thread(thread.read(cx), Some(message2_id)) - }); - assert_eq!(new_contexts.len(), 1); - let loaded_context = cx - .update(|cx| load_context(new_contexts, &project, &None, cx)) - .await - .loaded_context; - - assert!(!loaded_context.text.contains("file1.rs")); - assert!(loaded_context.text.contains("file2.rs")); - assert!(!loaded_context.text.contains("file3.rs")); - assert!(!loaded_context.text.contains("file4.rs")); - } - - #[gpui::test] - async fn test_message_without_files(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (_, _thread_store, thread, _context_store, model) = - setup_test_environment(cx, project.clone()).await; - - // Insert user message without any context (empty context vector) - let message_id = thread.update(cx, |thread, cx| { - thread.insert_user_message( - "What is the best way to learn Rust?", - ContextLoadResult::default(), - None, - Vec::new(), - cx, - ) - }); - - // Check content and context in message object - let message = thread.read_with(cx, |thread, _| thread.message(message_id).unwrap().clone()); - - // Context should be empty when no files are included - assert_eq!(message.role, Role::User); - assert_eq!(message.segments.len(), 1); - assert_eq!( - message.segments[0], - MessageSegment::Text("What is the best way to learn Rust?".to_string()) - ); - assert_eq!(message.loaded_context.text, ""); - - // Check message in request - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - - assert_eq!(request.messages.len(), 2); - assert_eq!( - request.messages[1].string_contents(), - "What is the best way to learn Rust?" - ); - - // Add second message, also without context - let message2_id = thread.update(cx, |thread, cx| { - thread.insert_user_message( - "Are there any good books?", - ContextLoadResult::default(), - None, - Vec::new(), - cx, - ) - }); - - let message2 = - thread.read_with(cx, |thread, _| thread.message(message2_id).unwrap().clone()); - assert_eq!(message2.loaded_context.text, ""); - - // Check that both messages appear in the request - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - - assert_eq!(request.messages.len(), 3); - assert_eq!( - request.messages[1].string_contents(), - "What is the best way to learn Rust?" - ); - assert_eq!( - request.messages[2].string_contents(), - "Are there any good books?" - ); - } - - #[gpui::test] - #[ignore] // turn this test on when project_notifications tool is re-enabled - async fn test_stale_buffer_notification(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (_workspace, _thread_store, thread, context_store, model) = - setup_test_environment(cx, project.clone()).await; - - // Add a buffer to the context. This will be a tracked buffer - let buffer = add_file_to_context(&project, &context_store, "test/code.rs", cx) - .await - .unwrap(); - - let context = context_store - .read_with(cx, |store, _| store.context().next().cloned()) - .unwrap(); - let loaded_context = cx - .update(|cx| load_context(vec![context], &project, &None, cx)) - .await; - - // Insert user message and assistant response - thread.update(cx, |thread, cx| { - thread.insert_user_message("Explain this code", loaded_context, None, Vec::new(), cx); - thread.insert_assistant_message( - vec![MessageSegment::Text("This code prints 42.".into())], - cx, - ); - }); - cx.run_until_parked(); - - // We shouldn't have a stale buffer notification yet - let notifications = thread.read_with(cx, |thread, _| { - find_tool_uses(thread, "project_notifications") - }); - assert!( - notifications.is_empty(), - "Should not have stale buffer notification before buffer is modified" - ); - - // Modify the buffer - buffer.update(cx, |buffer, cx| { - buffer.edit( - [(1..1, "\n println!(\"Added a new line\");\n")], - None, - cx, - ); - }); - - // Insert another user message - thread.update(cx, |thread, cx| { - thread.insert_user_message( - "What does the code do now?", - ContextLoadResult::default(), - None, - Vec::new(), - cx, - ) - }); - cx.run_until_parked(); - - // Check for the stale buffer warning - thread.update(cx, |thread, cx| { - thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) - }); - cx.run_until_parked(); - - let notifications = thread.read_with(cx, |thread, _cx| { - find_tool_uses(thread, "project_notifications") - }); - - let [notification] = notifications.as_slice() else { - panic!("Should have a `project_notifications` tool use"); - }; - - let Some(notification_content) = notification.content.to_str() else { - panic!("`project_notifications` should return text"); - }; - - assert!(notification_content.contains("These files have changed since the last read:")); - assert!(notification_content.contains("code.rs")); - - // Insert another user message and flush notifications again - thread.update(cx, |thread, cx| { - thread.insert_user_message( - "Can you tell me more?", - ContextLoadResult::default(), - None, - Vec::new(), - cx, - ) - }); - - thread.update(cx, |thread, cx| { - thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) - }); - cx.run_until_parked(); - - // There should be no new notifications (we already flushed one) - let notifications = thread.read_with(cx, |thread, _cx| { - find_tool_uses(thread, "project_notifications") - }); - - assert_eq!( - notifications.len(), - 1, - "Should still have only one notification after second flush - no duplicates" - ); - } - - fn find_tool_uses(thread: &Thread, tool_name: &str) -> Vec { - thread - .messages() - .flat_map(|message| { - thread - .tool_results_for_message(message.id) - .into_iter() - .filter(|result| result.tool_name == tool_name.into()) - .cloned() - .collect::>() - }) - .collect() - } - - #[gpui::test] - async fn test_storing_profile_setting_per_thread(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (_workspace, thread_store, thread, _context_store, _model) = - setup_test_environment(cx, project.clone()).await; - - // Check that we are starting with the default profile - let profile = cx.read(|cx| thread.read(cx).profile.clone()); - let tool_set = cx.read(|cx| thread_store.read(cx).tools()); - assert_eq!( - profile, - AgentProfile::new(AgentProfileId::default(), tool_set) - ); - } - - #[gpui::test] - async fn test_serializing_thread_profile(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (_workspace, thread_store, thread, _context_store, _model) = - setup_test_environment(cx, project.clone()).await; - - // Profile gets serialized with default values - let serialized = thread - .update(cx, |thread, cx| thread.serialize(cx)) - .await - .unwrap(); - - assert_eq!(serialized.profile, Some(AgentProfileId::default())); - - let deserialized = cx.update(|cx| { - thread.update(cx, |thread, cx| { - Thread::deserialize( - thread.id.clone(), - serialized, - thread.project.clone(), - thread.tools.clone(), - thread.prompt_builder.clone(), - thread.project_context.clone(), - None, - cx, - ) - }) - }); - let tool_set = cx.read(|cx| thread_store.read(cx).tools()); - - assert_eq!( - deserialized.profile, - AgentProfile::new(AgentProfileId::default(), tool_set) - ); - } - - #[gpui::test] - async fn test_temperature_setting(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project( - &fs, - cx, - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let (_workspace, _thread_store, thread, _context_store, model) = - setup_test_environment(cx, project.clone()).await; - - // Both model and provider - cx.update(|cx| { - AgentSettings::override_global( - AgentSettings { - model_parameters: vec![LanguageModelParameters { - provider: Some(model.provider_id().0.to_string().into()), - model: Some(model.id().0), - temperature: Some(0.66), - }], - ..AgentSettings::get_global(cx).clone() - }, - cx, - ); - }); - - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - assert_eq!(request.temperature, Some(0.66)); - - // Only model - cx.update(|cx| { - AgentSettings::override_global( - AgentSettings { - model_parameters: vec![LanguageModelParameters { - provider: None, - model: Some(model.id().0), - temperature: Some(0.66), - }], - ..AgentSettings::get_global(cx).clone() - }, - cx, - ); - }); - - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - assert_eq!(request.temperature, Some(0.66)); - - // Only provider - cx.update(|cx| { - AgentSettings::override_global( - AgentSettings { - model_parameters: vec![LanguageModelParameters { - provider: Some(model.provider_id().0.to_string().into()), - model: None, - temperature: Some(0.66), - }], - ..AgentSettings::get_global(cx).clone() - }, - cx, - ); - }); - - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - assert_eq!(request.temperature, Some(0.66)); - - // Same model name, different provider - cx.update(|cx| { - AgentSettings::override_global( - AgentSettings { - model_parameters: vec![LanguageModelParameters { - provider: Some("anthropic".into()), - model: Some(model.id().0), - temperature: Some(0.66), - }], - ..AgentSettings::get_global(cx).clone() - }, - cx, - ); - }); - - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - assert_eq!(request.temperature, None); - } - - #[gpui::test] - async fn test_thread_summary(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - - let (_, _thread_store, thread, _context_store, model) = - setup_test_environment(cx, project.clone()).await; - - // Initial state should be pending - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Pending)); - assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT); - }); - - // Manually setting the summary should not be allowed in this state - thread.update(cx, |thread, cx| { - thread.set_summary("This should not work", cx); - }); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Pending)); - }); - - // Send a message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx); - thread.send_to_model( - model.clone(), - CompletionIntent::ThreadSummarization, - None, - cx, - ); - }); - - let fake_model = model.as_fake(); - simulate_successful_response(fake_model, cx); - - // Should start generating summary when there are >= 2 messages - thread.read_with(cx, |thread, _| { - assert_eq!(*thread.summary(), ThreadSummary::Generating); - }); - - // Should not be able to set the summary while generating - thread.update(cx, |thread, cx| { - thread.set_summary("This should not work either", cx); - }); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Generating)); - assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT); - }); - - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Brief"); - fake_model.send_last_completion_stream_text_chunk(" Introduction"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - // Summary should be set - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Ready(_))); - assert_eq!(thread.summary().or_default(), "Brief Introduction"); - }); - - // Now we should be able to set a summary - thread.update(cx, |thread, cx| { - thread.set_summary("Brief Intro", cx); - }); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.summary().or_default(), "Brief Intro"); - }); - - // Test setting an empty summary (should default to DEFAULT) - thread.update(cx, |thread, cx| { - thread.set_summary("", cx); - }); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Ready(_))); - assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT); - }); - } - - #[gpui::test] - async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - - let (_, _thread_store, thread, _context_store, model) = - setup_test_environment(cx, project.clone()).await; - - test_summarize_error(&model, &thread, cx); - - // Now we should be able to set a summary - thread.update(cx, |thread, cx| { - thread.set_summary("Brief Intro", cx); - }); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Ready(_))); - assert_eq!(thread.summary().or_default(), "Brief Intro"); - }); - } - - #[gpui::test] - async fn test_thread_summary_error_retry(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - - let (_, _thread_store, thread, _context_store, model) = - setup_test_environment(cx, project.clone()).await; - - test_summarize_error(&model, &thread, cx); - - // Sending another message should not trigger another summarize request - thread.update(cx, |thread, cx| { - thread.insert_user_message( - "How are you?", - ContextLoadResult::default(), - None, - vec![], - cx, - ); - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - - let fake_model = model.as_fake(); - simulate_successful_response(fake_model, cx); - - thread.read_with(cx, |thread, _| { - // State is still Error, not Generating - assert!(matches!(thread.summary(), ThreadSummary::Error)); - }); - - // But the summarize request can be invoked manually - thread.update(cx, |thread, cx| { - thread.summarize(cx); - }); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Generating)); - }); - - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("A successful summary"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Ready(_))); - assert_eq!(thread.summary().or_default(), "A successful summary"); - }); - } - - // Helper to create a model that returns errors - enum TestError { - Overloaded, - InternalServerError, - } - - struct ErrorInjector { - inner: Arc, - error_type: TestError, - } - - impl ErrorInjector { - fn new(error_type: TestError) -> Self { - Self { - inner: Arc::new(FakeLanguageModel::default()), - error_type, - } - } - } - - impl LanguageModel for ErrorInjector { - fn id(&self) -> LanguageModelId { - self.inner.id() - } - - fn name(&self) -> LanguageModelName { - self.inner.name() - } - - fn provider_id(&self) -> LanguageModelProviderId { - self.inner.provider_id() - } - - fn provider_name(&self) -> LanguageModelProviderName { - self.inner.provider_name() - } - - fn supports_tools(&self) -> bool { - self.inner.supports_tools() - } - - fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { - self.inner.supports_tool_choice(choice) - } - - fn supports_images(&self) -> bool { - self.inner.supports_images() - } - - fn telemetry_id(&self) -> String { - self.inner.telemetry_id() - } - - fn max_token_count(&self) -> u64 { - self.inner.max_token_count() - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> BoxFuture<'static, Result> { - self.inner.count_tokens(request, cx) - } - - fn stream_completion( - &self, - _request: LanguageModelRequest, - _cx: &AsyncApp, - ) -> BoxFuture< - 'static, - Result< - BoxStream< - 'static, - Result, - >, - LanguageModelCompletionError, - >, - > { - let error = match self.error_type { - TestError::Overloaded => LanguageModelCompletionError::ServerOverloaded { - provider: self.provider_name(), - retry_after: None, - }, - TestError::InternalServerError => { - LanguageModelCompletionError::ApiInternalServerError { - provider: self.provider_name(), - message: "I'm a teapot orbiting the sun".to_string(), - } - } - }; - async move { - let stream = futures::stream::once(async move { Err(error) }); - Ok(stream.boxed()) - } - .boxed() - } - - fn as_fake(&self) -> &FakeLanguageModel { - &self.inner - } - } - - #[gpui::test] - async fn test_retry_on_overloaded_error(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create model that returns overloaded error - let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Start completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert!(thread.retry_state.is_some(), "Should have retry state"); - let retry_state = thread.retry_state.as_ref().unwrap(); - assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); - assert_eq!( - retry_state.max_attempts, MAX_RETRY_ATTEMPTS, - "Should retry MAX_RETRY_ATTEMPTS times for overloaded errors" - ); - }); - - // Check that a retry message was added - thread.read_with(cx, |thread, _| { - let mut messages = thread.messages(); - assert!( - messages.any(|msg| { - msg.role == Role::System - && msg.ui_only - && msg.segments.iter().any(|seg| { - if let MessageSegment::Text(text) = seg { - text.contains("overloaded") - && text - .contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS)) - } else { - false - } - }) - }), - "Should have added a system retry message" - ); - }); - - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - - assert_eq!(retry_count, 1, "Should have one retry message"); - } - - #[gpui::test] - async fn test_retry_on_internal_server_error(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create model that returns internal server error - let model = Arc::new(ErrorInjector::new(TestError::InternalServerError)); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Start completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - - cx.run_until_parked(); - - // Check retry state on thread - thread.read_with(cx, |thread, _| { - assert!(thread.retry_state.is_some(), "Should have retry state"); - let retry_state = thread.retry_state.as_ref().unwrap(); - assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); - assert_eq!( - retry_state.max_attempts, 3, - "Should have correct max attempts" - ); - }); - - // Check that a retry message was added with provider name - thread.read_with(cx, |thread, _| { - let mut messages = thread.messages(); - assert!( - messages.any(|msg| { - msg.role == Role::System - && msg.ui_only - && msg.segments.iter().any(|seg| { - if let MessageSegment::Text(text) = seg { - text.contains("internal") - && text.contains("Fake") - && text.contains("Retrying") - && text.contains("attempt 1 of 3") - && text.contains("seconds") - } else { - false - } - }) - }), - "Should have added a system retry message with provider name" - ); - }); - - // Count retry messages - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - - assert_eq!(retry_count, 1, "Should have one retry message"); - } - - #[gpui::test] - async fn test_exponential_backoff_on_retries(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create model that returns internal server error - let model = Arc::new(ErrorInjector::new(TestError::InternalServerError)); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Track retry events and completion count - // Track completion events - let completion_count = Arc::new(Mutex::new(0)); - let completion_count_clone = completion_count.clone(); - - let _subscription = thread.update(cx, |_, cx| { - cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| { - if let ThreadEvent::NewRequest = event { - *completion_count_clone.lock() += 1; - } - }) - }); - - // First attempt - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - cx.run_until_parked(); - - // Should have scheduled first retry - count retry messages - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - assert_eq!(retry_count, 1, "Should have scheduled first retry"); - - // Check retry state - thread.read_with(cx, |thread, _| { - assert!(thread.retry_state.is_some(), "Should have retry state"); - let retry_state = thread.retry_state.as_ref().unwrap(); - assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); - assert_eq!( - retry_state.max_attempts, 3, - "Internal server errors should retry up to 3 times" - ); - }); - - // Advance clock for first retry - cx.executor().advance_clock(BASE_RETRY_DELAY); - cx.run_until_parked(); - - // Advance clock for second retry - cx.executor().advance_clock(BASE_RETRY_DELAY); - cx.run_until_parked(); - - // Advance clock for third retry - cx.executor().advance_clock(BASE_RETRY_DELAY); - cx.run_until_parked(); - - // Should have completed all retries - count retry messages - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - assert_eq!( - retry_count, 3, - "Should have 3 retries for internal server errors" - ); - - // For internal server errors, we retry 3 times and then give up - // Check that retry_state is cleared after all retries - thread.read_with(cx, |thread, _| { - assert!( - thread.retry_state.is_none(), - "Retry state should be cleared after all retries" - ); - }); - - // Verify total attempts (1 initial + 3 retries) - assert_eq!( - *completion_count.lock(), - 4, - "Should have attempted once plus 3 retries" - ); - } - - #[gpui::test] - async fn test_max_retries_exceeded(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create model that returns overloaded error - let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Track events - let stopped_with_error = Arc::new(Mutex::new(false)); - let stopped_with_error_clone = stopped_with_error.clone(); - - let _subscription = thread.update(cx, |_, cx| { - cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| { - if let ThreadEvent::Stopped(Err(_)) = event { - *stopped_with_error_clone.lock() = true; - } - }) - }); - - // Start initial completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - cx.run_until_parked(); - - // Advance through all retries - for _ in 0..MAX_RETRY_ATTEMPTS { - cx.executor().advance_clock(BASE_RETRY_DELAY); - cx.run_until_parked(); - } - - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - - // After max retries, should emit Stopped(Err(...)) event - assert_eq!( - retry_count, MAX_RETRY_ATTEMPTS as usize, - "Should have attempted MAX_RETRY_ATTEMPTS retries for overloaded errors" - ); - assert!( - *stopped_with_error.lock(), - "Should emit Stopped(Err(...)) event after max retries exceeded" - ); - - // Retry state should be cleared - thread.read_with(cx, |thread, _| { - assert!( - thread.retry_state.is_none(), - "Retry state should be cleared after max retries" - ); - - // Verify we have the expected number of retry messages - let retry_messages = thread - .messages - .iter() - .filter(|msg| msg.ui_only && msg.role == Role::System) - .count(); - assert_eq!( - retry_messages, MAX_RETRY_ATTEMPTS as usize, - "Should have MAX_RETRY_ATTEMPTS retry messages for overloaded errors" - ); - }); - } - - #[gpui::test] - async fn test_retry_message_removed_on_retry(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // We'll use a wrapper to switch behavior after first failure - struct RetryTestModel { - inner: Arc, - failed_once: Arc>, - } - - impl LanguageModel for RetryTestModel { - fn id(&self) -> LanguageModelId { - self.inner.id() - } - - fn name(&self) -> LanguageModelName { - self.inner.name() - } - - fn provider_id(&self) -> LanguageModelProviderId { - self.inner.provider_id() - } - - fn provider_name(&self) -> LanguageModelProviderName { - self.inner.provider_name() - } - - fn supports_tools(&self) -> bool { - self.inner.supports_tools() - } - - fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { - self.inner.supports_tool_choice(choice) - } - - fn supports_images(&self) -> bool { - self.inner.supports_images() - } - - fn telemetry_id(&self) -> String { - self.inner.telemetry_id() - } - - fn max_token_count(&self) -> u64 { - self.inner.max_token_count() - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> BoxFuture<'static, Result> { - self.inner.count_tokens(request, cx) - } - - fn stream_completion( - &self, - request: LanguageModelRequest, - cx: &AsyncApp, - ) -> BoxFuture< - 'static, - Result< - BoxStream< - 'static, - Result, - >, - LanguageModelCompletionError, - >, - > { - if !*self.failed_once.lock() { - *self.failed_once.lock() = true; - let provider = self.provider_name(); - // Return error on first attempt - let stream = futures::stream::once(async move { - Err(LanguageModelCompletionError::ServerOverloaded { - provider, - retry_after: None, - }) - }); - async move { Ok(stream.boxed()) }.boxed() - } else { - // Succeed on retry - self.inner.stream_completion(request, cx) - } - } - - fn as_fake(&self) -> &FakeLanguageModel { - &self.inner - } - } - - let model = Arc::new(RetryTestModel { - inner: Arc::new(FakeLanguageModel::default()), - failed_once: Arc::new(Mutex::new(false)), - }); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Track message deletions - // Track when retry completes successfully - let retry_completed = Arc::new(Mutex::new(false)); - let retry_completed_clone = retry_completed.clone(); - - let _subscription = thread.update(cx, |_, cx| { - cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| { - if let ThreadEvent::StreamedCompletion = event { - *retry_completed_clone.lock() = true; - } - }) - }); - - // Start completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - cx.run_until_parked(); - - // Get the retry message ID - let retry_message_id = thread.read_with(cx, |thread, _| { - thread - .messages() - .find(|msg| msg.role == Role::System && msg.ui_only) - .map(|msg| msg.id) - .expect("Should have a retry message") - }); - - // Wait for retry - cx.executor().advance_clock(BASE_RETRY_DELAY); - cx.run_until_parked(); - - // Stream some successful content - let fake_model = model.as_fake(); - // After the retry, there should be a new pending completion - let pending = fake_model.pending_completions(); - assert!( - !pending.is_empty(), - "Should have a pending completion after retry" - ); - fake_model.send_completion_stream_text_chunk(&pending[0], "Success!"); - fake_model.end_completion_stream(&pending[0]); - cx.run_until_parked(); - - // Check that the retry completed successfully - assert!( - *retry_completed.lock(), - "Retry should have completed successfully" - ); - - // Retry message should still exist but be marked as ui_only - thread.read_with(cx, |thread, _| { - let retry_msg = thread - .message(retry_message_id) - .expect("Retry message should still exist"); - assert!(retry_msg.ui_only, "Retry message should be ui_only"); - assert_eq!( - retry_msg.role, - Role::System, - "Retry message should have System role" - ); - }); - } - - #[gpui::test] - async fn test_successful_completion_clears_retry_state(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create a model that fails once then succeeds - struct FailOnceModel { - inner: Arc, - failed_once: Arc>, - } - - impl LanguageModel for FailOnceModel { - fn id(&self) -> LanguageModelId { - self.inner.id() - } - - fn name(&self) -> LanguageModelName { - self.inner.name() - } - - fn provider_id(&self) -> LanguageModelProviderId { - self.inner.provider_id() - } - - fn provider_name(&self) -> LanguageModelProviderName { - self.inner.provider_name() - } - - fn supports_tools(&self) -> bool { - self.inner.supports_tools() - } - - fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { - self.inner.supports_tool_choice(choice) - } - - fn supports_images(&self) -> bool { - self.inner.supports_images() - } - - fn telemetry_id(&self) -> String { - self.inner.telemetry_id() - } - - fn max_token_count(&self) -> u64 { - self.inner.max_token_count() - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> BoxFuture<'static, Result> { - self.inner.count_tokens(request, cx) - } - - fn stream_completion( - &self, - request: LanguageModelRequest, - cx: &AsyncApp, - ) -> BoxFuture< - 'static, - Result< - BoxStream< - 'static, - Result, - >, - LanguageModelCompletionError, - >, - > { - if !*self.failed_once.lock() { - *self.failed_once.lock() = true; - let provider = self.provider_name(); - // Return error on first attempt - let stream = futures::stream::once(async move { - Err(LanguageModelCompletionError::ServerOverloaded { - provider, - retry_after: None, - }) - }); - async move { Ok(stream.boxed()) }.boxed() - } else { - // Succeed on retry - self.inner.stream_completion(request, cx) - } - } - } - - let fail_once_model = Arc::new(FailOnceModel { - inner: Arc::new(FakeLanguageModel::default()), - failed_once: Arc::new(Mutex::new(false)), - }); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message( - "Test message", - ContextLoadResult::default(), - None, - vec![], - cx, - ); - }); - - // Start completion with fail-once model - thread.update(cx, |thread, cx| { - thread.send_to_model( - fail_once_model.clone(), - CompletionIntent::UserPrompt, - None, - cx, - ); - }); - - cx.run_until_parked(); - - // Verify retry state exists after first failure - thread.read_with(cx, |thread, _| { - assert!( - thread.retry_state.is_some(), - "Should have retry state after failure" - ); - }); - - // Wait for retry delay - cx.executor().advance_clock(BASE_RETRY_DELAY); - cx.run_until_parked(); - - // The retry should now use our FailOnceModel which should succeed - // We need to help the FakeLanguageModel complete the stream - let inner_fake = fail_once_model.inner.clone(); - - // Wait a bit for the retry to start - cx.run_until_parked(); - - // Check for pending completions and complete them - if let Some(pending) = inner_fake.pending_completions().first() { - inner_fake.send_completion_stream_text_chunk(pending, "Success!"); - inner_fake.end_completion_stream(pending); - } - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert!( - thread.retry_state.is_none(), - "Retry state should be cleared after successful completion" - ); - - let has_assistant_message = thread - .messages - .iter() - .any(|msg| msg.role == Role::Assistant && !msg.ui_only); - assert!( - has_assistant_message, - "Should have an assistant message after successful retry" - ); - }); - } - - #[gpui::test] - async fn test_rate_limit_retry_single_attempt(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create a model that returns rate limit error with retry_after - struct RateLimitModel { - inner: Arc, - } - - impl LanguageModel for RateLimitModel { - fn id(&self) -> LanguageModelId { - self.inner.id() - } - - fn name(&self) -> LanguageModelName { - self.inner.name() - } - - fn provider_id(&self) -> LanguageModelProviderId { - self.inner.provider_id() - } - - fn provider_name(&self) -> LanguageModelProviderName { - self.inner.provider_name() - } - - fn supports_tools(&self) -> bool { - self.inner.supports_tools() - } - - fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { - self.inner.supports_tool_choice(choice) - } - - fn supports_images(&self) -> bool { - self.inner.supports_images() - } - - fn telemetry_id(&self) -> String { - self.inner.telemetry_id() - } - - fn max_token_count(&self) -> u64 { - self.inner.max_token_count() - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> BoxFuture<'static, Result> { - self.inner.count_tokens(request, cx) - } - - fn stream_completion( - &self, - _request: LanguageModelRequest, - _cx: &AsyncApp, - ) -> BoxFuture< - 'static, - Result< - BoxStream< - 'static, - Result, - >, - LanguageModelCompletionError, - >, - > { - let provider = self.provider_name(); - async move { - let stream = futures::stream::once(async move { - Err(LanguageModelCompletionError::RateLimitExceeded { - provider, - retry_after: Some(Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS)), - }) - }); - Ok(stream.boxed()) - } - .boxed() - } - - fn as_fake(&self) -> &FakeLanguageModel { - &self.inner - } - } - - let model = Arc::new(RateLimitModel { - inner: Arc::new(FakeLanguageModel::default()), - }); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Start completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - - cx.run_until_parked(); - - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("rate limit exceeded") - } else { - false - } - }) - }) - .count() - }); - assert_eq!(retry_count, 1, "Should have scheduled one retry"); - - thread.read_with(cx, |thread, _| { - assert!( - thread.retry_state.is_some(), - "Rate limit errors should set retry_state" - ); - if let Some(retry_state) = &thread.retry_state { - assert_eq!( - retry_state.max_attempts, MAX_RETRY_ATTEMPTS, - "Rate limit errors should use MAX_RETRY_ATTEMPTS" - ); - } - }); - - // Verify we have one retry message - thread.read_with(cx, |thread, _| { - let retry_messages = thread - .messages - .iter() - .filter(|msg| { - msg.ui_only - && msg.segments.iter().any(|seg| { - if let MessageSegment::Text(text) = seg { - text.contains("rate limit exceeded") - } else { - false - } - }) - }) - .count(); - assert_eq!( - retry_messages, 1, - "Should have one rate limit retry message" - ); - }); - - // Check that retry message doesn't include attempt count - thread.read_with(cx, |thread, _| { - let retry_message = thread - .messages - .iter() - .find(|msg| msg.role == Role::System && msg.ui_only) - .expect("Should have a retry message"); - - // Check that the message contains attempt count since we use retry_state - if let Some(MessageSegment::Text(text)) = retry_message.segments.first() { - assert!( - text.contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS)), - "Rate limit retry message should contain attempt count with MAX_RETRY_ATTEMPTS" - ); - assert!( - text.contains("Retrying"), - "Rate limit retry message should contain retry text" - ); - } - }); - } - - #[gpui::test] - async fn test_ui_only_messages_not_sent_to_model(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, model) = setup_test_environment(cx, project.clone()).await; - - // Insert a regular user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Insert a UI-only message (like our retry notifications) - thread.update(cx, |thread, cx| { - let id = thread.next_message_id.post_inc(); - thread.messages.push(Message { - id, - role: Role::System, - segments: vec![MessageSegment::Text( - "This is a UI-only message that should not be sent to the model".to_string(), - )], - loaded_context: LoadedContext::default(), - creases: Vec::new(), - is_hidden: true, - ui_only: true, - }); - cx.emit(ThreadEvent::MessageAdded(id)); - }); - - // Insert another regular message - thread.update(cx, |thread, cx| { - thread.insert_user_message( - "How are you?", - ContextLoadResult::default(), - None, - vec![], - cx, - ); - }); - - // Generate the completion request - let request = thread.update(cx, |thread, cx| { - thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx) - }); - - // Verify that the request only contains non-UI-only messages - // Should have system prompt + 2 user messages, but not the UI-only message - let user_messages: Vec<_> = request - .messages - .iter() - .filter(|msg| msg.role == Role::User) - .collect(); - assert_eq!( - user_messages.len(), - 2, - "Should have exactly 2 user messages" - ); - - // Verify the UI-only content is not present anywhere in the request - let request_text = request - .messages - .iter() - .flat_map(|msg| &msg.content) - .filter_map(|content| match content { - MessageContent::Text(text) => Some(text.as_str()), - _ => None, - }) - .collect::(); - - assert!( - !request_text.contains("UI-only message"), - "UI-only message content should not be in the request" - ); - - // Verify the thread still has all 3 messages (including UI-only) - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.messages().count(), - 3, - "Thread should have 3 messages" - ); - assert_eq!( - thread.messages().filter(|m| m.ui_only).count(), - 1, - "Thread should have 1 UI-only message" - ); - }); - - // Verify that UI-only messages are not serialized - let serialized = thread - .update(cx, |thread, cx| thread.serialize(cx)) - .await - .unwrap(); - assert_eq!( - serialized.messages.len(), - 2, - "Serialized thread should only have 2 messages (no UI-only)" - ); - } - - #[gpui::test] - async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Ensure we're in Normal mode (not Burn mode) - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Normal); - }); - - // Track error events - let error_events = Arc::new(Mutex::new(Vec::new())); - let error_events_clone = error_events.clone(); - - let _subscription = thread.update(cx, |_, cx| { - cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| { - if let ThreadEvent::ShowError(error) = event { - error_events_clone.lock().push(error.clone()); - } - }) - }); - - // Create model that returns overloaded error - let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Start completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - - cx.run_until_parked(); - - // Verify no retry state was created - thread.read_with(cx, |thread, _| { - assert!( - thread.retry_state.is_none(), - "Should not have retry state in Normal mode" - ); - }); - - // Check that a retryable error was reported - let errors = error_events.lock(); - assert!(!errors.is_empty(), "Should have received an error event"); - - if let ThreadError::RetryableError { - message: _, - can_enable_burn_mode, - } = &errors[0] - { - assert!( - *can_enable_burn_mode, - "Error should indicate burn mode can be enabled" - ); - } else { - panic!("Expected RetryableError, got {:?}", errors[0]); - } - - // Verify the thread is no longer generating - thread.read_with(cx, |thread, _| { - assert!( - !thread.is_generating(), - "Should not be generating after error without retry" - ); - }); - } - - #[gpui::test] - async fn test_retry_canceled_on_stop(cx: &mut TestAppContext) { - let fs = init_test_settings(cx); - - let project = create_test_project(&fs, cx, json!({})).await; - let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - - // Enable Burn Mode to allow retries - thread.update(cx, |thread, _| { - thread.set_completion_mode(CompletionMode::Burn); - }); - - // Create model that returns overloaded error - let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); - - // Insert a user message - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); - }); - - // Start completion - thread.update(cx, |thread, cx| { - thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); - }); - - cx.run_until_parked(); - - // Verify retry was scheduled by checking for retry message - let has_retry_message = thread.read_with(cx, |thread, _| { - thread.messages.iter().any(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - }); - assert!(has_retry_message, "Should have scheduled a retry"); - - // Cancel the completion before the retry happens - thread.update(cx, |thread, cx| { - thread.cancel_last_completion(None, cx); - }); - - cx.run_until_parked(); - - // The retry should not have happened - no pending completions - let fake_model = model.as_fake(); - assert_eq!( - fake_model.pending_completions().len(), - 0, - "Should have no pending completions after cancellation" - ); - - // Verify the retry was canceled by checking retry state - thread.read_with(cx, |thread, _| { - if let Some(retry_state) = &thread.retry_state { - panic!( - "retry_state should be cleared after cancellation, but found: attempt={}, max_attempts={}, intent={:?}", - retry_state.attempt, retry_state.max_attempts, retry_state.intent - ); - } - }); - } - - fn test_summarize_error( - model: &Arc, - thread: &Entity, - cx: &mut TestAppContext, - ) { - thread.update(cx, |thread, cx| { - thread.insert_user_message("Hi!", ContextLoadResult::default(), None, vec![], cx); - thread.send_to_model( - model.clone(), - CompletionIntent::ThreadSummarization, - None, - cx, - ); - }); - - let fake_model = model.as_fake(); - simulate_successful_response(fake_model, cx); - - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Generating)); - assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT); - }); - - // Simulate summary request ending - cx.run_until_parked(); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - // State is set to Error and default message - thread.read_with(cx, |thread, _| { - assert!(matches!(thread.summary(), ThreadSummary::Error)); - assert_eq!(thread.summary().or_default(), ThreadSummary::DEFAULT); - }); - } - - fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestAppContext) { - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Assistant response"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - } - - fn init_test_settings(cx: &mut TestAppContext) -> Arc { - let fs = FakeFs::new(cx.executor()); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - AgentSettings::register(cx); - prompt_store::init(cx); - thread_store::init(fs.clone(), cx); - workspace::init_settings(cx); - language_model::init_settings(cx); - theme::init(theme::LoadThemes::JustBase, cx); - ToolRegistry::default_global(cx); - assistant_tool::init(cx); - - let http_client = Arc::new(http_client::HttpClientWithUrl::new( - http_client::FakeHttpClient::with_200_response(), - "http://localhost".to_string(), - None, - )); - assistant_tools::init(http_client, cx); - }); - fs - } - - // Helper to create a test project with test files - async fn create_test_project( - fs: &Arc, - cx: &mut TestAppContext, - files: serde_json::Value, - ) -> Entity { - fs.as_fake().insert_tree(path!("/test"), files).await; - Project::test(fs.clone(), [path!("/test").as_ref()], cx).await - } - - async fn setup_test_environment( - cx: &mut TestAppContext, - project: Entity, - ) -> ( - Entity, - Entity, - Entity, - Entity, - Arc, - ) { - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let thread_store = cx - .update(|_, cx| { - ThreadStore::load( - project.clone(), - cx.new(|_| ToolWorkingSet::default()), - None, - Arc::new(PromptBuilder::new(None).unwrap()), - cx, - ) - }) - .await - .unwrap(); - - let thread = thread_store.update(cx, |store, cx| store.create_thread(cx)); - let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None)); - - let provider = Arc::new(FakeLanguageModelProvider::default()); - let model = provider.test_model(); - let model: Arc = Arc::new(model); - - cx.update(|_, cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.set_default_model( - Some(ConfiguredModel { - provider: provider.clone(), - model: model.clone(), - }), - cx, - ); - registry.set_thread_summary_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); - }) - }); - - (workspace, thread_store, thread, context_store, model) - } - - async fn add_file_to_context( - project: &Entity, - context_store: &Entity, - path: &str, - cx: &mut TestAppContext, - ) -> Result> { - let buffer_path = project - .read_with(cx, |project, cx| project.find_project_path(path, cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(buffer_path.clone(), cx) - }) - .await - .unwrap(); - - context_store.update(cx, |context_store, cx| { - context_store.add_file_from_buffer(&buffer_path, buffer.clone(), false, cx); - }); - - Ok(buffer) +fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { + LanguageModelImage { + source: image_content.data.into(), + // TODO: make this optional? + size: gpui::Size::new(0.into(), 0.into()), } } diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs deleted file mode 100644 index 2139f232e3e99b1affb78928dec70e1aaef2a03a..0000000000000000000000000000000000000000 --- a/crates/agent/src/thread_store.rs +++ /dev/null @@ -1,1287 +0,0 @@ -use crate::{ - context_server_tool::ContextServerTool, - thread::{ - DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId, - }, -}; -use agent_settings::{AgentProfileId, CompletionMode}; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolId, ToolWorkingSet}; -use chrono::{DateTime, Utc}; -use collections::HashMap; -use context_server::ContextServerId; -use fs::{Fs, RemoveOptions}; -use futures::{ - FutureExt as _, StreamExt as _, - channel::{mpsc, oneshot}, - future::{self, BoxFuture, Shared}, -}; -use gpui::{ - App, BackgroundExecutor, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, - Subscription, Task, Window, prelude::*, -}; -use indoc::indoc; -use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage}; -use project::context_server_store::{ContextServerStatus, ContextServerStore}; -use project::{Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{ - ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext, - UserRulesContext, WorktreeContext, -}; -use serde::{Deserialize, Serialize}; -use sqlez::{ - bindable::{Bind, Column}, - connection::Connection, - statement::Statement, -}; -use std::{ - cell::{Ref, RefCell}, - path::{Path, PathBuf}, - rc::Rc, - sync::{Arc, LazyLock, Mutex}, -}; -use util::{ResultExt as _, rel_path::RelPath}; - -use zed_env_vars::ZED_STATELESS; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum DataType { - #[serde(rename = "json")] - Json, - #[serde(rename = "zstd")] - Zstd, -} - -impl Bind for DataType { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let value = match self { - DataType::Json => "json", - DataType::Zstd => "zstd", - }; - value.bind(statement, start_index) - } -} - -impl Column for DataType { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (value, next_index) = String::column(statement, start_index)?; - let data_type = match value.as_str() { - "json" => DataType::Json, - "zstd" => DataType::Zstd, - _ => anyhow::bail!("Unknown data type: {}", value), - }; - Ok((data_type, next_index)) - } -} - -static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| { - [ - RelPath::unix(".rules").unwrap(), - RelPath::unix(".cursorrules").unwrap(), - RelPath::unix(".windsurfrules").unwrap(), - RelPath::unix(".clinerules").unwrap(), - RelPath::unix(".github/copilot-instructions.md").unwrap(), - RelPath::unix("CLAUDE.md").unwrap(), - RelPath::unix("AGENT.md").unwrap(), - RelPath::unix("AGENTS.md").unwrap(), - RelPath::unix("GEMINI.md").unwrap(), - ] -}); - -pub fn init(fs: Arc, cx: &mut App) { - ThreadsDatabase::init(fs, cx); -} - -/// A system prompt shared by all threads created by this ThreadStore -#[derive(Clone, Default)] -pub struct SharedProjectContext(Rc>>); - -impl SharedProjectContext { - pub fn borrow(&self) -> Ref<'_, Option> { - self.0.borrow() - } -} - -pub type TextThreadStore = assistant_context::ContextStore; - -pub struct ThreadStore { - project: Entity, - tools: Entity, - prompt_builder: Arc, - prompt_store: Option>, - context_server_tool_ids: HashMap>, - threads: Vec, - project_context: SharedProjectContext, - reload_system_prompt_tx: mpsc::Sender<()>, - _reload_system_prompt_task: Task<()>, - _subscriptions: Vec, -} - -pub struct RulesLoadingError { - pub message: SharedString, -} - -impl EventEmitter for ThreadStore {} - -impl ThreadStore { - pub fn load( - project: Entity, - tools: Entity, - prompt_store: Option>, - prompt_builder: Arc, - cx: &mut App, - ) -> Task>> { - cx.spawn(async move |cx| { - let (thread_store, ready_rx) = cx.update(|cx| { - let mut option_ready_rx = None; - let thread_store = cx.new(|cx| { - let (thread_store, ready_rx) = - Self::new(project, tools, prompt_builder, prompt_store, cx); - option_ready_rx = Some(ready_rx); - thread_store - }); - (thread_store, option_ready_rx.take().unwrap()) - })?; - ready_rx.await?; - Ok(thread_store) - }) - } - - fn new( - project: Entity, - tools: Entity, - prompt_builder: Arc, - prompt_store: Option>, - cx: &mut Context, - ) -> (Self, oneshot::Receiver<()>) { - let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)]; - - if let Some(prompt_store) = prompt_store.as_ref() { - subscriptions.push(cx.subscribe( - prompt_store, - |this, _prompt_store, PromptsUpdatedEvent, _cx| { - this.enqueue_system_prompt_reload(); - }, - )) - } - - // This channel and task prevent concurrent and redundant loading of the system prompt. - let (reload_system_prompt_tx, mut reload_system_prompt_rx) = mpsc::channel(1); - let (ready_tx, ready_rx) = oneshot::channel(); - let mut ready_tx = Some(ready_tx); - let reload_system_prompt_task = cx.spawn({ - let prompt_store = prompt_store.clone(); - async move |thread_store, cx| { - loop { - let Some(reload_task) = thread_store - .update(cx, |thread_store, cx| { - thread_store.reload_system_prompt(prompt_store.clone(), cx) - }) - .ok() - else { - return; - }; - reload_task.await; - if let Some(ready_tx) = ready_tx.take() { - ready_tx.send(()).ok(); - } - reload_system_prompt_rx.next().await; - } - } - }); - - let this = Self { - project, - tools, - prompt_builder, - prompt_store, - context_server_tool_ids: HashMap::default(), - threads: Vec::new(), - project_context: SharedProjectContext::default(), - reload_system_prompt_tx, - _reload_system_prompt_task: reload_system_prompt_task, - _subscriptions: subscriptions, - }; - this.register_context_server_handlers(cx); - this.reload(cx).detach_and_log_err(cx); - (this, ready_rx) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn fake(project: Entity, cx: &mut App) -> Self { - Self { - project, - tools: cx.new(|_| ToolWorkingSet::default()), - prompt_builder: Arc::new(PromptBuilder::new(None).unwrap()), - prompt_store: None, - context_server_tool_ids: HashMap::default(), - threads: Vec::new(), - project_context: SharedProjectContext::default(), - reload_system_prompt_tx: mpsc::channel(0).0, - _reload_system_prompt_task: Task::ready(()), - _subscriptions: vec![], - } - } - - fn handle_project_event( - &mut self, - _project: Entity, - event: &project::Event, - _cx: &mut Context, - ) { - match event { - project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { - self.enqueue_system_prompt_reload(); - } - project::Event::WorktreeUpdatedEntries(_, items) => { - if items - .iter() - .any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name)) - { - self.enqueue_system_prompt_reload(); - } - } - _ => {} - } - } - - fn enqueue_system_prompt_reload(&mut self) { - self.reload_system_prompt_tx.try_send(()).ok(); - } - - // Note that this should only be called from `reload_system_prompt_task`. - fn reload_system_prompt( - &self, - prompt_store: Option>, - cx: &mut Context, - ) -> Task<()> { - let worktrees = self - .project - .read(cx) - .visible_worktrees(cx) - .collect::>(); - let worktree_tasks = worktrees - .into_iter() - .map(|worktree| { - Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx) - }) - .collect::>(); - let default_user_rules_task = match prompt_store { - None => Task::ready(vec![]), - Some(prompt_store) => prompt_store.read_with(cx, |prompt_store, cx| { - let prompts = prompt_store.default_prompt_metadata(); - let load_tasks = prompts.into_iter().map(|prompt_metadata| { - let contents = prompt_store.load(prompt_metadata.id, cx); - async move { (contents.await, prompt_metadata) } - }); - cx.background_spawn(future::join_all(load_tasks)) - }), - }; - - cx.spawn(async move |this, cx| { - let (worktrees, default_user_rules) = - future::join(future::join_all(worktree_tasks), default_user_rules_task).await; - - let worktrees = worktrees - .into_iter() - .map(|(worktree, rules_error)| { - if let Some(rules_error) = rules_error { - this.update(cx, |_, cx| cx.emit(rules_error)).ok(); - } - worktree - }) - .collect::>(); - - let default_user_rules = default_user_rules - .into_iter() - .flat_map(|(contents, prompt_metadata)| match contents { - Ok(contents) => Some(UserRulesContext { - uuid: match prompt_metadata.id { - PromptId::User { uuid } => uuid, - PromptId::EditWorkflow => return None, - }, - title: prompt_metadata.title.map(|title| title.to_string()), - contents, - }), - Err(err) => { - this.update(cx, |_, cx| { - cx.emit(RulesLoadingError { - message: format!("{err:?}").into(), - }); - }) - .ok(); - None - } - }) - .collect::>(); - - this.update(cx, |this, _cx| { - *this.project_context.0.borrow_mut() = - Some(ProjectContext::new(worktrees, default_user_rules)); - }) - .ok(); - }) - } - - fn load_worktree_info_for_system_prompt( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Task<(WorktreeContext, Option)> { - let tree = worktree.read(cx); - let root_name = tree.root_name_str().into(); - let abs_path = tree.abs_path(); - - let mut context = WorktreeContext { - root_name, - abs_path, - rules_file: None, - }; - - let rules_task = Self::load_worktree_rules_file(worktree, project, cx); - let Some(rules_task) = rules_task else { - return Task::ready((context, None)); - }; - - cx.spawn(async move |_| { - let (rules_file, rules_file_error) = match rules_task.await { - Ok(rules_file) => (Some(rules_file), None), - Err(err) => ( - None, - Some(RulesLoadingError { - message: format!("{err}").into(), - }), - ), - }; - context.rules_file = rules_file; - (context, rules_file_error) - }) - } - - fn load_worktree_rules_file( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Option>> { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let selected_rules_file = RULES_FILE_NAMES - .into_iter() - .filter_map(|name| { - worktree - .entry_for_path(name) - .filter(|entry| entry.is_file()) - .map(|entry| entry.path.clone()) - }) - .next(); - - // Note that Cline supports `.clinerules` being a directory, but that is not currently - // supported. This doesn't seem to occur often in GitHub repositories. - selected_rules_file.map(|path_in_worktree| { - let project_path = ProjectPath { - worktree_id, - path: path_in_worktree.clone(), - }; - let buffer_task = - project.update(cx, |project, cx| project.open_buffer(project_path, cx)); - let rope_task = cx.spawn(async move |cx| { - buffer_task.await?.read_with(cx, |buffer, cx| { - let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; - anyhow::Ok((project_entry_id, buffer.as_rope().clone())) - })? - }); - // Build a string from the rope on a background thread. - cx.background_spawn(async move { - let (project_entry_id, rope) = rope_task.await?; - anyhow::Ok(RulesFileContext { - path_in_worktree, - text: rope.to_string().trim().to_string(), - project_entry_id: project_entry_id.to_usize(), - }) - }) - }) - } - - pub fn prompt_store(&self) -> &Option> { - &self.prompt_store - } - - pub fn tools(&self) -> Entity { - self.tools.clone() - } - - /// Returns the number of threads. - pub fn thread_count(&self) -> usize { - self.threads.len() - } - - pub fn reverse_chronological_threads(&self) -> impl Iterator { - // ordering is from "ORDER BY" in `list_threads` - self.threads.iter() - } - - pub fn create_thread(&mut self, cx: &mut Context) -> Entity { - cx.new(|cx| { - Thread::new( - self.project.clone(), - self.tools.clone(), - self.prompt_builder.clone(), - self.project_context.clone(), - cx, - ) - }) - } - - pub fn create_thread_from_serialized( - &mut self, - serialized: SerializedThread, - cx: &mut Context, - ) -> Entity { - cx.new(|cx| { - Thread::deserialize( - ThreadId::new(), - serialized, - self.project.clone(), - self.tools.clone(), - self.prompt_builder.clone(), - self.project_context.clone(), - None, - cx, - ) - }) - } - - pub fn open_thread( - &self, - id: &ThreadId, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let id = id.clone(); - let database_future = ThreadsDatabase::global_future(cx); - let this = cx.weak_entity(); - window.spawn(cx, async move |cx| { - let database = database_future.await.map_err(|err| anyhow!(err))?; - let thread = database - .try_find_thread(id.clone()) - .await? - .with_context(|| format!("no thread found with ID: {id:?}"))?; - - let thread = this.update_in(cx, |this, window, cx| { - cx.new(|cx| { - Thread::deserialize( - id.clone(), - thread, - this.project.clone(), - this.tools.clone(), - this.prompt_builder.clone(), - this.project_context.clone(), - Some(window), - cx, - ) - }) - })?; - - Ok(thread) - }) - } - - pub fn save_thread(&self, thread: &Entity, cx: &mut Context) -> Task> { - let (metadata, serialized_thread) = - thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx))); - - let database_future = ThreadsDatabase::global_future(cx); - cx.spawn(async move |this, cx| { - let serialized_thread = serialized_thread.await?; - let database = database_future.await.map_err(|err| anyhow!(err))?; - database.save_thread(metadata, serialized_thread).await?; - - this.update(cx, |this, cx| this.reload(cx))?.await - }) - } - - pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut Context) -> Task> { - let id = id.clone(); - let database_future = ThreadsDatabase::global_future(cx); - cx.spawn(async move |this, cx| { - let database = database_future.await.map_err(|err| anyhow!(err))?; - database.delete_thread(id.clone()).await?; - - this.update(cx, |this, cx| { - this.threads.retain(|thread| thread.id != id); - cx.notify(); - }) - }) - } - - pub fn reload(&self, cx: &mut Context) -> Task> { - let database_future = ThreadsDatabase::global_future(cx); - cx.spawn(async move |this, cx| { - let threads = database_future - .await - .map_err(|err| anyhow!(err))? - .list_threads() - .await?; - - this.update(cx, |this, cx| { - this.threads = threads; - cx.notify(); - }) - }) - } - - fn register_context_server_handlers(&self, cx: &mut Context) { - let context_server_store = self.project.read(cx).context_server_store(); - cx.subscribe(&context_server_store, Self::handle_context_server_event) - .detach(); - - // Check for any servers that were already running before the handler was registered - for server in context_server_store.read(cx).running_servers() { - self.load_context_server_tools(server.id(), context_server_store.clone(), cx); - } - } - - fn handle_context_server_event( - &mut self, - context_server_store: Entity, - event: &project::context_server_store::Event, - cx: &mut Context, - ) { - let tool_working_set = self.tools.clone(); - match event { - project::context_server_store::Event::ServerStatusChanged { server_id, status } => { - match status { - ContextServerStatus::Starting => {} - ContextServerStatus::Running => { - self.load_context_server_tools(server_id.clone(), context_server_store, cx); - } - ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { - if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) { - tool_working_set.update(cx, |tool_working_set, cx| { - tool_working_set.remove(&tool_ids, cx); - }); - } - } - } - } - } - } - - fn load_context_server_tools( - &self, - server_id: ContextServerId, - context_server_store: Entity, - cx: &mut Context, - ) { - let Some(server) = context_server_store.read(cx).get_running_server(&server_id) else { - return; - }; - let tool_working_set = self.tools.clone(); - cx.spawn(async move |this, cx| { - let Some(protocol) = server.client() else { - return; - }; - - if protocol.capable(context_server::protocol::ServerCapability::Tools) - && let Some(response) = protocol - .request::(()) - .await - .log_err() - { - let tool_ids = tool_working_set - .update(cx, |tool_working_set, cx| { - tool_working_set.extend( - response.tools.into_iter().map(|tool| { - Arc::new(ContextServerTool::new( - context_server_store.clone(), - server.id(), - tool, - )) as Arc - }), - cx, - ) - }) - .log_err(); - - if let Some(tool_ids) = tool_ids { - this.update(cx, |this, _| { - this.context_server_tool_ids.insert(server_id, tool_ids); - }) - .log_err(); - } - } - }) - .detach(); - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SerializedThreadMetadata { - pub id: ThreadId, - pub summary: SharedString, - pub updated_at: DateTime, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct SerializedThread { - pub version: String, - pub summary: SharedString, - pub updated_at: DateTime, - pub messages: Vec, - #[serde(default)] - pub initial_project_snapshot: Option>, - #[serde(default)] - pub cumulative_token_usage: TokenUsage, - #[serde(default)] - pub request_token_usage: Vec, - #[serde(default)] - pub detailed_summary_state: DetailedSummaryState, - #[serde(default)] - pub exceeded_window_error: Option, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub completion_mode: Option, - #[serde(default)] - pub tool_use_limit_reached: bool, - #[serde(default)] - pub profile: Option, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct SerializedLanguageModel { - pub provider: String, - pub model: String, -} - -impl SerializedThread { - pub const VERSION: &'static str = "0.2.0"; - - pub fn from_json(json: &[u8]) -> Result { - let saved_thread_json = serde_json::from_slice::(json)?; - match saved_thread_json.get("version") { - Some(serde_json::Value::String(version)) => match version.as_str() { - SerializedThreadV0_1_0::VERSION => { - let saved_thread = - serde_json::from_value::(saved_thread_json)?; - Ok(saved_thread.upgrade()) - } - SerializedThread::VERSION => Ok(serde_json::from_value::( - saved_thread_json, - )?), - _ => anyhow::bail!("unrecognized serialized thread version: {version:?}"), - }, - None => { - let saved_thread = - serde_json::from_value::(saved_thread_json)?; - Ok(saved_thread.upgrade()) - } - version => anyhow::bail!("unrecognized serialized thread version: {version:?}"), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SerializedThreadV0_1_0( - // The structure did not change, so we are reusing the latest SerializedThread. - // When making the next version, make sure this points to SerializedThreadV0_2_0 - SerializedThread, -); - -impl SerializedThreadV0_1_0 { - pub const VERSION: &'static str = "0.1.0"; - - pub fn upgrade(self) -> SerializedThread { - debug_assert_eq!(SerializedThread::VERSION, "0.2.0"); - - let mut messages: Vec = Vec::with_capacity(self.0.messages.len()); - - for message in self.0.messages { - if message.role == Role::User - && !message.tool_results.is_empty() - && let Some(last_message) = messages.last_mut() - { - debug_assert!(last_message.role == Role::Assistant); - - last_message.tool_results = message.tool_results; - continue; - } - - messages.push(message); - } - - SerializedThread { - messages, - version: SerializedThread::VERSION.to_string(), - ..self.0 - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedMessage { - pub id: MessageId, - pub role: Role, - #[serde(default)] - pub segments: Vec, - #[serde(default)] - pub tool_uses: Vec, - #[serde(default)] - pub tool_results: Vec, - #[serde(default)] - pub context: String, - #[serde(default)] - pub creases: Vec, - #[serde(default)] - pub is_hidden: bool, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub enum SerializedMessageSegment { - #[serde(rename = "text")] - Text { - text: String, - }, - #[serde(rename = "thinking")] - Thinking { - text: String, - #[serde(skip_serializing_if = "Option::is_none")] - signature: Option, - }, - RedactedThinking { - data: String, - }, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedToolUse { - pub id: LanguageModelToolUseId, - pub name: SharedString, - pub input: serde_json::Value, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedToolResult { - pub tool_use_id: LanguageModelToolUseId, - pub is_error: bool, - pub content: LanguageModelToolResultContent, - pub output: Option, -} - -#[derive(Serialize, Deserialize)] -struct LegacySerializedThread { - pub summary: SharedString, - pub updated_at: DateTime, - pub messages: Vec, - #[serde(default)] - pub initial_project_snapshot: Option>, -} - -impl LegacySerializedThread { - pub fn upgrade(self) -> SerializedThread { - SerializedThread { - version: SerializedThread::VERSION.to_string(), - summary: self.summary, - updated_at: self.updated_at, - messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(), - initial_project_snapshot: self.initial_project_snapshot, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: Vec::new(), - detailed_summary_state: DetailedSummaryState::default(), - exceeded_window_error: None, - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct LegacySerializedMessage { - pub id: MessageId, - pub role: Role, - pub text: String, - #[serde(default)] - pub tool_uses: Vec, - #[serde(default)] - pub tool_results: Vec, -} - -impl LegacySerializedMessage { - fn upgrade(self) -> SerializedMessage { - SerializedMessage { - id: self.id, - role: self.role, - segments: vec![SerializedMessageSegment::Text { text: self.text }], - tool_uses: self.tool_uses, - tool_results: self.tool_results, - context: String::new(), - creases: Vec::new(), - is_hidden: false, - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedCrease { - pub start: usize, - pub end: usize, - pub icon_path: SharedString, - pub label: SharedString, -} - -struct GlobalThreadsDatabase( - Shared, Arc>>>, -); - -impl Global for GlobalThreadsDatabase {} - -pub(crate) struct ThreadsDatabase { - executor: BackgroundExecutor, - connection: Arc>, -} - -impl ThreadsDatabase { - fn connection(&self) -> Arc> { - self.connection.clone() - } - - const COMPRESSION_LEVEL: i32 = 3; -} - -impl Bind for ThreadId { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.to_string().bind(statement, start_index) - } -} - -impl Column for ThreadId { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (id_str, next_index) = String::column(statement, start_index)?; - Ok((ThreadId::from(id_str.as_str()), next_index)) - } -} - -impl ThreadsDatabase { - fn global_future( - cx: &mut App, - ) -> Shared, Arc>>> { - GlobalThreadsDatabase::global(cx).0.clone() - } - - fn init(fs: Arc, cx: &mut App) { - let executor = cx.background_executor().clone(); - let database_future = executor - .spawn({ - let executor = executor.clone(); - let threads_dir = paths::data_dir().join("threads"); - async move { ThreadsDatabase::new(fs, threads_dir, executor).await } - }) - .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new))) - .boxed() - .shared(); - - cx.set_global(GlobalThreadsDatabase(database_future)); - } - - pub async fn new( - fs: Arc, - threads_dir: PathBuf, - executor: BackgroundExecutor, - ) -> Result { - fs.create_dir(&threads_dir).await?; - - let sqlite_path = threads_dir.join("threads.db"); - let mdb_path = threads_dir.join("threads-db.1.mdb"); - - let needs_migration_from_heed = fs.is_file(&mdb_path).await; - - let connection = if *ZED_STATELESS { - Connection::open_memory(Some("THREAD_FALLBACK_DB")) - } else if cfg!(any(feature = "test-support", test)) { - // rust stores the name of the test on the current thread. - // We use this to automatically create a database that will - // be shared within the test (for the test_retrieve_old_thread) - // but not with concurrent tests. - let thread = std::thread::current(); - let test_name = thread.name(); - Connection::open_memory(Some(&format!( - "THREAD_FALLBACK_{}", - test_name.unwrap_or_default() - ))) - } else { - Connection::open_file(&sqlite_path.to_string_lossy()) - }; - - connection.exec(indoc! {" - CREATE TABLE IF NOT EXISTS threads ( - id TEXT PRIMARY KEY, - summary TEXT NOT NULL, - updated_at TEXT NOT NULL, - data_type TEXT NOT NULL, - data BLOB NOT NULL - ) - "})?() - .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; - - let db = Self { - executor: executor.clone(), - connection: Arc::new(Mutex::new(connection)), - }; - - if needs_migration_from_heed { - let db_connection = db.connection(); - let executor_clone = executor.clone(); - executor - .spawn(async move { - log::info!("Starting threads.db migration"); - Self::migrate_from_heed(&mdb_path, db_connection, executor_clone)?; - fs.remove_dir( - &mdb_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; - log::info!("threads.db migrated to sqlite"); - Ok::<(), anyhow::Error>(()) - }) - .detach(); - } - - Ok(db) - } - - // Remove this migration after 2025-09-01 - fn migrate_from_heed( - mdb_path: &Path, - connection: Arc>, - _executor: BackgroundExecutor, - ) -> Result<()> { - use heed::types::SerdeBincode; - struct SerializedThreadHeed(SerializedThread); - - impl heed::BytesEncode<'_> for SerializedThreadHeed { - type EItem = SerializedThreadHeed; - - fn bytes_encode( - item: &Self::EItem, - ) -> Result, heed::BoxedError> { - serde_json::to_vec(&item.0) - .map(std::borrow::Cow::Owned) - .map_err(Into::into) - } - } - - impl<'a> heed::BytesDecode<'a> for SerializedThreadHeed { - type DItem = SerializedThreadHeed; - - fn bytes_decode(bytes: &'a [u8]) -> Result { - SerializedThread::from_json(bytes) - .map(SerializedThreadHeed) - .map_err(Into::into) - } - } - - const ONE_GB_IN_BYTES: usize = 1024 * 1024 * 1024; - - let env = unsafe { - heed::EnvOpenOptions::new() - .map_size(ONE_GB_IN_BYTES) - .max_dbs(1) - .open(mdb_path)? - }; - - let txn = env.write_txn()?; - let threads: heed::Database, SerializedThreadHeed> = env - .open_database(&txn, Some("threads"))? - .ok_or_else(|| anyhow!("threads database not found"))?; - - for result in threads.iter(&txn)? { - let (thread_id, thread_heed) = result?; - Self::save_thread_sync(&connection, thread_id, thread_heed.0)?; - } - - Ok(()) - } - - fn save_thread_sync( - connection: &Arc>, - id: ThreadId, - thread: SerializedThread, - ) -> Result<()> { - let json_data = serde_json::to_string(&thread)?; - let summary = thread.summary.to_string(); - let updated_at = thread.updated_at.to_rfc3339(); - - let connection = connection.lock().unwrap(); - - let compressed = zstd::encode_all(json_data.as_bytes(), Self::COMPRESSION_LEVEL)?; - let data_type = DataType::Zstd; - let data = compressed; - - let mut insert = connection.exec_bound::<(ThreadId, String, String, DataType, Vec)>(indoc! {" - INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) - "})?; - - insert((id, summary, updated_at, data_type, data))?; - - Ok(()) - } - - pub fn list_threads(&self) -> Task>> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock().unwrap(); - let mut select = - connection.select_bound::<(), (ThreadId, String, String)>(indoc! {" - SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC - "})?; - - let rows = select(())?; - let mut threads = Vec::new(); - - for (id, summary, updated_at) in rows { - threads.push(SerializedThreadMetadata { - id, - summary: summary.into(), - updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc), - }); - } - - Ok(threads) - }) - } - - pub fn try_find_thread(&self, id: ThreadId) -> Task>> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock().unwrap(); - let mut select = connection.select_bound::)>(indoc! {" - SELECT data_type, data FROM threads WHERE id = ? LIMIT 1 - "})?; - - let rows = select(id)?; - if let Some((data_type, data)) = rows.into_iter().next() { - let json_data = match data_type { - DataType::Zstd => { - let decompressed = zstd::decode_all(&data[..])?; - String::from_utf8(decompressed)? - } - DataType::Json => String::from_utf8(data)?, - }; - - let thread = SerializedThread::from_json(json_data.as_bytes())?; - Ok(Some(thread)) - } else { - Ok(None) - } - }) - } - - pub fn save_thread(&self, id: ThreadId, thread: SerializedThread) -> Task> { - let connection = self.connection.clone(); - - self.executor - .spawn(async move { Self::save_thread_sync(&connection, id, thread) }) - } - - pub fn delete_thread(&self, id: ThreadId) -> Task> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock().unwrap(); - - let mut delete = connection.exec_bound::(indoc! {" - DELETE FROM threads WHERE id = ? - "})?; - - delete(id)?; - - Ok(()) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::thread::{DetailedSummaryState, MessageId}; - use chrono::Utc; - use language_model::{Role, TokenUsage}; - use pretty_assertions::assert_eq; - - #[test] - fn test_legacy_serialized_thread_upgrade() { - let updated_at = Utc::now(); - let legacy_thread = LegacySerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![LegacySerializedMessage { - id: MessageId(1), - role: Role::User, - text: "Hello, world!".to_string(), - tool_uses: vec![], - tool_results: vec![], - }], - initial_project_snapshot: None, - }; - - let upgraded = legacy_thread.upgrade(); - - assert_eq!( - upgraded, - SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Hello, world!".to_string() - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false - }], - version: SerializedThread::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - exceeded_window_error: None, - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None - } - ) - } - - #[test] - fn test_serialized_threadv0_1_0_upgrade() { - let updated_at = Utc::now(); - let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![ - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Use tool_1".to_string(), - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - SerializedMessage { - id: MessageId(2), - role: Role::Assistant, - segments: vec![SerializedMessageSegment::Text { - text: "I want to use a tool".to_string(), - }], - tool_uses: vec![SerializedToolUse { - id: "abc".into(), - name: "tool_1".into(), - input: serde_json::Value::Null, - }], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Here is the tool result".to_string(), - }], - tool_uses: vec![], - tool_results: vec![SerializedToolResult { - tool_use_id: "abc".into(), - is_error: false, - content: LanguageModelToolResultContent::Text("abcdef".into()), - output: Some(serde_json::Value::Null), - }], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - ], - version: SerializedThreadV0_1_0::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - exceeded_window_error: None, - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None, - }); - let upgraded = thread_v0_1_0.upgrade(); - - assert_eq!( - upgraded, - SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![ - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Use tool_1".to_string() - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false - }, - SerializedMessage { - id: MessageId(2), - role: Role::Assistant, - segments: vec![SerializedMessageSegment::Text { - text: "I want to use a tool".to_string(), - }], - tool_uses: vec![SerializedToolUse { - id: "abc".into(), - name: "tool_1".into(), - input: serde_json::Value::Null, - }], - tool_results: vec![SerializedToolResult { - tool_use_id: "abc".into(), - is_error: false, - content: LanguageModelToolResultContent::Text("abcdef".into()), - output: Some(serde_json::Value::Null), - }], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - ], - version: SerializedThread::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - exceeded_window_error: None, - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None - } - ) - } -} diff --git a/crates/assistant_tool/src/tool_schema.rs b/crates/agent/src/tool_schema.rs similarity index 85% rename from crates/assistant_tool/src/tool_schema.rs rename to crates/agent/src/tool_schema.rs index 192f7c8a2bb565ece01a3472a9e46dad316377f4..4b0de3e5c63fb0c5ccafbb89a22dad8a33072b35 100644 --- a/crates/assistant_tool/src/tool_schema.rs +++ b/crates/agent/src/tool_schema.rs @@ -1,7 +1,48 @@ use anyhow::Result; +use language_model::LanguageModelToolSchemaFormat; +use schemars::{ + JsonSchema, Schema, + generate::SchemaSettings, + transform::{Transform, transform_subschemas}, +}; use serde_json::Value; -use crate::LanguageModelToolSchemaFormat; +pub(crate) fn root_schema_for(format: LanguageModelToolSchemaFormat) -> Schema { + let mut generator = match format { + LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), + LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() + .with(|settings| { + settings.meta_schema = None; + settings.inline_subschemas = true; + }) + .with_transform(ToJsonSchemaSubsetTransform) + .into_generator(), + }; + generator.root_schema_for::() +} + +#[derive(Debug, Clone)] +struct ToJsonSchemaSubsetTransform; + +impl Transform for ToJsonSchemaSubsetTransform { + fn transform(&mut self, schema: &mut Schema) { + // Ensure that the type field is not an array, this happens when we use + // Option, the type will be [T, "null"]. + if let Some(type_field) = schema.get_mut("type") + && let Some(types) = type_field.as_array() + && let Some(first_type) = types.first() + { + *type_field = first_type.clone(); + } + + // oneOf is not supported, use anyOf instead + if let Some(one_of) = schema.remove("oneOf") { + schema.insert("anyOf".to_string(), one_of); + } + + transform_subschemas(self, schema); + } +} /// Tries to adapt a JSON schema representation to be compatible with the specified format. /// diff --git a/crates/agent/src/tool_use.rs b/crates/agent/src/tool_use.rs deleted file mode 100644 index 962dca591fb66f4679d44b8e8a4733c879bc2e0c..0000000000000000000000000000000000000000 --- a/crates/agent/src/tool_use.rs +++ /dev/null @@ -1,575 +0,0 @@ -use crate::{ - thread::{MessageId, PromptId, ThreadId}, - thread_store::SerializedMessage, -}; -use agent_settings::CompletionMode; -use anyhow::Result; -use assistant_tool::{ - AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet, -}; -use collections::HashMap; -use futures::{FutureExt as _, future::Shared}; -use gpui::{App, Entity, SharedString, Task, Window}; -use icons::IconName; -use language_model::{ - ConfiguredModel, LanguageModel, LanguageModelExt, LanguageModelRequest, - LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolUse, - LanguageModelToolUseId, Role, -}; -use project::Project; -use std::sync::Arc; -use util::truncate_lines_to_byte_limit; - -#[derive(Debug)] -pub struct ToolUse { - pub id: LanguageModelToolUseId, - pub name: SharedString, - pub ui_text: SharedString, - pub status: ToolUseStatus, - pub input: serde_json::Value, - pub icon: icons::IconName, - pub needs_confirmation: bool, -} - -pub struct ToolUseState { - tools: Entity, - tool_uses_by_assistant_message: HashMap>, - tool_results: HashMap, - pending_tool_uses_by_id: HashMap, - tool_result_cards: HashMap, - tool_use_metadata_by_id: HashMap, -} - -impl ToolUseState { - pub fn new(tools: Entity) -> Self { - Self { - tools, - tool_uses_by_assistant_message: HashMap::default(), - tool_results: HashMap::default(), - pending_tool_uses_by_id: HashMap::default(), - tool_result_cards: HashMap::default(), - tool_use_metadata_by_id: HashMap::default(), - } - } - - /// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s. - /// - /// Accepts a function to filter the tools that should be used to populate the state. - /// - /// If `window` is `None` (e.g., when in headless mode or when running evals), - /// tool cards won't be deserialized - pub fn from_serialized_messages( - tools: Entity, - messages: &[SerializedMessage], - project: Entity, - window: Option<&mut Window>, // None in headless mode - cx: &mut App, - ) -> Self { - let mut this = Self::new(tools); - let mut tool_names_by_id = HashMap::default(); - let mut window = window; - - for message in messages { - match message.role { - Role::Assistant => { - if !message.tool_uses.is_empty() { - let tool_uses = message - .tool_uses - .iter() - .map(|tool_use| LanguageModelToolUse { - id: tool_use.id.clone(), - name: tool_use.name.clone().into(), - raw_input: tool_use.input.to_string(), - input: tool_use.input.clone(), - is_input_complete: true, - }) - .collect::>(); - - tool_names_by_id.extend( - tool_uses - .iter() - .map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())), - ); - - this.tool_uses_by_assistant_message - .insert(message.id, tool_uses); - - for tool_result in &message.tool_results { - let tool_use_id = tool_result.tool_use_id.clone(); - let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else { - log::warn!("no tool name found for tool use: {tool_use_id:?}"); - continue; - }; - - this.tool_results.insert( - tool_use_id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use_id.clone(), - tool_name: tool_use.clone(), - is_error: tool_result.is_error, - content: tool_result.content.clone(), - output: tool_result.output.clone(), - }, - ); - - if let Some(window) = &mut window - && let Some(tool) = this.tools.read(cx).tool(tool_use, cx) - && let Some(output) = tool_result.output.clone() - && let Some(card) = - tool.deserialize_card(output, project.clone(), window, cx) - { - this.tool_result_cards.insert(tool_use_id, card); - } - } - } - } - Role::System | Role::User => {} - } - } - - this - } - - pub fn cancel_pending(&mut self) -> Vec { - let mut canceled_tool_uses = Vec::new(); - self.pending_tool_uses_by_id - .retain(|tool_use_id, tool_use| { - if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) { - return true; - } - - let content = "Tool canceled by user".into(); - self.tool_results.insert( - tool_use_id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use_id.clone(), - tool_name: tool_use.name.clone(), - content, - output: None, - is_error: true, - }, - ); - canceled_tool_uses.push(tool_use.clone()); - false - }); - canceled_tool_uses - } - - pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> { - self.pending_tool_uses_by_id.values().collect() - } - - pub fn tool_uses_for_message( - &self, - id: MessageId, - project: &Entity, - cx: &App, - ) -> Vec { - let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else { - return Vec::new(); - }; - - let mut tool_uses = Vec::new(); - - for tool_use in tool_uses_for_message.iter() { - let tool_result = self.tool_results.get(&tool_use.id); - - let status = (|| { - if let Some(tool_result) = tool_result { - let content = tool_result - .content - .to_str() - .map(|str| str.to_owned().into()) - .unwrap_or_default(); - - return if tool_result.is_error { - ToolUseStatus::Error(content) - } else { - ToolUseStatus::Finished(content) - }; - } - - if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) { - match pending_tool_use.status { - PendingToolUseStatus::Idle => ToolUseStatus::Pending, - PendingToolUseStatus::NeedsConfirmation { .. } => { - ToolUseStatus::NeedsConfirmation - } - PendingToolUseStatus::Running { .. } => ToolUseStatus::Running, - PendingToolUseStatus::Error(ref err) => { - ToolUseStatus::Error(err.clone().into()) - } - PendingToolUseStatus::InputStillStreaming => { - ToolUseStatus::InputStillStreaming - } - } - } else { - ToolUseStatus::Pending - } - })(); - - let (icon, needs_confirmation) = - if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) { - ( - tool.icon(), - tool.needs_confirmation(&tool_use.input, project, cx), - ) - } else { - (IconName::Cog, false) - }; - - tool_uses.push(ToolUse { - id: tool_use.id.clone(), - name: tool_use.name.clone().into(), - ui_text: self.tool_ui_label( - &tool_use.name, - &tool_use.input, - tool_use.is_input_complete, - cx, - ), - input: tool_use.input.clone(), - status, - icon, - needs_confirmation, - }) - } - - tool_uses - } - - pub fn tool_ui_label( - &self, - tool_name: &str, - input: &serde_json::Value, - is_input_complete: bool, - cx: &App, - ) -> SharedString { - if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) { - if is_input_complete { - tool.ui_text(input).into() - } else { - tool.still_streaming_ui_text(input).into() - } - } else { - format!("Unknown tool {tool_name:?}").into() - } - } - - pub fn tool_results_for_message( - &self, - assistant_message_id: MessageId, - ) -> Vec<&LanguageModelToolResult> { - let Some(tool_uses) = self - .tool_uses_by_assistant_message - .get(&assistant_message_id) - else { - return Vec::new(); - }; - - tool_uses - .iter() - .filter_map(|tool_use| self.tool_results.get(&tool_use.id)) - .collect() - } - - pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool { - self.tool_uses_by_assistant_message - .get(&assistant_message_id) - .is_some_and(|results| !results.is_empty()) - } - - pub fn tool_result( - &self, - tool_use_id: &LanguageModelToolUseId, - ) -> Option<&LanguageModelToolResult> { - self.tool_results.get(tool_use_id) - } - - pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> { - self.tool_result_cards.get(tool_use_id) - } - - pub fn insert_tool_result_card( - &mut self, - tool_use_id: LanguageModelToolUseId, - card: AnyToolCard, - ) { - self.tool_result_cards.insert(tool_use_id, card); - } - - pub fn request_tool_use( - &mut self, - assistant_message_id: MessageId, - tool_use: LanguageModelToolUse, - metadata: ToolUseMetadata, - cx: &App, - ) -> Arc { - let tool_uses = self - .tool_uses_by_assistant_message - .entry(assistant_message_id) - .or_default(); - - let mut existing_tool_use_found = false; - - for existing_tool_use in tool_uses.iter_mut() { - if existing_tool_use.id == tool_use.id { - *existing_tool_use = tool_use.clone(); - existing_tool_use_found = true; - } - } - - if !existing_tool_use_found { - tool_uses.push(tool_use.clone()); - } - - let status = if tool_use.is_input_complete { - self.tool_use_metadata_by_id - .insert(tool_use.id.clone(), metadata); - - PendingToolUseStatus::Idle - } else { - PendingToolUseStatus::InputStillStreaming - }; - - let ui_text: Arc = self - .tool_ui_label( - &tool_use.name, - &tool_use.input, - tool_use.is_input_complete, - cx, - ) - .into(); - - let may_perform_edits = self - .tools - .read(cx) - .tool(&tool_use.name, cx) - .is_some_and(|tool| tool.may_perform_edits()); - - self.pending_tool_uses_by_id.insert( - tool_use.id.clone(), - PendingToolUse { - assistant_message_id, - id: tool_use.id, - name: tool_use.name.clone(), - ui_text: ui_text.clone(), - input: tool_use.input, - may_perform_edits, - status, - }, - ); - - ui_text - } - - pub fn run_pending_tool( - &mut self, - tool_use_id: LanguageModelToolUseId, - ui_text: SharedString, - task: Task<()>, - ) { - if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) { - tool_use.ui_text = ui_text.into(); - tool_use.status = PendingToolUseStatus::Running { - _task: task.shared(), - }; - } - } - - pub fn confirm_tool_use( - &mut self, - tool_use_id: LanguageModelToolUseId, - ui_text: impl Into>, - input: serde_json::Value, - request: Arc, - tool: Arc, - ) { - if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) { - let ui_text = ui_text.into(); - tool_use.ui_text = ui_text.clone(); - let confirmation = Confirmation { - tool_use_id, - input, - request, - tool, - ui_text, - }; - tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation)); - } - } - - pub fn insert_tool_output( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - output: Result, - configured_model: Option<&ConfiguredModel>, - completion_mode: CompletionMode, - ) -> Option { - let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id); - - telemetry::event!( - "Agent Tool Finished", - model = metadata - .as_ref() - .map(|metadata| metadata.model.telemetry_id()), - model_provider = metadata - .as_ref() - .map(|metadata| metadata.model.provider_id().to_string()), - thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()), - prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()), - tool_name, - success = output.is_ok() - ); - - match output { - Ok(output) => { - let tool_result = output.content; - const BYTES_PER_TOKEN_ESTIMATE: usize = 3; - - let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id); - - // Protect from overly large output - let tool_output_limit = configured_model - .map(|model| { - model.model.max_token_count_for_mode(completion_mode.into()) as usize - * BYTES_PER_TOKEN_ESTIMATE - }) - .unwrap_or(usize::MAX); - - let content = match tool_result { - ToolResultContent::Text(text) => { - let text = if text.len() < tool_output_limit { - text - } else { - let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit); - format!( - "Tool result too long. The first {} bytes:\n\n{}", - truncated.len(), - truncated - ) - }; - LanguageModelToolResultContent::Text(text.into()) - } - ToolResultContent::Image(language_model_image) => { - if language_model_image.estimate_tokens() < tool_output_limit { - LanguageModelToolResultContent::Image(language_model_image) - } else { - self.tool_results.insert( - tool_use_id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use_id.clone(), - tool_name, - content: "Tool responded with an image that would exceeded the remaining tokens".into(), - is_error: true, - output: None, - }, - ); - - return old_use; - } - } - }; - - self.tool_results.insert( - tool_use_id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use_id.clone(), - tool_name, - content, - is_error: false, - output: output.output, - }, - ); - - old_use - } - Err(err) => { - self.tool_results.insert( - tool_use_id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use_id.clone(), - tool_name, - content: LanguageModelToolResultContent::Text(err.to_string().into()), - is_error: true, - output: None, - }, - ); - - if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) { - tool_use.status = PendingToolUseStatus::Error(err.to_string().into()); - } - - self.pending_tool_uses_by_id.get(&tool_use_id).cloned() - } - } - } - - pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool { - self.tool_uses_by_assistant_message - .contains_key(&assistant_message_id) - } - - pub fn tool_results( - &self, - assistant_message_id: MessageId, - ) -> impl Iterator)> { - self.tool_uses_by_assistant_message - .get(&assistant_message_id) - .into_iter() - .flatten() - .map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id))) - } -} - -#[derive(Debug, Clone)] -pub struct PendingToolUse { - pub id: LanguageModelToolUseId, - /// The ID of the Assistant message in which the tool use was requested. - #[allow(unused)] - pub assistant_message_id: MessageId, - pub name: Arc, - pub ui_text: Arc, - pub input: serde_json::Value, - pub status: PendingToolUseStatus, - pub may_perform_edits: bool, -} - -#[derive(Debug, Clone)] -pub struct Confirmation { - pub tool_use_id: LanguageModelToolUseId, - pub input: serde_json::Value, - pub ui_text: Arc, - pub request: Arc, - pub tool: Arc, -} - -#[derive(Debug, Clone)] -pub enum PendingToolUseStatus { - InputStillStreaming, - Idle, - NeedsConfirmation(Arc), - Running { _task: Shared> }, - Error(#[allow(unused)] Arc), -} - -impl PendingToolUseStatus { - pub fn is_idle(&self) -> bool { - matches!(self, PendingToolUseStatus::Idle) - } - - pub fn is_error(&self) -> bool { - matches!(self, PendingToolUseStatus::Error(_)) - } - - pub fn needs_confirmation(&self) -> bool { - matches!(self, PendingToolUseStatus::NeedsConfirmation { .. }) - } -} - -#[derive(Clone)] -pub struct ToolUseMetadata { - pub model: Arc, - pub thread_id: ThreadId, - pub prompt_id: PromptId, -} diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs new file mode 100644 index 0000000000000000000000000000000000000000..831efcad8f154de9aac19d9fd587fafb345d1aad --- /dev/null +++ b/crates/agent/src/tools.rs @@ -0,0 +1,88 @@ +mod context_server_registry; +mod copy_path_tool; +mod create_directory_tool; +mod delete_path_tool; +mod diagnostics_tool; +mod edit_file_tool; +mod fetch_tool; +mod find_path_tool; +mod grep_tool; +mod list_directory_tool; +mod move_path_tool; +mod now_tool; +mod open_tool; +mod read_file_tool; +mod terminal_tool; +mod thinking_tool; +mod web_search_tool; + +use crate::AgentTool; +use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat}; + +pub use context_server_registry::*; +pub use copy_path_tool::*; +pub use create_directory_tool::*; +pub use delete_path_tool::*; +pub use diagnostics_tool::*; +pub use edit_file_tool::*; +pub use fetch_tool::*; +pub use find_path_tool::*; +pub use grep_tool::*; +pub use list_directory_tool::*; +pub use move_path_tool::*; +pub use now_tool::*; +pub use open_tool::*; +pub use read_file_tool::*; +pub use terminal_tool::*; +pub use thinking_tool::*; +pub use web_search_tool::*; + +macro_rules! tools { + ($($tool:ty),* $(,)?) => { + /// A list of all built-in tool names + pub fn built_in_tool_names() -> impl Iterator { + [ + $( + <$tool>::name().to_string(), + )* + ] + .into_iter() + } + + /// A list of all built-in tools + pub fn built_in_tools() -> impl Iterator { + fn language_model_tool() -> LanguageModelRequestTool { + LanguageModelRequestTool { + name: T::name().to_string(), + description: T::description().to_string(), + input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(), + } + } + [ + $( + language_model_tool::<$tool>(), + )* + ] + .into_iter() + } + }; +} + +tools! { + CopyPathTool, + CreateDirectoryTool, + DeletePathTool, + DiagnosticsTool, + EditFileTool, + FetchTool, + FindPathTool, + GrepTool, + ListDirectoryTool, + MovePathTool, + NowTool, + OpenTool, + ReadFileTool, + TerminalTool, + ThinkingTool, + WebSearchTool, +} diff --git a/crates/agent2/src/tools/context_server_registry.rs b/crates/agent/src/tools/context_server_registry.rs similarity index 95% rename from crates/agent2/src/tools/context_server_registry.rs rename to crates/agent/src/tools/context_server_registry.rs index 46fa0298044de017464dc1a2e5bd21bf57c1bfcf..382d2ba9be74b4518de853037c858fd054366d5d 100644 --- a/crates/agent2/src/tools/context_server_registry.rs +++ b/crates/agent/src/tools/context_server_registry.rs @@ -32,6 +32,17 @@ impl ContextServerRegistry { this } + pub fn tools_for_server( + &self, + server_id: &ContextServerId, + ) -> impl Iterator> { + self.registered_servers + .get(server_id) + .map(|server| server.tools.values()) + .into_iter() + .flatten() + } + pub fn servers( &self, ) -> impl Iterator< @@ -154,7 +165,7 @@ impl AnyAgentTool for ContextServerTool { format: language_model::LanguageModelToolSchemaFormat, ) -> Result { let mut schema = self.tool.input_schema.clone(); - assistant_tool::adapt_schema_to_format(&mut schema, format)?; + crate::tool_schema::adapt_schema_to_format(&mut schema, format)?; Ok(match schema { serde_json::Value::Null => { serde_json::json!({ "type": "object", "properties": [] }) diff --git a/crates/agent2/src/tools/copy_path_tool.rs b/crates/agent/src/tools/copy_path_tool.rs similarity index 100% rename from crates/agent2/src/tools/copy_path_tool.rs rename to crates/agent/src/tools/copy_path_tool.rs diff --git a/crates/agent2/src/tools/create_directory_tool.rs b/crates/agent/src/tools/create_directory_tool.rs similarity index 100% rename from crates/agent2/src/tools/create_directory_tool.rs rename to crates/agent/src/tools/create_directory_tool.rs diff --git a/crates/agent2/src/tools/delete_path_tool.rs b/crates/agent/src/tools/delete_path_tool.rs similarity index 100% rename from crates/agent2/src/tools/delete_path_tool.rs rename to crates/agent/src/tools/delete_path_tool.rs diff --git a/crates/agent2/src/tools/diagnostics_tool.rs b/crates/agent/src/tools/diagnostics_tool.rs similarity index 100% rename from crates/agent2/src/tools/diagnostics_tool.rs rename to crates/agent/src/tools/diagnostics_tool.rs diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs similarity index 98% rename from crates/agent2/src/tools/edit_file_tool.rs rename to crates/agent/src/tools/edit_file_tool.rs index 90bb68979439b92eef685a500a81147fde8099d6..0adff2dee3571f09b40ee69896c05e50c56b51b9 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -1,8 +1,10 @@ -use crate::{AgentTool, Thread, ToolCallEventStream}; +use crate::{ + AgentTool, Templates, Thread, ToolCallEventStream, + edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, +}; use acp_thread::Diff; use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; -use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}; use cloud_llm_client::CompletionIntent; use collections::HashSet; use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; @@ -34,7 +36,7 @@ const DEFAULT_UI_TEXT: &str = "Editing file"; /// /// 2. Verify the directory path is correct (only applicable when creating new files): /// - Use the `list_directory` tool to verify the parent directory exists and is the correct location -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct EditFileToolInput { /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. /// @@ -75,7 +77,7 @@ pub struct EditFileToolInput { pub mode: EditFileMode, } -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] struct EditFileToolPartialInput { #[serde(default)] path: String, @@ -123,6 +125,7 @@ pub struct EditFileTool { thread: WeakEntity, language_registry: Arc, project: Entity, + templates: Arc, } impl EditFileTool { @@ -130,11 +133,13 @@ impl EditFileTool { project: Entity, thread: WeakEntity, language_registry: Arc, + templates: Arc, ) -> Self { Self { project, thread, language_registry, + templates, } } @@ -294,8 +299,7 @@ impl AgentTool for EditFileTool { model, project.clone(), action_log.clone(), - // TODO: move edit agent to this crate so we can use our templates - assistant_tools::templates::Templates::new(), + self.templates.clone(), edit_format, ); @@ -599,6 +603,7 @@ mod tests { project, thread.downgrade(), language_registry, + Templates::new(), )) .run(input, ToolCallEventStream::test().0, cx) }) @@ -807,6 +812,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry.clone(), + Templates::new(), )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -865,6 +871,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -951,6 +958,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry.clone(), + Templates::new(), )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -1005,6 +1013,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )) .run(input, ToolCallEventStream::test().0, cx) }); @@ -1057,6 +1066,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )); fs.insert_tree("/root", json!({})).await; @@ -1197,6 +1207,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )); // Test global config paths - these should require confirmation if they exist and are outside the project @@ -1309,6 +1320,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )); // Test files in different worktrees @@ -1393,6 +1405,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )); // Test edge cases @@ -1482,6 +1495,7 @@ mod tests { project.clone(), thread.downgrade(), language_registry, + Templates::new(), )); // Test different EditFileMode values @@ -1566,6 +1580,7 @@ mod tests { project, thread.downgrade(), language_registry, + Templates::new(), )); cx.update(|cx| { @@ -1653,6 +1668,7 @@ mod tests { project.clone(), thread.downgrade(), languages.clone(), + Templates::new(), )); let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let edit = cx.update(|cx| { @@ -1682,6 +1698,7 @@ mod tests { project.clone(), thread.downgrade(), languages.clone(), + Templates::new(), )); let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let edit = cx.update(|cx| { @@ -1709,6 +1726,7 @@ mod tests { project.clone(), thread.downgrade(), languages.clone(), + Templates::new(), )); let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); let edit = cx.update(|cx| { diff --git a/crates/agent2/src/tools/fetch_tool.rs b/crates/agent/src/tools/fetch_tool.rs similarity index 100% rename from crates/agent2/src/tools/fetch_tool.rs rename to crates/agent/src/tools/fetch_tool.rs diff --git a/crates/agent2/src/tools/find_path_tool.rs b/crates/agent/src/tools/find_path_tool.rs similarity index 100% rename from crates/agent2/src/tools/find_path_tool.rs rename to crates/agent/src/tools/find_path_tool.rs diff --git a/crates/agent2/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs similarity index 100% rename from crates/agent2/src/tools/grep_tool.rs rename to crates/agent/src/tools/grep_tool.rs diff --git a/crates/agent2/src/tools/list_directory_tool.rs b/crates/agent/src/tools/list_directory_tool.rs similarity index 100% rename from crates/agent2/src/tools/list_directory_tool.rs rename to crates/agent/src/tools/list_directory_tool.rs diff --git a/crates/agent2/src/tools/move_path_tool.rs b/crates/agent/src/tools/move_path_tool.rs similarity index 100% rename from crates/agent2/src/tools/move_path_tool.rs rename to crates/agent/src/tools/move_path_tool.rs diff --git a/crates/agent2/src/tools/now_tool.rs b/crates/agent/src/tools/now_tool.rs similarity index 100% rename from crates/agent2/src/tools/now_tool.rs rename to crates/agent/src/tools/now_tool.rs diff --git a/crates/agent2/src/tools/open_tool.rs b/crates/agent/src/tools/open_tool.rs similarity index 100% rename from crates/agent2/src/tools/open_tool.rs rename to crates/agent/src/tools/open_tool.rs diff --git a/crates/agent2/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs similarity index 99% rename from crates/agent2/src/tools/read_file_tool.rs rename to crates/agent/src/tools/read_file_tool.rs index ce8dcba10236aa194e8b30d3fe6855d8c5fa5148..f3ce8e35f2856a3dd53770eef48ec1091fe9b116 100644 --- a/crates/agent2/src/tools/read_file_tool.rs +++ b/crates/agent/src/tools/read_file_tool.rs @@ -1,7 +1,6 @@ use action_log::ActionLog; use agent_client_protocol::{self as acp, ToolCallUpdateFields}; use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::outline; use gpui::{App, Entity, SharedString, Task}; use indoc::formatdoc; use language::Point; @@ -13,7 +12,7 @@ use settings::Settings; use std::sync::Arc; use util::markdown::MarkdownCodeBlock; -use crate::{AgentTool, ToolCallEventStream}; +use crate::{AgentTool, ToolCallEventStream, outline}; /// Reads the content of the given file in the project. /// diff --git a/crates/agent2/src/tools/terminal_tool.rs b/crates/agent/src/tools/terminal_tool.rs similarity index 100% rename from crates/agent2/src/tools/terminal_tool.rs rename to crates/agent/src/tools/terminal_tool.rs diff --git a/crates/agent2/src/tools/thinking_tool.rs b/crates/agent/src/tools/thinking_tool.rs similarity index 100% rename from crates/agent2/src/tools/thinking_tool.rs rename to crates/agent/src/tools/thinking_tool.rs diff --git a/crates/agent2/src/tools/web_search_tool.rs b/crates/agent/src/tools/web_search_tool.rs similarity index 100% rename from crates/agent2/src/tools/web_search_tool.rs rename to crates/agent/src/tools/web_search_tool.rs diff --git a/crates/agent2/Cargo.toml b/crates/agent2/Cargo.toml deleted file mode 100644 index b712bed258dfb69ddf81a1ba431ec7a3566b9baf..0000000000000000000000000000000000000000 --- a/crates/agent2/Cargo.toml +++ /dev/null @@ -1,102 +0,0 @@ -[package] -name = "agent2" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/agent2.rs" - -[features] -test-support = ["db/test-support"] -e2e = [] - -[lints] -workspace = true - -[dependencies] -acp_thread.workspace = true -action_log.workspace = true -agent.workspace = true -agent-client-protocol.workspace = true -agent_servers.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -assistant_context.workspace = true -assistant_tool.workspace = true -assistant_tools.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -context_server.workspace = true -db.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -gpui.workspace = true -handlebars = { workspace = true, features = ["rust-embed"] } -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -open.workspace = true -parking_lot.workspace = true -paths.workspace = true -project.workspace = true -prompt_store.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -sqlez.workspace = true -task.workspace = true -telemetry.workspace = true -terminal.workspace = true -thiserror.workspace = true -text.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true -web_search.workspace = true -workspace-hack.workspace = true -zed_env_vars.workspace = true -zstd.workspace = true - -[dev-dependencies] -agent = { workspace = true, "features" = ["test-support"] } -agent_servers = { workspace = true, "features" = ["test-support"] } -assistant_context = { workspace = true, "features" = ["test-support"] } -ctor.workspace = true -client = { workspace = true, "features" = ["test-support"] } -clock = { workspace = true, "features" = ["test-support"] } -context_server = { workspace = true, "features" = ["test-support"] } -db = { workspace = true, "features" = ["test-support"] } -editor = { workspace = true, "features" = ["test-support"] } -env_logger.workspace = true -fs = { workspace = true, "features" = ["test-support"] } -git = { workspace = true, "features" = ["test-support"] } -gpui = { workspace = true, "features" = ["test-support"] } -gpui_tokio.workspace = true -language = { workspace = true, "features" = ["test-support"] } -language_model = { workspace = true, "features" = ["test-support"] } -lsp = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, "features" = ["test-support"] } -reqwest_client.workspace = true -settings = { workspace = true, "features" = ["test-support"] } -tempfile.workspace = true -terminal = { workspace = true, "features" = ["test-support"] } -theme = { workspace = true, "features" = ["test-support"] } -tree-sitter-rust.workspace = true -unindent = { workspace = true } -worktree = { workspace = true, "features" = ["test-support"] } -zlog.workspace = true diff --git a/crates/agent2/LICENSE-GPL b/crates/agent2/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/agent2/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs deleted file mode 100644 index bf1fe8b5bb72038e197eafc842ca02e417b9e7c3..0000000000000000000000000000000000000000 --- a/crates/agent2/src/agent.rs +++ /dev/null @@ -1,1588 +0,0 @@ -use crate::{ - ContextServerRegistry, Thread, ThreadEvent, ThreadsDatabase, ToolCallAuthorization, - UserMessageContent, templates::Templates, -}; -use crate::{HistoryStore, TerminalHandle, ThreadEnvironment, TitleUpdated, TokenUsageUpdated}; -use acp_thread::{AcpThread, AgentModelSelector}; -use action_log::ActionLog; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashSet, IndexMap}; -use fs::Fs; -use futures::channel::{mpsc, oneshot}; -use futures::future::Shared; -use futures::{StreamExt, future}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, -}; -use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry}; -use project::{Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{ - ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, -}; -use settings::{LanguageModelSelection, update_settings_file}; -use std::any::Any; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::sync::Arc; -use util::ResultExt; -use util::rel_path::RelPath; - -const RULES_FILE_NAMES: [&str; 9] = [ - ".rules", - ".cursorrules", - ".windsurfrules", - ".clinerules", - ".github/copilot-instructions.md", - "CLAUDE.md", - "AGENT.md", - "AGENTS.md", - "GEMINI.md", -]; - -pub struct RulesLoadingError { - pub message: SharedString, -} - -/// Holds both the internal Thread and the AcpThread for a session -struct Session { - /// The internal thread that processes messages - thread: Entity, - /// The ACP thread that handles protocol communication - acp_thread: WeakEntity, - pending_save: Task<()>, - _subscriptions: Vec, -} - -pub struct LanguageModels { - /// Access language model by ID - models: HashMap>, - /// Cached list for returning language model information - model_list: acp_thread::AgentModelList, - refresh_models_rx: watch::Receiver<()>, - refresh_models_tx: watch::Sender<()>, - _authenticate_all_providers_task: Task<()>, -} - -impl LanguageModels { - fn new(cx: &mut App) -> Self { - let (refresh_models_tx, refresh_models_rx) = watch::channel(()); - - let mut this = Self { - models: HashMap::default(), - model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()), - refresh_models_rx, - refresh_models_tx, - _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx), - }; - this.refresh_list(cx); - this - } - - fn refresh_list(&mut self, cx: &App) { - let providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .into_iter() - .filter(|provider| provider.is_authenticated(cx)) - .collect::>(); - - let mut language_model_list = IndexMap::default(); - let mut recommended_models = HashSet::default(); - - let mut recommended = Vec::new(); - for provider in &providers { - for model in provider.recommended_models(cx) { - recommended_models.insert((model.provider_id(), model.id())); - recommended.push(Self::map_language_model_to_info(&model, provider)); - } - } - if !recommended.is_empty() { - language_model_list.insert( - acp_thread::AgentModelGroupName("Recommended".into()), - recommended, - ); - } - - let mut models = HashMap::default(); - for provider in providers { - let mut provider_models = Vec::new(); - for model in provider.provided_models(cx) { - let model_info = Self::map_language_model_to_info(&model, &provider); - let model_id = model_info.id.clone(); - if !recommended_models.contains(&(model.provider_id(), model.id())) { - provider_models.push(model_info); - } - models.insert(model_id, model); - } - if !provider_models.is_empty() { - language_model_list.insert( - acp_thread::AgentModelGroupName(provider.name().0.clone()), - provider_models, - ); - } - } - - self.models = models; - self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); - self.refresh_models_tx.send(()).ok(); - } - - fn watch(&self) -> watch::Receiver<()> { - self.refresh_models_rx.clone() - } - - pub fn model_from_id(&self, model_id: &acp::ModelId) -> Option> { - self.models.get(model_id).cloned() - } - - fn map_language_model_to_info( - model: &Arc, - provider: &Arc, - ) -> acp_thread::AgentModelInfo { - acp_thread::AgentModelInfo { - id: Self::model_id(model), - name: model.name().0, - description: None, - icon: Some(provider.icon()), - } - } - - fn model_id(model: &Arc) -> acp::ModelId { - acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into()) - } - - fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.background_spawn(async move { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - match err { - language_model::AuthenticateError::CredentialsNotFound => { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } - language_model::AuthenticateError::ConnectionRefused => { - // Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures. - // LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it. - // TODO: Better manage LM Studio auth logic to avoid these noisy failures. - } - _ => { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - } - } - }) - } -} - -pub struct NativeAgent { - /// Session ID -> Session mapping - sessions: HashMap, - history: Entity, - /// Shared project context for all threads - project_context: Entity, - project_context_needs_refresh: watch::Sender<()>, - _maintain_project_context: Task>, - context_server_registry: Entity, - /// Shared templates for all threads - templates: Arc, - /// Cached model information - models: LanguageModels, - project: Entity, - prompt_store: Option>, - fs: Arc, - _subscriptions: Vec, -} - -impl NativeAgent { - pub async fn new( - project: Entity, - history: Entity, - templates: Arc, - prompt_store: Option>, - fs: Arc, - cx: &mut AsyncApp, - ) -> Result> { - log::debug!("Creating new NativeAgent"); - - let project_context = cx - .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? - .await; - - cx.new(|cx| { - let mut subscriptions = vec![ - cx.subscribe(&project, Self::handle_project_event), - cx.subscribe( - &LanguageModelRegistry::global(cx), - Self::handle_models_updated_event, - ), - ]; - if let Some(prompt_store) = prompt_store.as_ref() { - subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) - } - - let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) = - watch::channel(()); - Self { - sessions: HashMap::new(), - history, - project_context: cx.new(|_| project_context), - project_context_needs_refresh: project_context_needs_refresh_tx, - _maintain_project_context: cx.spawn(async move |this, cx| { - Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await - }), - context_server_registry: cx.new(|cx| { - ContextServerRegistry::new(project.read(cx).context_server_store(), cx) - }), - templates, - models: LanguageModels::new(cx), - project, - prompt_store, - fs, - _subscriptions: subscriptions, - } - }) - } - - fn register_session( - &mut self, - thread_handle: Entity, - cx: &mut Context, - ) -> Entity { - let connection = Rc::new(NativeAgentConnection(cx.entity())); - - let thread = thread_handle.read(cx); - let session_id = thread.id().clone(); - let title = thread.title(); - let project = thread.project.clone(); - let action_log = thread.action_log.clone(); - let prompt_capabilities_rx = thread.prompt_capabilities_rx.clone(); - let acp_thread = cx.new(|cx| { - acp_thread::AcpThread::new( - title, - connection, - project.clone(), - action_log.clone(), - session_id.clone(), - prompt_capabilities_rx, - cx, - ) - }); - - let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); - - thread_handle.update(cx, |thread, cx| { - thread.set_summarization_model(summarization_model, cx); - thread.add_default_tools( - Rc::new(AcpThreadEnvironment { - acp_thread: acp_thread.downgrade(), - }) as _, - cx, - ) - }); - - let subscriptions = vec![ - cx.observe_release(&acp_thread, |this, acp_thread, _cx| { - this.sessions.remove(acp_thread.session_id()); - }), - cx.subscribe(&thread_handle, Self::handle_thread_title_updated), - cx.subscribe(&thread_handle, Self::handle_thread_token_usage_updated), - cx.observe(&thread_handle, move |this, thread, cx| { - this.save_thread(thread, cx) - }), - ]; - - self.sessions.insert( - session_id, - Session { - thread: thread_handle, - acp_thread: acp_thread.downgrade(), - _subscriptions: subscriptions, - pending_save: Task::ready(()), - }, - ); - acp_thread - } - - pub fn models(&self) -> &LanguageModels { - &self.models - } - - async fn maintain_project_context( - this: WeakEntity, - mut needs_refresh: watch::Receiver<()>, - cx: &mut AsyncApp, - ) -> Result<()> { - while needs_refresh.changed().await.is_ok() { - let project_context = this - .update(cx, |this, cx| { - Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) - })? - .await; - this.update(cx, |this, cx| { - this.project_context = cx.new(|_| project_context); - })?; - } - - Ok(()) - } - - fn build_project_context( - project: &Entity, - prompt_store: Option<&Entity>, - cx: &mut App, - ) -> Task { - let worktrees = project.read(cx).visible_worktrees(cx).collect::>(); - let worktree_tasks = worktrees - .into_iter() - .map(|worktree| { - Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx) - }) - .collect::>(); - let default_user_rules_task = if let Some(prompt_store) = prompt_store.as_ref() { - prompt_store.read_with(cx, |prompt_store, cx| { - let prompts = prompt_store.default_prompt_metadata(); - let load_tasks = prompts.into_iter().map(|prompt_metadata| { - let contents = prompt_store.load(prompt_metadata.id, cx); - async move { (contents.await, prompt_metadata) } - }); - cx.background_spawn(future::join_all(load_tasks)) - }) - } else { - Task::ready(vec![]) - }; - - cx.spawn(async move |_cx| { - let (worktrees, default_user_rules) = - future::join(future::join_all(worktree_tasks), default_user_rules_task).await; - - let worktrees = worktrees - .into_iter() - .map(|(worktree, _rules_error)| { - // TODO: show error message - // if let Some(rules_error) = rules_error { - // this.update(cx, |_, cx| cx.emit(rules_error)).ok(); - // } - worktree - }) - .collect::>(); - - let default_user_rules = default_user_rules - .into_iter() - .flat_map(|(contents, prompt_metadata)| match contents { - Ok(contents) => Some(UserRulesContext { - uuid: match prompt_metadata.id { - PromptId::User { uuid } => uuid, - PromptId::EditWorkflow => return None, - }, - title: prompt_metadata.title.map(|title| title.to_string()), - contents, - }), - Err(_err) => { - // TODO: show error message - // this.update(cx, |_, cx| { - // cx.emit(RulesLoadingError { - // message: format!("{err:?}").into(), - // }); - // }) - // .ok(); - None - } - }) - .collect::>(); - - ProjectContext::new(worktrees, default_user_rules) - }) - } - - fn load_worktree_info_for_system_prompt( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Task<(WorktreeContext, Option)> { - let tree = worktree.read(cx); - let root_name = tree.root_name_str().into(); - let abs_path = tree.abs_path(); - - let mut context = WorktreeContext { - root_name, - abs_path, - rules_file: None, - }; - - let rules_task = Self::load_worktree_rules_file(worktree, project, cx); - let Some(rules_task) = rules_task else { - return Task::ready((context, None)); - }; - - cx.spawn(async move |_| { - let (rules_file, rules_file_error) = match rules_task.await { - Ok(rules_file) => (Some(rules_file), None), - Err(err) => ( - None, - Some(RulesLoadingError { - message: format!("{err}").into(), - }), - ), - }; - context.rules_file = rules_file; - (context, rules_file_error) - }) - } - - fn load_worktree_rules_file( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Option>> { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let selected_rules_file = RULES_FILE_NAMES - .into_iter() - .filter_map(|name| { - worktree - .entry_for_path(RelPath::unix(name).unwrap()) - .filter(|entry| entry.is_file()) - .map(|entry| entry.path.clone()) - }) - .next(); - - // Note that Cline supports `.clinerules` being a directory, but that is not currently - // supported. This doesn't seem to occur often in GitHub repositories. - selected_rules_file.map(|path_in_worktree| { - let project_path = ProjectPath { - worktree_id, - path: path_in_worktree.clone(), - }; - let buffer_task = - project.update(cx, |project, cx| project.open_buffer(project_path, cx)); - let rope_task = cx.spawn(async move |cx| { - buffer_task.await?.read_with(cx, |buffer, cx| { - let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; - anyhow::Ok((project_entry_id, buffer.as_rope().clone())) - })? - }); - // Build a string from the rope on a background thread. - cx.background_spawn(async move { - let (project_entry_id, rope) = rope_task.await?; - anyhow::Ok(RulesFileContext { - path_in_worktree, - text: rope.to_string().trim().to_string(), - project_entry_id: project_entry_id.to_usize(), - }) - }) - }) - } - - fn handle_thread_title_updated( - &mut self, - thread: Entity, - _: &TitleUpdated, - cx: &mut Context, - ) { - let session_id = thread.read(cx).id(); - let Some(session) = self.sessions.get(session_id) else { - return; - }; - let thread = thread.downgrade(); - let acp_thread = session.acp_thread.clone(); - cx.spawn(async move |_, cx| { - let title = thread.read_with(cx, |thread, _| thread.title())?; - let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; - task.await - }) - .detach_and_log_err(cx); - } - - fn handle_thread_token_usage_updated( - &mut self, - thread: Entity, - usage: &TokenUsageUpdated, - cx: &mut Context, - ) { - let Some(session) = self.sessions.get(thread.read(cx).id()) else { - return; - }; - session - .acp_thread - .update(cx, |acp_thread, cx| { - acp_thread.update_token_usage(usage.0.clone(), cx); - }) - .ok(); - } - - fn handle_project_event( - &mut self, - _project: Entity, - event: &project::Event, - _cx: &mut Context, - ) { - match event { - project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { - self.project_context_needs_refresh.send(()).ok(); - } - project::Event::WorktreeUpdatedEntries(_, items) => { - if items.iter().any(|(path, _, _)| { - RULES_FILE_NAMES - .iter() - .any(|name| path.as_ref() == RelPath::unix(name).unwrap()) - }) { - self.project_context_needs_refresh.send(()).ok(); - } - } - _ => {} - } - } - - fn handle_prompts_updated_event( - &mut self, - _prompt_store: Entity, - _event: &prompt_store::PromptsUpdatedEvent, - _cx: &mut Context, - ) { - self.project_context_needs_refresh.send(()).ok(); - } - - fn handle_models_updated_event( - &mut self, - _registry: Entity, - _event: &language_model::Event, - cx: &mut Context, - ) { - self.models.refresh_list(cx); - - let registry = LanguageModelRegistry::read_global(cx); - let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model().map(|m| m.model); - - for session in self.sessions.values_mut() { - session.thread.update(cx, |thread, cx| { - if thread.model().is_none() - && let Some(model) = default_model.clone() - { - thread.set_model(model, cx); - cx.notify(); - } - thread.set_summarization_model(summarization_model.clone(), cx); - }); - } - } - - pub fn open_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task>> { - let database_future = ThreadsDatabase::connect(cx); - cx.spawn(async move |this, cx| { - let database = database_future.await.map_err(|err| anyhow!(err))?; - let db_thread = database - .load_thread(id.clone()) - .await? - .with_context(|| format!("no thread found with ID: {id:?}"))?; - - let thread = this.update(cx, |this, cx| { - let action_log = cx.new(|_cx| ActionLog::new(this.project.clone())); - cx.new(|cx| { - Thread::from_db( - id.clone(), - db_thread, - this.project.clone(), - this.project_context.clone(), - this.context_server_registry.clone(), - action_log.clone(), - this.templates.clone(), - cx, - ) - }) - })?; - let acp_thread = - this.update(cx, |this, cx| this.register_session(thread.clone(), cx))?; - let events = thread.update(cx, |thread, cx| thread.replay(cx))?; - cx.update(|cx| { - NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx) - })? - .await?; - Ok(acp_thread) - }) - } - - pub fn thread_summary( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task> { - let thread = self.open_thread(id.clone(), cx); - cx.spawn(async move |this, cx| { - let acp_thread = thread.await?; - let result = this - .update(cx, |this, cx| { - this.sessions - .get(&id) - .unwrap() - .thread - .update(cx, |thread, cx| thread.summary(cx)) - })? - .await?; - drop(acp_thread); - Ok(result) - }) - } - - fn save_thread(&mut self, thread: Entity, cx: &mut Context) { - if thread.read(cx).is_empty() { - return; - } - - let database_future = ThreadsDatabase::connect(cx); - let (id, db_thread) = - thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); - let Some(session) = self.sessions.get_mut(&id) else { - return; - }; - let history = self.history.clone(); - session.pending_save = cx.spawn(async move |_, cx| { - let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { - return; - }; - let db_thread = db_thread.await; - database.save_thread(id, db_thread).await.log_err(); - history.update(cx, |history, cx| history.reload(cx)).ok(); - }); - } -} - -/// Wrapper struct that implements the AgentConnection trait -#[derive(Clone)] -pub struct NativeAgentConnection(pub Entity); - -impl NativeAgentConnection { - pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option> { - self.0 - .read(cx) - .sessions - .get(session_id) - .map(|session| session.thread.clone()) - } - - fn run_turn( - &self, - session_id: acp::SessionId, - cx: &mut App, - f: impl 'static - + FnOnce(Entity, &mut App) -> Result>>, - ) -> Task> { - let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { - agent - .sessions - .get_mut(&session_id) - .map(|s| (s.thread.clone(), s.acp_thread.clone())) - }) else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - log::debug!("Found session for: {}", session_id); - - let response_stream = match f(thread, cx) { - Ok(stream) => stream, - Err(err) => return Task::ready(Err(err)), - }; - Self::handle_thread_events(response_stream, acp_thread, cx) - } - - fn handle_thread_events( - mut events: mpsc::UnboundedReceiver>, - acp_thread: WeakEntity, - cx: &App, - ) -> Task> { - cx.spawn(async move |cx| { - // Handle response stream and forward to session.acp_thread - while let Some(result) = events.next().await { - match result { - Ok(event) => { - log::trace!("Received completion event: {:?}", event); - - match event { - ThreadEvent::UserMessage(message) => { - acp_thread.update(cx, |thread, cx| { - for content in message.content { - thread.push_user_content_block( - Some(message.id.clone()), - content.into(), - cx, - ); - } - })?; - } - ThreadEvent::AgentText(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block( - acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - meta: None, - }), - false, - cx, - ) - })?; - } - ThreadEvent::AgentThinking(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block( - acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - meta: None, - }), - true, - cx, - ) - })?; - } - ThreadEvent::ToolCallAuthorization(ToolCallAuthorization { - tool_call, - options, - response, - }) => { - let outcome_task = acp_thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call, options, true, cx, - ) - })??; - cx.background_spawn(async move { - if let acp::RequestPermissionOutcome::Selected { option_id } = - outcome_task.await - { - response - .send(option_id) - .map(|_| anyhow!("authorization receiver was dropped")) - .log_err(); - } - }) - .detach(); - } - ThreadEvent::ToolCall(tool_call) => { - acp_thread.update(cx, |thread, cx| { - thread.upsert_tool_call(tool_call, cx) - })??; - } - ThreadEvent::ToolCallUpdate(update) => { - acp_thread.update(cx, |thread, cx| { - thread.update_tool_call(update, cx) - })??; - } - ThreadEvent::Retry(status) => { - acp_thread.update(cx, |thread, cx| { - thread.update_retry_status(status, cx) - })?; - } - ThreadEvent::Stop(stop_reason) => { - log::debug!("Assistant message complete: {:?}", stop_reason); - return Ok(acp::PromptResponse { - stop_reason, - meta: None, - }); - } - } - } - Err(e) => { - log::error!("Error in model response stream: {:?}", e); - return Err(e); - } - } - } - - log::debug!("Response stream completed"); - anyhow::Ok(acp::PromptResponse { - stop_reason: acp::StopReason::EndTurn, - meta: None, - }) - }) - } -} - -struct NativeAgentModelSelector { - session_id: acp::SessionId, - connection: NativeAgentConnection, -} - -impl acp_thread::AgentModelSelector for NativeAgentModelSelector { - fn list_models(&self, cx: &mut App) -> Task> { - log::debug!("NativeAgentConnection::list_models called"); - let list = self.connection.0.read(cx).models.model_list.clone(); - Task::ready(if list.is_empty() { - Err(anyhow::anyhow!("No models available")) - } else { - Ok(list) - }) - } - - fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task> { - log::debug!( - "Setting model for session {}: {}", - self.session_id, - model_id - ); - let Some(thread) = self - .connection - .0 - .read(cx) - .sessions - .get(&self.session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let Some(model) = self.connection.0.read(cx).models.model_from_id(&model_id) else { - return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); - }; - - thread.update(cx, |thread, cx| { - thread.set_model(model.clone(), cx); - }); - - update_settings_file( - self.connection.0.read(cx).fs.clone(), - cx, - move |settings, _cx| { - let provider = model.provider_id().0.to_string(); - let model = model.id().0.to_string(); - settings - .agent - .get_or_insert_default() - .set_model(LanguageModelSelection { - provider: provider.into(), - model, - }); - }, - ); - - Task::ready(Ok(())) - } - - fn selected_model(&self, cx: &mut App) -> Task> { - let Some(thread) = self - .connection - .0 - .read(cx) - .sessions - .get(&self.session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - let Some(model) = thread.read(cx).model() else { - return Task::ready(Err(anyhow!("Model not found"))); - }; - let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) - else { - return Task::ready(Err(anyhow!("Provider not found"))); - }; - Task::ready(Ok(LanguageModels::map_language_model_to_info( - model, &provider, - ))) - } - - fn watch(&self, cx: &mut App) -> Option> { - Some(self.connection.0.read(cx).models.watch()) - } -} - -impl acp_thread::AgentConnection for NativeAgentConnection { - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let agent = self.0.clone(); - log::debug!("Creating new thread for project at: {:?}", cwd); - - cx.spawn(async move |cx| { - log::debug!("Starting thread creation in async context"); - - // Create Thread - let thread = agent.update( - cx, - |agent, cx: &mut gpui::Context| -> Result<_> { - // Fetch default model from registry settings - let registry = LanguageModelRegistry::read_global(cx); - // Log available models for debugging - let available_count = registry.available_models(cx).count(); - log::debug!("Total available models: {}", available_count); - - let default_model = registry.default_model().and_then(|default_model| { - agent - .models - .model_from_id(&LanguageModels::model_id(&default_model.model)) - }); - Ok(cx.new(|cx| { - Thread::new( - project.clone(), - agent.project_context.clone(), - agent.context_server_registry.clone(), - agent.templates.clone(), - default_model, - cx, - ) - })) - }, - )??; - agent.update(cx, |agent, cx| agent.register_session(thread, cx)) - }) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] // No auth for in-process - } - - fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task> { - Task::ready(Ok(())) - } - - fn model_selector(&self, session_id: &acp::SessionId) -> Option> { - Some(Rc::new(NativeAgentModelSelector { - session_id: session_id.clone(), - connection: self.clone(), - }) as Rc) - } - - fn prompt( - &self, - id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let id = id.expect("UserMessageId is required"); - let session_id = params.session_id.clone(); - log::info!("Received prompt request for session: {}", session_id); - log::debug!("Prompt blocks count: {}", params.prompt.len()); - - self.run_turn(session_id, cx, |thread, cx| { - let content: Vec = params - .prompt - .into_iter() - .map(Into::into) - .collect::>(); - log::debug!("Converted prompt to message: {} chars", content.len()); - log::debug!("Message id: {:?}", id); - log::debug!("Message content: {:?}", content); - - thread.update(cx, |thread, cx| thread.send(id, content, cx)) - }) - } - - fn resume( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(NativeAgentSessionResume { - connection: self.clone(), - session_id: session_id.clone(), - }) as _) - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - log::info!("Cancelling on session: {}", session_id); - self.0.update(cx, |agent, cx| { - if let Some(agent) = agent.sessions.get(session_id) { - agent.thread.update(cx, |thread, cx| thread.cancel(cx)); - } - }); - } - - fn truncate( - &self, - session_id: &agent_client_protocol::SessionId, - cx: &App, - ) -> Option> { - self.0.read_with(cx, |agent, _cx| { - agent.sessions.get(session_id).map(|session| { - Rc::new(NativeAgentSessionTruncate { - thread: session.thread.clone(), - acp_thread: session.acp_thread.clone(), - }) as _ - }) - }) - } - - fn set_title( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(NativeAgentSessionSetTitle { - connection: self.clone(), - session_id: session_id.clone(), - }) as _) - } - - fn telemetry(&self) -> Option> { - Some(Rc::new(self.clone()) as Rc) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -impl acp_thread::AgentTelemetry for NativeAgentConnection { - fn agent_name(&self) -> String { - "Zed".into() - } - - fn thread_data( - &self, - session_id: &acp::SessionId, - cx: &mut App, - ) -> Task> { - let Some(session) = self.0.read(cx).sessions.get(session_id) else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let task = session.thread.read(cx).to_db(cx); - cx.background_spawn(async move { - serde_json::to_value(task.await).context("Failed to serialize thread") - }) - } -} - -struct NativeAgentSessionTruncate { - thread: Entity, - acp_thread: WeakEntity, -} - -impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate { - fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { - match self.thread.update(cx, |thread, cx| { - thread.truncate(message_id.clone(), cx)?; - Ok(thread.latest_token_usage()) - }) { - Ok(usage) => { - self.acp_thread - .update(cx, |thread, cx| { - thread.update_token_usage(usage, cx); - }) - .ok(); - Task::ready(Ok(())) - } - Err(error) => Task::ready(Err(error)), - } - } -} - -struct NativeAgentSessionResume { - connection: NativeAgentConnection, - session_id: acp::SessionId, -} - -impl acp_thread::AgentSessionResume for NativeAgentSessionResume { - fn run(&self, cx: &mut App) -> Task> { - self.connection - .run_turn(self.session_id.clone(), cx, |thread, cx| { - thread.update(cx, |thread, cx| thread.resume(cx)) - }) - } -} - -struct NativeAgentSessionSetTitle { - connection: NativeAgentConnection, - session_id: acp::SessionId, -} - -impl acp_thread::AgentSessionSetTitle for NativeAgentSessionSetTitle { - fn run(&self, title: SharedString, cx: &mut App) -> Task> { - let Some(session) = self.connection.0.read(cx).sessions.get(&self.session_id) else { - return Task::ready(Err(anyhow!("session not found"))); - }; - let thread = session.thread.clone(); - thread.update(cx, |thread, cx| thread.set_title(title, cx)); - Task::ready(Ok(())) - } -} - -pub struct AcpThreadEnvironment { - acp_thread: WeakEntity, -} - -impl ThreadEnvironment for AcpThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>> { - let task = self.acp_thread.update(cx, |thread, cx| { - thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx) - }); - - let acp_thread = self.acp_thread.clone(); - cx.spawn(async move |cx| { - let terminal = task?.await?; - - let (drop_tx, drop_rx) = oneshot::channel(); - let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?; - - cx.spawn(async move |cx| { - drop_rx.await.ok(); - acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx)) - }) - .detach(); - - let handle = AcpTerminalHandle { - terminal, - _drop_tx: Some(drop_tx), - }; - - Ok(Rc::new(handle) as _) - }) - } -} - -pub struct AcpTerminalHandle { - terminal: Entity, - _drop_tx: Option>, -} - -impl TerminalHandle for AcpTerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result { - self.terminal.read_with(cx, |term, _cx| term.id().clone()) - } - - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { - self.terminal - .read_with(cx, |term, _cx| term.wait_for_exit()) - } - - fn current_output(&self, cx: &AsyncApp) -> Result { - self.terminal - .read_with(cx, |term, cx| term.current_output(cx)) - } -} - -#[cfg(test)] -mod tests { - use crate::HistoryEntryId; - - use super::*; - use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri}; - use fs::FakeFs; - use gpui::TestAppContext; - use indoc::formatdoc; - use language_model::fake_provider::FakeLanguageModel; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_path}; - - #[gpui::test] - async fn test_maintaining_project_context(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/", - json!({ - "a": {} - }), - ) - .await; - let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - let agent = NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - agent.read_with(cx, |agent, cx| { - assert_eq!(agent.project_context.read(cx).worktrees, vec![]) - }); - - let worktree = project - .update(cx, |project, cx| project.create_worktree("/a", true, cx)) - .await - .unwrap(); - cx.run_until_parked(); - agent.read_with(cx, |agent, cx| { - assert_eq!( - agent.project_context.read(cx).worktrees, - vec![WorktreeContext { - root_name: "a".into(), - abs_path: Path::new("/a").into(), - rules_file: None - }] - ) - }); - - // Creating `/a/.rules` updates the project context. - fs.insert_file("/a/.rules", Vec::new()).await; - cx.run_until_parked(); - agent.read_with(cx, |agent, cx| { - let rules_entry = worktree - .read(cx) - .entry_for_path(rel_path(".rules")) - .unwrap(); - assert_eq!( - agent.project_context.read(cx).worktrees, - vec![WorktreeContext { - root_name: "a".into(), - abs_path: Path::new("/a").into(), - rules_file: Some(RulesFileContext { - path_in_worktree: rel_path(".rules").into(), - text: "".into(), - project_entry_id: rules_entry.id.to_usize() - }) - }] - ) - }); - } - - #[gpui::test] - async fn test_listing_models(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({ "a": {} })).await; - let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - let connection = NativeAgentConnection( - NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(), - ); - - // Create a thread/session - let acp_thread = cx - .update(|cx| { - Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) - }) - .await - .unwrap(); - - let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); - - let models = cx - .update(|cx| { - connection - .model_selector(&session_id) - .unwrap() - .list_models(cx) - }) - .await - .unwrap(); - - let acp_thread::AgentModelList::Grouped(models) = models else { - panic!("Unexpected model group"); - }; - assert_eq!( - models, - IndexMap::from_iter([( - AgentModelGroupName("Fake".into()), - vec![AgentModelInfo { - id: acp::ModelId("fake/fake".into()), - name: "Fake".into(), - description: None, - icon: Some(ui::IconName::ZedAssistant), - }] - )]) - ); - } - - #[gpui::test] - async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.create_dir(paths::settings_file().parent().unwrap()) - .await - .unwrap(); - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "default_model": { - "provider": "foo", - "model": "bar" - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - let project = Project::test(fs.clone(), [], cx).await; - - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - - // Create the agent and connection - let agent = NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = NativeAgentConnection(agent.clone()); - - // Create a thread/session - let acp_thread = cx - .update(|cx| { - Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) - }) - .await - .unwrap(); - - let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); - - // Select a model - let selector = connection.model_selector(&session_id).unwrap(); - let model_id = acp::ModelId("fake/fake".into()); - cx.update(|cx| selector.select_model(model_id.clone(), cx)) - .await - .unwrap(); - - // Verify the thread has the selected model - agent.read_with(cx, |agent, _| { - let session = agent.sessions.get(&session_id).unwrap(); - session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.model().unwrap().id().0, "fake"); - }); - }); - - cx.run_until_parked(); - - // Verify settings file was updated - let settings_content = fs.load(paths::settings_file()).await.unwrap(); - let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap(); - - // Check that the agent settings contain the selected model - assert_eq!( - settings_json["agent"]["default_model"]["model"], - json!("fake") - ); - assert_eq!( - settings_json["agent"]["default_model"]["provider"], - json!("fake") - ); - } - - #[gpui::test] - async fn test_save_load_thread(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/", - json!({ - "a": { - "b.md": "Lorem" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); - let agent = NativeAgent::new( - project.clone(), - history_store.clone(), - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = Rc::new(NativeAgentConnection(agent.clone())); - - let acp_thread = cx - .update(|cx| { - connection - .clone() - .new_thread(project.clone(), Path::new(""), cx) - }) - .await - .unwrap(); - let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); - let thread = agent.read_with(cx, |agent, _| { - agent.sessions.get(&session_id).unwrap().thread.clone() - }); - - // Ensure empty threads are not saved, even if they get mutated. - let model = Arc::new(FakeLanguageModel::default()); - let summary_model = Arc::new(FakeLanguageModel::default()); - thread.update(cx, |thread, cx| { - thread.set_model(model.clone(), cx); - thread.set_summarization_model(Some(summary_model.clone()), cx); - }); - cx.run_until_parked(); - assert_eq!(history_entries(&history_store, cx), vec![]); - - let send = acp_thread.update(cx, |thread, cx| { - thread.send( - vec![ - "What does ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink { - name: "b.md".into(), - uri: MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri() - .to_string(), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - meta: None, - }), - " mean?".into(), - ], - cx, - ) - }); - let send = cx.foreground_executor().spawn(send); - cx.run_until_parked(); - - model.send_last_completion_stream_text_chunk("Lorem."); - model.end_last_completion_stream(); - cx.run_until_parked(); - summary_model - .send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md"))); - summary_model.end_last_completion_stream(); - - send.await.unwrap(); - let uri = MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - formatdoc! {" - ## User - - What does [@b.md]({uri}) mean? - - ## Assistant - - Lorem. - - "} - ) - }); - - cx.run_until_parked(); - - // Drop the ACP thread, which should cause the session to be dropped as well. - cx.update(|_| { - drop(thread); - drop(acp_thread); - }); - agent.read_with(cx, |agent, _| { - assert_eq!(agent.sessions.keys().cloned().collect::>(), []); - }); - - // Ensure the thread can be reloaded from disk. - assert_eq!( - history_entries(&history_store, cx), - vec![( - HistoryEntryId::AcpThread(session_id.clone()), - format!("Explaining {}", path!("/a/b.md")) - )] - ); - let acp_thread = agent - .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) - .await - .unwrap(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - formatdoc! {" - ## User - - What does [@b.md]({uri}) mean? - - ## Assistant - - Lorem. - - "} - ) - }); - } - - fn history_entries( - history: &Entity, - cx: &mut TestAppContext, - ) -> Vec<(HistoryEntryId, String)> { - history.read_with(cx, |history, _| { - history - .entries() - .map(|e| (e.id(), e.title().to_string())) - .collect::>() - }) - } - - fn init_test(cx: &mut TestAppContext) { - env_logger::try_init().ok(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - Project::init_settings(cx); - agent_settings::init(cx); - language::init(cx); - LanguageModelRegistry::test(cx); - }); - } -} diff --git a/crates/agent2/src/agent2.rs b/crates/agent2/src/agent2.rs deleted file mode 100644 index 1fc9c1cb956d1676c42713b5d9bb2a0b51e8ac90..0000000000000000000000000000000000000000 --- a/crates/agent2/src/agent2.rs +++ /dev/null @@ -1,19 +0,0 @@ -mod agent; -mod db; -mod history_store; -mod native_agent_server; -mod templates; -mod thread; -mod tool_schema; -mod tools; - -#[cfg(test)] -mod tests; - -pub use agent::*; -pub use db::*; -pub use history_store::*; -pub use native_agent_server::NativeAgentServer; -pub use templates::*; -pub use thread::*; -pub use tools::*; diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs deleted file mode 100644 index 756b868dcfc26239911d6e5c0cd8ad984cd7dc4e..0000000000000000000000000000000000000000 --- a/crates/agent2/src/thread.rs +++ /dev/null @@ -1,2663 +0,0 @@ -use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, - DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, - ListDirectoryTool, MovePathTool, NowTool, OpenTool, ReadFileTool, SystemPromptTemplate, - Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, -}; -use acp_thread::{MentionUri, UserMessageId}; -use action_log::ActionLog; -use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot}; -use agent_client_protocol as acp; -use agent_settings::{ - AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, - SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, -}; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::adapt_schema_to_format; -use chrono::{DateTime, Utc}; -use client::{ModelRequestUsage, RequestUsage, UserStore}; -use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit}; -use collections::{HashMap, HashSet, IndexMap}; -use fs::Fs; -use futures::stream; -use futures::{ - FutureExt, - channel::{mpsc, oneshot}, - future::Shared, - stream::FuturesUnordered, -}; -use git::repository::DiffType; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, -}; -use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, - LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult, - LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, - LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID, -}; -use project::{ - Project, - git_store::{GitStore, RepositoryState}, -}; -use prompt_store::ProjectContext; -use schemars::{JsonSchema, Schema}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, update_settings_file}; -use smol::stream::StreamExt; -use std::{ - collections::BTreeMap, - ops::RangeInclusive, - path::Path, - rc::Rc, - sync::Arc, - time::{Duration, Instant}, -}; -use std::{fmt::Write, path::PathBuf}; -use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock}; -use uuid::Uuid; - -const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user"; -pub const MAX_TOOL_NAME_LENGTH: usize = 64; - -/// The ID of the user prompt that initiated a request. -/// -/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key). -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] -pub struct PromptId(Arc); - -impl PromptId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} - -impl std::fmt::Display for PromptId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4; -pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); - -#[derive(Debug, Clone)] -enum RetryStrategy { - ExponentialBackoff { - initial_delay: Duration, - max_attempts: u8, - }, - Fixed { - delay: Duration, - max_attempts: u8, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum Message { - User(UserMessage), - Agent(AgentMessage), - Resume, -} - -impl Message { - pub fn as_agent_message(&self) -> Option<&AgentMessage> { - match self { - Message::Agent(agent_message) => Some(agent_message), - _ => None, - } - } - - pub fn to_request(&self) -> Vec { - match self { - Message::User(message) => vec![message.to_request()], - Message::Agent(message) => message.to_request(), - Message::Resume => vec![LanguageModelRequestMessage { - role: Role::User, - content: vec!["Continue where you left off".into()], - cache: false, - }], - } - } - - pub fn to_markdown(&self) -> String { - match self { - Message::User(message) => message.to_markdown(), - Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resume]\n".into(), - } - } - - pub fn role(&self) -> Role { - match self { - Message::User(_) | Message::Resume => Role::User, - Message::Agent(_) => Role::Assistant, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserMessage { - pub id: UserMessageId, - pub content: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum UserMessageContent { - Text(String), - Mention { uri: MentionUri, content: String }, - Image(LanguageModelImage), -} - -impl UserMessage { - pub fn to_markdown(&self) -> String { - let mut markdown = String::from("## User\n\n"); - - for content in &self.content { - match content { - UserMessageContent::Text(text) => { - markdown.push_str(text); - markdown.push('\n'); - } - UserMessageContent::Image(_) => { - markdown.push_str("\n"); - } - UserMessageContent::Mention { uri, content } => { - if !content.is_empty() { - let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); - } else { - let _ = writeln!(&mut markdown, "{}", uri.as_link()); - } - } - } - } - - markdown - } - - fn to_request(&self) -> LanguageModelRequestMessage { - let mut message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::with_capacity(self.content.len()), - cache: false, - }; - - const OPEN_CONTEXT: &str = "\n\ - The following items were attached by the user. \ - They are up-to-date and don't need to be re-read.\n\n"; - - const OPEN_FILES_TAG: &str = ""; - const OPEN_DIRECTORIES_TAG: &str = ""; - const OPEN_SYMBOLS_TAG: &str = ""; - const OPEN_SELECTIONS_TAG: &str = ""; - const OPEN_THREADS_TAG: &str = ""; - const OPEN_FETCH_TAG: &str = ""; - const OPEN_RULES_TAG: &str = - "\nThe user has specified the following rules that should be applied:\n"; - - let mut file_context = OPEN_FILES_TAG.to_string(); - let mut directory_context = OPEN_DIRECTORIES_TAG.to_string(); - let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); - let mut selection_context = OPEN_SELECTIONS_TAG.to_string(); - let mut thread_context = OPEN_THREADS_TAG.to_string(); - let mut fetch_context = OPEN_FETCH_TAG.to_string(); - let mut rules_context = OPEN_RULES_TAG.to_string(); - - for chunk in &self.content { - let chunk = match chunk { - UserMessageContent::Text(text) => { - language_model::MessageContent::Text(text.clone()) - } - UserMessageContent::Image(value) => { - language_model::MessageContent::Image(value.clone()) - } - UserMessageContent::Mention { uri, content } => { - match uri { - MentionUri::File { abs_path } => { - write!( - &mut file_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag(abs_path, None), - text: &content.to_string(), - } - ) - .ok(); - } - MentionUri::PastedImage => { - debug_panic!("pasted image URI should not be used in mention content") - } - MentionUri::Directory { .. } => { - write!(&mut directory_context, "\n{}\n", content).ok(); - } - MentionUri::Symbol { - abs_path: path, - line_range, - .. - } => { - write!( - &mut symbol_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag(path, Some(line_range)), - text: content - } - ) - .ok(); - } - MentionUri::Selection { - abs_path: path, - line_range, - .. - } => { - write!( - &mut selection_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag( - path.as_deref().unwrap_or("Untitled".as_ref()), - Some(line_range) - ), - text: content - } - ) - .ok(); - } - MentionUri::Thread { .. } => { - write!(&mut thread_context, "\n{}\n", content).ok(); - } - MentionUri::TextThread { .. } => { - write!(&mut thread_context, "\n{}\n", content).ok(); - } - MentionUri::Rule { .. } => { - write!( - &mut rules_context, - "\n{}", - MarkdownCodeBlock { - tag: "", - text: content - } - ) - .ok(); - } - MentionUri::Fetch { url } => { - write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok(); - } - } - - language_model::MessageContent::Text(uri.as_link().to_string()) - } - }; - - message.content.push(chunk); - } - - let len_before_context = message.content.len(); - - if file_context.len() > OPEN_FILES_TAG.len() { - file_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(file_context)); - } - - if directory_context.len() > OPEN_DIRECTORIES_TAG.len() { - directory_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(directory_context)); - } - - if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { - symbol_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(symbol_context)); - } - - if selection_context.len() > OPEN_SELECTIONS_TAG.len() { - selection_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(selection_context)); - } - - if thread_context.len() > OPEN_THREADS_TAG.len() { - thread_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(thread_context)); - } - - if fetch_context.len() > OPEN_FETCH_TAG.len() { - fetch_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(fetch_context)); - } - - if rules_context.len() > OPEN_RULES_TAG.len() { - rules_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(rules_context)); - } - - if message.content.len() > len_before_context { - message.content.insert( - len_before_context, - language_model::MessageContent::Text(OPEN_CONTEXT.into()), - ); - message - .content - .push(language_model::MessageContent::Text("".into())); - } - - message - } -} - -fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { - let mut result = String::new(); - - if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { - let _ = write!(result, "{} ", extension); - } - - let _ = write!(result, "{}", full_path.display()); - - if let Some(range) = line_range { - if range.start() == range.end() { - let _ = write!(result, ":{}", range.start() + 1); - } else { - let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); - } - } - - result -} - -impl AgentMessage { - pub fn to_markdown(&self) -> String { - let mut markdown = String::from("## Assistant\n\n"); - - for content in &self.content { - match content { - AgentMessageContent::Text(text) => { - markdown.push_str(text); - markdown.push('\n'); - } - AgentMessageContent::Thinking { text, .. } => { - markdown.push_str(""); - markdown.push_str(text); - markdown.push_str("\n"); - } - AgentMessageContent::RedactedThinking(_) => { - markdown.push_str("\n") - } - AgentMessageContent::ToolUse(tool_use) => { - markdown.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - markdown.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - } - } - - for tool_result in self.tool_results.values() { - markdown.push_str(&format!( - "**Tool Result**: {} (ID: {})\n\n", - tool_result.tool_name, tool_result.tool_use_id - )); - if tool_result.is_error { - markdown.push_str("**ERROR:**\n"); - } - - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(markdown, "{text}\n").ok(); - } - LanguageModelToolResultContent::Image(_) => { - writeln!(markdown, "\n").ok(); - } - } - - if let Some(output) = tool_result.output.as_ref() { - writeln!( - markdown, - "**Debug Output**:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output).unwrap() - ) - .unwrap(); - } - } - - markdown - } - - pub fn to_request(&self) -> Vec { - let mut assistant_message = LanguageModelRequestMessage { - role: Role::Assistant, - content: Vec::with_capacity(self.content.len()), - cache: false, - }; - for chunk in &self.content { - match chunk { - AgentMessageContent::Text(text) => { - assistant_message - .content - .push(language_model::MessageContent::Text(text.clone())); - } - AgentMessageContent::Thinking { text, signature } => { - assistant_message - .content - .push(language_model::MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - }); - } - AgentMessageContent::RedactedThinking(value) => { - assistant_message.content.push( - language_model::MessageContent::RedactedThinking(value.clone()), - ); - } - AgentMessageContent::ToolUse(tool_use) => { - if self.tool_results.contains_key(&tool_use.id) { - assistant_message - .content - .push(language_model::MessageContent::ToolUse(tool_use.clone())); - } - } - }; - } - - let mut user_message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::new(), - cache: false, - }; - - for tool_result in self.tool_results.values() { - let mut tool_result = tool_result.clone(); - // Surprisingly, the API fails if we return an empty string here. - // It thinks we are sending a tool use without a tool result. - if tool_result.content.is_empty() { - tool_result.content = "".into(); - } - user_message - .content - .push(language_model::MessageContent::ToolResult(tool_result)); - } - - let mut messages = Vec::new(); - if !assistant_message.content.is_empty() { - messages.push(assistant_message); - } - if !user_message.content.is_empty() { - messages.push(user_message); - } - messages - } -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AgentMessage { - pub content: Vec, - pub tool_results: IndexMap, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum AgentMessageContent { - Text(String), - Thinking { - text: String, - signature: Option, - }, - RedactedThinking(String), - ToolUse(LanguageModelToolUse), -} - -pub trait TerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result; - fn current_output(&self, cx: &AsyncApp) -> Result; - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>>; -} - -pub trait ThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>>; -} - -#[derive(Debug)] -pub enum ThreadEvent { - UserMessage(UserMessage), - AgentText(String), - AgentThinking(String), - ToolCall(acp::ToolCall), - ToolCallUpdate(acp_thread::ToolCallUpdate), - ToolCallAuthorization(ToolCallAuthorization), - Retry(acp_thread::RetryStatus), - Stop(acp::StopReason), -} - -#[derive(Debug)] -pub struct NewTerminal { - pub command: String, - pub output_byte_limit: Option, - pub cwd: Option, - pub response: oneshot::Sender>>, -} - -#[derive(Debug)] -pub struct ToolCallAuthorization { - pub tool_call: acp::ToolCallUpdate, - pub options: Vec, - pub response: oneshot::Sender, -} - -#[derive(Debug, thiserror::Error)] -enum CompletionError { - #[error("max tokens")] - MaxTokens, - #[error("refusal")] - Refusal, - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -pub struct Thread { - id: acp::SessionId, - prompt_id: PromptId, - updated_at: DateTime, - title: Option, - pending_title_generation: Option>, - summary: Option, - messages: Vec, - user_store: Entity, - completion_mode: CompletionMode, - /// Holds the task that handles agent interaction until the end of the turn. - /// Survives across multiple requests as the model performs tool calls and - /// we run tools, report their results. - running_turn: Option, - pending_message: Option, - tools: BTreeMap>, - tool_use_limit_reached: bool, - request_token_usage: HashMap, - #[allow(unused)] - cumulative_token_usage: TokenUsage, - #[allow(unused)] - initial_project_snapshot: Shared>>>, - context_server_registry: Entity, - profile_id: AgentProfileId, - project_context: Entity, - templates: Arc, - model: Option>, - summarization_model: Option>, - prompt_capabilities_tx: watch::Sender, - pub(crate) prompt_capabilities_rx: watch::Receiver, - pub(crate) project: Entity, - pub(crate) action_log: Entity, -} - -impl Thread { - fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { - let image = model.map_or(true, |model| model.supports_images()); - acp::PromptCapabilities { - meta: None, - image, - audio: false, - embedded_context: true, - } - } - - pub fn new( - project: Entity, - project_context: Entity, - context_server_registry: Entity, - templates: Arc, - model: Option>, - cx: &mut Context, - ) -> Self { - let profile_id = AgentSettings::get_global(cx).default_profile.clone(); - let action_log = cx.new(|_cx| ActionLog::new(project.clone())); - let (prompt_capabilities_tx, prompt_capabilities_rx) = - watch::channel(Self::prompt_capabilities(model.as_deref())); - Self { - id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()), - prompt_id: PromptId::new(), - updated_at: Utc::now(), - title: None, - pending_title_generation: None, - summary: None, - messages: Vec::new(), - user_store: project.read(cx).user_store(), - completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, - running_turn: None, - pending_message: None, - tools: BTreeMap::default(), - tool_use_limit_reached: false, - request_token_usage: HashMap::default(), - cumulative_token_usage: TokenUsage::default(), - initial_project_snapshot: { - let project_snapshot = Self::project_snapshot(project.clone(), cx); - cx.foreground_executor() - .spawn(async move { Some(project_snapshot.await) }) - .shared() - }, - context_server_registry, - profile_id, - project_context, - templates, - model, - summarization_model: None, - prompt_capabilities_tx, - prompt_capabilities_rx, - project, - action_log, - } - } - - pub fn id(&self) -> &acp::SessionId { - &self.id - } - - pub fn replay( - &mut self, - cx: &mut Context, - ) -> mpsc::UnboundedReceiver> { - let (tx, rx) = mpsc::unbounded(); - let stream = ThreadEventStream(tx); - for message in &self.messages { - match message { - Message::User(user_message) => stream.send_user_message(user_message), - Message::Agent(assistant_message) => { - for content in &assistant_message.content { - match content { - AgentMessageContent::Text(text) => stream.send_text(text), - AgentMessageContent::Thinking { text, .. } => { - stream.send_thinking(text) - } - AgentMessageContent::RedactedThinking(_) => {} - AgentMessageContent::ToolUse(tool_use) => { - self.replay_tool_call( - tool_use, - assistant_message.tool_results.get(&tool_use.id), - &stream, - cx, - ); - } - } - } - } - Message::Resume => {} - } - } - rx - } - - fn replay_tool_call( - &self, - tool_use: &LanguageModelToolUse, - tool_result: Option<&LanguageModelToolResult>, - stream: &ThreadEventStream, - cx: &mut Context, - ) { - let tool = self.tools.get(tool_use.name.as_ref()).cloned().or_else(|| { - self.context_server_registry - .read(cx) - .servers() - .find_map(|(_, tools)| { - if let Some(tool) = tools.get(tool_use.name.as_ref()) { - Some(tool.clone()) - } else { - None - } - }) - }); - - let Some(tool) = tool else { - stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall { - meta: None, - id: acp::ToolCallId(tool_use.id.to_string().into()), - title: tool_use.name.to_string(), - kind: acp::ToolKind::Other, - status: acp::ToolCallStatus::Failed, - content: Vec::new(), - locations: Vec::new(), - raw_input: Some(tool_use.input.clone()), - raw_output: None, - }))) - .ok(); - return; - }; - - let title = tool.initial_title(tool_use.input.clone(), cx); - let kind = tool.kind(); - stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); - - let output = tool_result - .as_ref() - .and_then(|result| result.output.clone()); - if let Some(output) = output.clone() { - let tool_event_stream = ToolCallEventStream::new( - tool_use.id.clone(), - stream.clone(), - Some(self.project.read(cx).fs().clone()), - ); - tool.replay(tool_use.input.clone(), output, tool_event_stream, cx) - .log_err(); - } - - stream.update_tool_call_fields( - &tool_use.id, - acp::ToolCallUpdateFields { - status: Some( - tool_result - .as_ref() - .map_or(acp::ToolCallStatus::Failed, |result| { - if result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - } - }), - ), - raw_output: output, - ..Default::default() - }, - ); - } - - pub fn from_db( - id: acp::SessionId, - db_thread: DbThread, - project: Entity, - project_context: Entity, - context_server_registry: Entity, - action_log: Entity, - templates: Arc, - cx: &mut Context, - ) -> Self { - let profile_id = db_thread - .profile - .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); - let model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - db_thread - .model - .and_then(|model| { - let model = SelectedModel { - provider: model.provider.clone().into(), - model: model.model.into(), - }; - registry.select_model(&model, cx) - }) - .or_else(|| registry.default_model()) - .map(|model| model.model) - }); - let (prompt_capabilities_tx, prompt_capabilities_rx) = - watch::channel(Self::prompt_capabilities(model.as_deref())); - - Self { - id, - prompt_id: PromptId::new(), - title: if db_thread.title.is_empty() { - None - } else { - Some(db_thread.title.clone()) - }, - pending_title_generation: None, - summary: db_thread.detailed_summary, - messages: db_thread.messages, - user_store: project.read(cx).user_store(), - completion_mode: db_thread.completion_mode.unwrap_or_default(), - running_turn: None, - pending_message: None, - tools: BTreeMap::default(), - tool_use_limit_reached: false, - request_token_usage: db_thread.request_token_usage.clone(), - cumulative_token_usage: db_thread.cumulative_token_usage, - initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(), - context_server_registry, - profile_id, - project_context, - templates, - model, - summarization_model: None, - project, - action_log, - updated_at: db_thread.updated_at, - prompt_capabilities_tx, - prompt_capabilities_rx, - } - } - - pub fn to_db(&self, cx: &App) -> Task { - let initial_project_snapshot = self.initial_project_snapshot.clone(); - let mut thread = DbThread { - title: self.title(), - messages: self.messages.clone(), - updated_at: self.updated_at, - detailed_summary: self.summary.clone(), - initial_project_snapshot: None, - cumulative_token_usage: self.cumulative_token_usage, - request_token_usage: self.request_token_usage.clone(), - model: self.model.as_ref().map(|model| DbLanguageModel { - provider: model.provider_id().to_string(), - model: model.name().0.to_string(), - }), - completion_mode: Some(self.completion_mode), - profile: Some(self.profile_id.clone()), - }; - - cx.background_spawn(async move { - let initial_project_snapshot = initial_project_snapshot.await; - thread.initial_project_snapshot = initial_project_snapshot; - thread - }) - } - - /// Create a snapshot of the current project state including git information and unsaved buffers. - fn project_snapshot( - project: Entity, - cx: &mut Context, - ) -> Task> { - let git_store = project.read(cx).git_store().clone(); - let worktree_snapshots: Vec<_> = project - .read(cx) - .visible_worktrees(cx) - .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx)) - .collect(); - - cx.spawn(async move |_, _| { - let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; - - Arc::new(ProjectSnapshot { - worktree_snapshots, - timestamp: Utc::now(), - }) - }) - } - - fn worktree_snapshot( - worktree: Entity, - git_store: Entity, - cx: &App, - ) -> Task { - cx.spawn(async move |cx| { - // Get worktree path and snapshot - let worktree_info = cx.update(|app_cx| { - let worktree = worktree.read(app_cx); - let path = worktree.abs_path().to_string_lossy().into_owned(); - let snapshot = worktree.snapshot(); - (path, snapshot) - }); - - let Ok((worktree_path, _snapshot)) = worktree_info else { - return WorktreeSnapshot { - worktree_path: String::new(), - git_state: None, - }; - }; - - let git_state = git_store - .update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .find(|repo| { - repo.read(cx) - .abs_path_to_repo_path(&worktree.read(cx).abs_path()) - .is_some() - }) - .cloned() - }) - .ok() - .flatten() - .map(|repo| { - repo.update(cx, |repo, _| { - let current_branch = - repo.branch.as_ref().map(|branch| branch.name().to_owned()); - repo.send_job(None, |state, _| async move { - let RepositoryState::Local { backend, .. } = state else { - return GitState { - remote_url: None, - head_sha: None, - current_branch, - diff: None, - }; - }; - - let remote_url = backend.remote_url("origin"); - let head_sha = backend.head_sha().await; - let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); - - GitState { - remote_url, - head_sha, - current_branch, - diff, - } - }) - }) - }); - - let git_state = match git_state { - Some(git_state) => match git_state.ok() { - Some(git_state) => git_state.await.ok(), - None => None, - }, - None => None, - }; - - WorktreeSnapshot { - worktree_path, - git_state, - } - }) - } - - pub fn project_context(&self) -> &Entity { - &self.project_context - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn action_log(&self) -> &Entity { - &self.action_log - } - - pub fn is_empty(&self) -> bool { - self.messages.is_empty() && self.title.is_none() - } - - pub fn model(&self) -> Option<&Arc> { - self.model.as_ref() - } - - pub fn set_model(&mut self, model: Arc, cx: &mut Context) { - let old_usage = self.latest_token_usage(); - self.model = Some(model); - let new_caps = Self::prompt_capabilities(self.model.as_deref()); - let new_usage = self.latest_token_usage(); - if old_usage != new_usage { - cx.emit(TokenUsageUpdated(new_usage)); - } - self.prompt_capabilities_tx.send(new_caps).log_err(); - cx.notify() - } - - pub fn summarization_model(&self) -> Option<&Arc> { - self.summarization_model.as_ref() - } - - pub fn set_summarization_model( - &mut self, - model: Option>, - cx: &mut Context, - ) { - self.summarization_model = model; - cx.notify() - } - - pub fn completion_mode(&self) -> CompletionMode { - self.completion_mode - } - - pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context) { - let old_usage = self.latest_token_usage(); - self.completion_mode = mode; - let new_usage = self.latest_token_usage(); - if old_usage != new_usage { - cx.emit(TokenUsageUpdated(new_usage)); - } - cx.notify() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn last_message(&self) -> Option { - if let Some(message) = self.pending_message.clone() { - Some(Message::Agent(message)) - } else { - self.messages.last().cloned() - } - } - - pub fn add_default_tools( - &mut self, - environment: Rc, - cx: &mut Context, - ) { - let language_registry = self.project.read(cx).languages().clone(); - self.add_tool(CopyPathTool::new(self.project.clone())); - self.add_tool(CreateDirectoryTool::new(self.project.clone())); - self.add_tool(DeletePathTool::new( - self.project.clone(), - self.action_log.clone(), - )); - self.add_tool(DiagnosticsTool::new(self.project.clone())); - self.add_tool(EditFileTool::new( - self.project.clone(), - cx.weak_entity(), - language_registry, - )); - self.add_tool(FetchTool::new(self.project.read(cx).client().http_client())); - self.add_tool(FindPathTool::new(self.project.clone())); - self.add_tool(GrepTool::new(self.project.clone())); - self.add_tool(ListDirectoryTool::new(self.project.clone())); - self.add_tool(MovePathTool::new(self.project.clone())); - self.add_tool(NowTool); - self.add_tool(OpenTool::new(self.project.clone())); - self.add_tool(ReadFileTool::new( - self.project.clone(), - self.action_log.clone(), - )); - self.add_tool(TerminalTool::new(self.project.clone(), environment)); - self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); - } - - pub fn add_tool(&mut self, tool: T) { - self.tools.insert(T::name().into(), tool.erase()); - } - - pub fn remove_tool(&mut self, name: &str) -> bool { - self.tools.remove(name).is_some() - } - - pub fn profile(&self) -> &AgentProfileId { - &self.profile_id - } - - pub fn set_profile(&mut self, profile_id: AgentProfileId) { - self.profile_id = profile_id; - } - - pub fn cancel(&mut self, cx: &mut Context) { - if let Some(running_turn) = self.running_turn.take() { - running_turn.cancel(); - } - self.flush_pending_message(cx); - } - - fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context) { - let Some(last_user_message) = self.last_user_message() else { - return; - }; - - self.request_token_usage - .insert(last_user_message.id.clone(), update); - cx.emit(TokenUsageUpdated(self.latest_token_usage())); - cx.notify(); - } - - pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context) -> Result<()> { - self.cancel(cx); - let Some(position) = self.messages.iter().position( - |msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id), - ) else { - return Err(anyhow!("Message not found")); - }; - - for message in self.messages.drain(position..) { - match message { - Message::User(message) => { - self.request_token_usage.remove(&message.id); - } - Message::Agent(_) | Message::Resume => {} - } - } - self.summary = None; - cx.notify(); - Ok(()) - } - - pub fn latest_token_usage(&self) -> Option { - let last_user_message = self.last_user_message()?; - let tokens = self.request_token_usage.get(&last_user_message.id)?; - let model = self.model.clone()?; - - Some(acp_thread::TokenUsage { - max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), - used_tokens: tokens.total_tokens(), - }) - } - - pub fn resume( - &mut self, - cx: &mut Context, - ) -> Result>> { - self.messages.push(Message::Resume); - cx.notify(); - - log::debug!("Total messages in thread: {}", self.messages.len()); - self.run_turn(cx) - } - - /// Sending a message results in the model streaming a response, which could include tool calls. - /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. - /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. - pub fn send( - &mut self, - id: UserMessageId, - content: impl IntoIterator, - cx: &mut Context, - ) -> Result>> - where - T: Into, - { - let model = self.model().context("No language model configured")?; - - log::info!("Thread::send called with model: {}", model.name().0); - self.advance_prompt_id(); - - let content = content.into_iter().map(Into::into).collect::>(); - log::debug!("Thread::send content: {:?}", content); - - self.messages - .push(Message::User(UserMessage { id, content })); - cx.notify(); - - log::debug!("Total messages in thread: {}", self.messages.len()); - self.run_turn(cx) - } - - fn run_turn( - &mut self, - cx: &mut Context, - ) -> Result>> { - self.cancel(cx); - - let model = self.model.clone().context("No language model configured")?; - let profile = AgentSettings::get_global(cx) - .profiles - .get(&self.profile_id) - .context("Profile not found")?; - let (events_tx, events_rx) = mpsc::unbounded::>(); - let event_stream = ThreadEventStream(events_tx); - let message_ix = self.messages.len().saturating_sub(1); - self.tool_use_limit_reached = false; - self.summary = None; - self.running_turn = Some(RunningTurn { - event_stream: event_stream.clone(), - tools: self.enabled_tools(profile, &model, cx), - _task: cx.spawn(async move |this, cx| { - log::debug!("Starting agent turn execution"); - - let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; - _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); - - match turn_result { - Ok(()) => { - log::debug!("Turn execution completed"); - event_stream.send_stop(acp::StopReason::EndTurn); - } - Err(error) => { - log::error!("Turn execution failed: {:?}", error); - match error.downcast::() { - Ok(CompletionError::Refusal) => { - event_stream.send_stop(acp::StopReason::Refusal); - _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); - } - Ok(CompletionError::MaxTokens) => { - event_stream.send_stop(acp::StopReason::MaxTokens); - } - Ok(CompletionError::Other(error)) | Err(error) => { - event_stream.send_error(error); - } - } - } - } - - _ = this.update(cx, |this, _| this.running_turn.take()); - }), - }); - Ok(events_rx) - } - - async fn run_turn_internal( - this: &WeakEntity, - model: Arc, - event_stream: &ThreadEventStream, - cx: &mut AsyncApp, - ) -> Result<()> { - let mut attempt = 0; - let mut intent = CompletionIntent::UserPrompt; - loop { - let request = - this.update(cx, |this, cx| this.build_completion_request(intent, cx))??; - - telemetry::event!( - "Agent Thread Completion", - thread_id = this.read_with(cx, |this, _| this.id.to_string())?, - prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, - model = model.telemetry_id(), - model_provider = model.provider_id().to_string(), - attempt - ); - - log::debug!("Calling model.stream_completion, attempt {}", attempt); - - let (mut events, mut error) = match model.stream_completion(request, cx).await { - Ok(events) => (events, None), - Err(err) => (stream::empty().boxed(), Some(err)), - }; - let mut tool_results = FuturesUnordered::new(); - while let Some(event) = events.next().await { - log::trace!("Received completion event: {:?}", event); - match event { - Ok(event) => { - tool_results.extend(this.update(cx, |this, cx| { - this.handle_completion_event(event, event_stream, cx) - })??); - } - Err(err) => { - error = Some(err); - break; - } - } - } - - let end_turn = tool_results.is_empty(); - while let Some(tool_result) = tool_results.next().await { - log::debug!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields { - status: Some(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }), - raw_output: tool_result.output.clone(), - ..Default::default() - }, - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - })?; - } - - this.update(cx, |this, cx| { - this.flush_pending_message(cx); - if this.title.is_none() && this.pending_title_generation.is_none() { - this.generate_title(cx); - } - })?; - - if let Some(error) = error { - attempt += 1; - let retry = this.update(cx, |this, cx| { - let user_store = this.user_store.read(cx); - this.handle_completion_error(error, attempt, user_store.plan()) - })??; - let timer = cx.background_executor().timer(retry.duration); - event_stream.send_retry(retry); - timer.await; - this.update(cx, |this, _cx| { - if let Some(Message::Agent(message)) = this.messages.last() { - if message.tool_results.is_empty() { - intent = CompletionIntent::UserPrompt; - this.messages.push(Message::Resume); - } - } - })?; - } else if this.read_with(cx, |this, _| this.tool_use_limit_reached)? { - return Err(language_model::ToolUseLimitReachedError.into()); - } else if end_turn { - return Ok(()); - } else { - intent = CompletionIntent::ToolResults; - attempt = 0; - } - } - } - - fn handle_completion_error( - &mut self, - error: LanguageModelCompletionError, - attempt: u8, - plan: Option, - ) -> Result { - let Some(model) = self.model.as_ref() else { - return Err(anyhow!(error)); - }; - - let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID { - match plan { - Some(Plan::V2(_)) => true, - Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn, - None => false, - } - } else { - true - }; - - if !auto_retry { - return Err(anyhow!(error)); - } - - let Some(strategy) = Self::retry_strategy_for(&error) else { - return Err(anyhow!(error)); - }; - - let max_attempts = match &strategy { - RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, - RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, - }; - - if attempt > max_attempts { - return Err(anyhow!(error)); - } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - Ok(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }) - } - - /// A helper method that's called on every streamed completion event. - /// Returns an optional tool result task, which the main agentic loop will - /// send back to the model when it resolves. - fn handle_completion_event( - &mut self, - event: LanguageModelCompletionEvent, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Result>> { - log::trace!("Handling streamed completion event: {:?}", event); - use LanguageModelCompletionEvent::*; - - match event { - StartMessage { .. } => { - self.flush_pending_message(cx); - self.pending_message = Some(AgentMessage::default()); - } - Text(new_text) => self.handle_text_event(new_text, event_stream, cx), - Thinking { text, signature } => { - self.handle_thinking_event(text, signature, event_stream, cx) - } - RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), - ToolUse(tool_use) => { - return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); - } - ToolUseJsonParseError { - id, - tool_name, - raw_input, - json_parse_error, - } => { - return Ok(Some(Task::ready( - self.handle_tool_use_json_parse_error_event( - id, - tool_name, - raw_input, - json_parse_error, - ), - ))); - } - UsageUpdate(usage) => { - telemetry::event!( - "Agent Thread Completion Usage Updated", - thread_id = self.id.to_string(), - prompt_id = self.prompt_id.to_string(), - model = self.model.as_ref().map(|m| m.telemetry_id()), - model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), - input_tokens = usage.input_tokens, - output_tokens = usage.output_tokens, - cache_creation_input_tokens = usage.cache_creation_input_tokens, - cache_read_input_tokens = usage.cache_read_input_tokens, - ); - self.update_token_usage(usage, cx); - } - StatusUpdate(CompletionRequestStatus::UsageUpdated { amount, limit }) => { - self.update_model_request_usage(amount, limit, cx); - } - StatusUpdate( - CompletionRequestStatus::Started - | CompletionRequestStatus::Queued { .. } - | CompletionRequestStatus::Failed { .. }, - ) => {} - StatusUpdate(CompletionRequestStatus::ToolUseLimitReached) => { - self.tool_use_limit_reached = true; - } - Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), - Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), - Stop(StopReason::ToolUse | StopReason::EndTurn) => {} - } - - Ok(None) - } - - fn handle_text_event( - &mut self, - new_text: String, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) { - event_stream.send_text(&new_text); - - let last_message = self.pending_message(); - if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { - text.push_str(&new_text); - } else { - last_message - .content - .push(AgentMessageContent::Text(new_text)); - } - - cx.notify(); - } - - fn handle_thinking_event( - &mut self, - new_text: String, - new_signature: Option, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) { - event_stream.send_thinking(&new_text); - - let last_message = self.pending_message(); - if let Some(AgentMessageContent::Thinking { text, signature }) = - last_message.content.last_mut() - { - text.push_str(&new_text); - *signature = new_signature.or(signature.take()); - } else { - last_message.content.push(AgentMessageContent::Thinking { - text: new_text, - signature: new_signature, - }); - } - - cx.notify(); - } - - fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { - let last_message = self.pending_message(); - last_message - .content - .push(AgentMessageContent::RedactedThinking(data)); - cx.notify(); - } - - fn handle_tool_use_event( - &mut self, - tool_use: LanguageModelToolUse, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Option> { - cx.notify(); - - let tool = self.tool(tool_use.name.as_ref()); - let mut title = SharedString::from(&tool_use.name); - let mut kind = acp::ToolKind::Other; - if let Some(tool) = tool.as_ref() { - title = tool.initial_title(tool_use.input.clone(), cx); - kind = tool.kind(); - } - - // Ensure the last message ends in the current tool use - let last_message = self.pending_message(); - let push_new_tool_use = last_message.content.last_mut().is_none_or(|content| { - if let AgentMessageContent::ToolUse(last_tool_use) = content { - if last_tool_use.id == tool_use.id { - *last_tool_use = tool_use.clone(); - false - } else { - true - } - } else { - true - } - }); - - if push_new_tool_use { - event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); - last_message - .content - .push(AgentMessageContent::ToolUse(tool_use.clone())); - } else { - event_stream.update_tool_call_fields( - &tool_use.id, - acp::ToolCallUpdateFields { - title: Some(title.into()), - kind: Some(kind), - raw_input: Some(tool_use.input.clone()), - ..Default::default() - }, - ); - } - - if !tool_use.is_input_complete { - return None; - } - - let Some(tool) = tool else { - let content = format!("No tool named {} exists", tool_use.name); - return Some(Task::ready(LanguageModelToolResult { - content: LanguageModelToolResultContent::Text(Arc::from(content)), - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - output: None, - })); - }; - - let fs = self.project.read(cx).fs().clone(); - let tool_event_stream = - ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs)); - tool_event_stream.update_fields(acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::InProgress), - ..Default::default() - }); - let supports_images = self.model().is_some_and(|model| model.supports_images()); - let tool_result = tool.run(tool_use.input, tool_event_stream, cx); - log::debug!("Running tool {}", tool_use.name); - Some(cx.foreground_executor().spawn(async move { - let tool_result = tool_result.await.and_then(|output| { - if let LanguageModelToolResultContent::Image(_) = &output.llm_output - && !supports_images - { - return Err(anyhow!( - "Attempted to read an image, but this model doesn't support it.", - )); - } - Ok(output) - }); - - match tool_result { - Ok(output) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: false, - content: output.llm_output, - output: Some(output.raw_output), - }, - Err(error) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: Some(error.to_string().into()), - }, - } - })) - } - - fn handle_tool_use_json_parse_error_event( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - raw_input: Arc, - json_parse_error: String, - ) -> LanguageModelToolResult { - let tool_output = format!("Error parsing input JSON: {json_parse_error}"); - LanguageModelToolResult { - tool_use_id, - tool_name, - is_error: true, - content: LanguageModelToolResultContent::Text(tool_output.into()), - output: Some(serde_json::Value::String(raw_input.to_string())), - } - } - - fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context) { - self.project - .read(cx) - .user_store() - .update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); - } - - pub fn title(&self) -> SharedString { - self.title.clone().unwrap_or("New Thread".into()) - } - - pub fn summary(&mut self, cx: &mut Context) -> Task> { - if let Some(summary) = self.summary.as_ref() { - return Task::ready(Ok(summary.clone())); - } - let Some(model) = self.summarization_model.clone() else { - return Task::ready(Err(anyhow!("No summarization model available"))); - }; - let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadContextSummarization), - temperature: AgentSettings::temperature_for_model(&model, cx), - ..Default::default() - }; - - for message in &self.messages { - request.messages.extend(message.to_request()); - } - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], - cache: false, - }); - cx.spawn(async move |this, cx| { - let mut summary = String::new(); - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - })?; - continue; - } - _ => continue, - }; - - let mut lines = text.lines(); - summary.extend(lines.next()); - } - - log::debug!("Setting summary: {}", summary); - let summary = SharedString::from(summary); - - this.update(cx, |this, cx| { - this.summary = Some(summary.clone()); - cx.notify() - })?; - - Ok(summary) - }) - } - - fn generate_title(&mut self, cx: &mut Context) { - let Some(model) = self.summarization_model.clone() else { - return; - }; - - log::debug!( - "Generating title with model: {:?}", - self.summarization_model.as_ref().map(|model| model.name()) - ); - let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadSummarization), - temperature: AgentSettings::temperature_for_model(&model, cx), - ..Default::default() - }; - - for message in &self.messages { - request.messages.extend(message.to_request()); - } - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![SUMMARIZE_THREAD_PROMPT.into()], - cache: false, - }); - self.pending_title_generation = Some(cx.spawn(async move |this, cx| { - let mut title = String::new(); - - let generate = async { - let mut messages = model.stream_completion(request, cx).await?; - while let Some(event) = messages.next().await { - let event = event?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::StatusUpdate( - CompletionRequestStatus::UsageUpdated { amount, limit }, - ) => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - })?; - continue; - } - _ => continue, - }; - - let mut lines = text.lines(); - title.extend(lines.next()); - - // Stop if the LLM generated multiple lines. - if lines.next().is_some() { - break; - } - } - anyhow::Ok(()) - }; - - if generate.await.context("failed to generate title").is_ok() { - _ = this.update(cx, |this, cx| this.set_title(title.into(), cx)); - } - _ = this.update(cx, |this, _| this.pending_title_generation = None); - })); - } - - pub fn set_title(&mut self, title: SharedString, cx: &mut Context) { - self.pending_title_generation = None; - if Some(&title) != self.title.as_ref() { - self.title = Some(title); - cx.emit(TitleUpdated); - cx.notify(); - } - } - - fn last_user_message(&self) -> Option<&UserMessage> { - self.messages - .iter() - .rev() - .find_map(|message| match message { - Message::User(user_message) => Some(user_message), - Message::Agent(_) => None, - Message::Resume => None, - }) - } - - fn pending_message(&mut self) -> &mut AgentMessage { - self.pending_message.get_or_insert_default() - } - - fn flush_pending_message(&mut self, cx: &mut Context) { - let Some(mut message) = self.pending_message.take() else { - return; - }; - - if message.content.is_empty() { - return; - } - - for content in &message.content { - let AgentMessageContent::ToolUse(tool_use) = content else { - continue; - }; - - if !message.tool_results.contains_key(&tool_use.id) { - message.tool_results.insert( - tool_use.id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use.id.clone(), - tool_name: tool_use.name.clone(), - is_error: true, - content: LanguageModelToolResultContent::Text(TOOL_CANCELED_MESSAGE.into()), - output: None, - }, - ); - } - } - - self.messages.push(Message::Agent(message)); - self.updated_at = Utc::now(); - self.summary = None; - cx.notify() - } - - pub(crate) fn build_completion_request( - &self, - completion_intent: CompletionIntent, - cx: &App, - ) -> Result { - let model = self.model().context("No language model configured")?; - let tools = if let Some(turn) = self.running_turn.as_ref() { - turn.tools - .iter() - .filter_map(|(tool_name, tool)| { - log::trace!("Including tool: {}", tool_name); - Some(LanguageModelRequestTool { - name: tool_name.to_string(), - description: tool.description().to_string(), - input_schema: tool.input_schema(model.tool_input_format()).log_err()?, - }) - }) - .collect::>() - } else { - Vec::new() - }; - - log::debug!("Building completion request"); - log::debug!("Completion intent: {:?}", completion_intent); - log::debug!("Completion mode: {:?}", self.completion_mode); - - let messages = self.build_request_messages(cx); - log::debug!("Request will include {} messages", messages.len()); - log::debug!("Request includes {} tools", tools.len()); - - let request = LanguageModelRequest { - thread_id: Some(self.id.to_string()), - prompt_id: Some(self.prompt_id.to_string()), - intent: Some(completion_intent), - mode: Some(self.completion_mode.into()), - messages, - tools, - tool_choice: None, - stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(model, cx), - thinking_allowed: true, - }; - - log::debug!("Completion request built successfully"); - Ok(request) - } - - fn enabled_tools( - &self, - profile: &AgentProfileSettings, - model: &Arc, - cx: &App, - ) -> BTreeMap> { - fn truncate(tool_name: &SharedString) -> SharedString { - if tool_name.len() > MAX_TOOL_NAME_LENGTH { - let mut truncated = tool_name.to_string(); - truncated.truncate(MAX_TOOL_NAME_LENGTH); - truncated.into() - } else { - tool_name.clone() - } - } - - let mut tools = self - .tools - .iter() - .filter_map(|(tool_name, tool)| { - if tool.supported_provider(&model.provider_id()) - && profile.is_tool_enabled(tool_name) - { - Some((truncate(tool_name), tool.clone())) - } else { - None - } - }) - .collect::>(); - - let mut context_server_tools = Vec::new(); - let mut seen_tools = tools.keys().cloned().collect::>(); - let mut duplicate_tool_names = HashSet::default(); - for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { - for (tool_name, tool) in server_tools { - if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { - let tool_name = truncate(tool_name); - if !seen_tools.insert(tool_name.clone()) { - duplicate_tool_names.insert(tool_name.clone()); - } - context_server_tools.push((server_id.clone(), tool_name, tool.clone())); - } - } - } - - // When there are duplicate tool names, disambiguate by prefixing them - // with the server ID. In the rare case there isn't enough space for the - // disambiguated tool name, keep only the last tool with this name. - for (server_id, tool_name, tool) in context_server_tools { - if duplicate_tool_names.contains(&tool_name) { - let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); - if available >= 2 { - let mut disambiguated = server_id.0.to_string(); - disambiguated.truncate(available - 1); - disambiguated.push('_'); - disambiguated.push_str(&tool_name); - tools.insert(disambiguated.into(), tool.clone()); - } else { - tools.insert(tool_name, tool.clone()); - } - } else { - tools.insert(tool_name, tool.clone()); - } - } - - tools - } - - fn tool(&self, name: &str) -> Option> { - self.running_turn.as_ref()?.tools.get(name).cloned() - } - - fn build_request_messages(&self, cx: &App) -> Vec { - log::trace!( - "Building request messages from {} thread messages", - self.messages.len() - ); - - let system_prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools: self.tools.keys().cloned().collect(), - } - .render(&self.templates) - .context("failed to build system prompt") - .expect("Invalid template"); - let mut messages = vec![LanguageModelRequestMessage { - role: Role::System, - content: vec![system_prompt.into()], - cache: false, - }]; - for message in &self.messages { - messages.extend(message.to_request()); - } - - if let Some(last_message) = messages.last_mut() { - last_message.cache = true; - } - - if let Some(message) = self.pending_message.as_ref() { - messages.extend(message.to_request()); - } - - messages - } - - pub fn to_markdown(&self) -> String { - let mut markdown = String::new(); - for (ix, message) in self.messages.iter().enumerate() { - if ix > 0 { - markdown.push('\n'); - } - markdown.push_str(&message.to_markdown()); - } - - if let Some(message) = self.pending_message.as_ref() { - markdown.push('\n'); - markdown.push_str(&message.to_markdown()); - } - - markdown - } - - fn advance_prompt_id(&mut self) { - self.prompt_id = PromptId::new(); - } - - fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option { - use LanguageModelCompletionError::*; - use http_client::StatusCode; - - // General strategy here: - // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all. - // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff. - // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times. - match error { - HttpResponseError { - status_code: StatusCode::TOO_MANY_REQUESTS, - .. - } => Some(RetryStrategy::ExponentialBackoff { - initial_delay: BASE_RETRY_DELAY, - max_attempts: MAX_RETRY_ATTEMPTS, - }), - ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } - UpstreamProviderError { - status, - retry_after, - .. - } => match *status { - StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } - StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - // Internal Server Error could be anything, retry up to 3 times. - max_attempts: 3, - }), - status => { - // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"), - // but we frequently get them in practice. See https://http.dev/529 - if status.as_u16() == 529 { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } else { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: 2, - }) - } - } - }, - ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }), - ApiReadResponseError { .. } - | HttpSend { .. } - | DeserializeResponse { .. } - | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }), - // Retrying these errors definitely shouldn't help. - HttpResponseError { - status_code: - StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, - .. - } - | AuthenticationError { .. } - | PermissionError { .. } - | NoApiKey { .. } - | ApiEndpointNotFound { .. } - | PromptTooLarge { .. } => None, - // These errors might be transient, so retry them - SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 1, - }), - // Retry all other 4xx and 5xx errors once. - HttpResponseError { status_code, .. } - if status_code.is_client_error() || status_code.is_server_error() => - { - Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }) - } - Other(err) - if err.is::() - || err.is::() => - { - // Retrying won't help for Payment Required or Model Request Limit errors (where - // the user must upgrade to usage-based billing to get more requests, or else wait - // for a significant amount of time for the request limit to reset). - None - } - // Conservatively assume that any other errors are non-retryable - HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 2, - }), - } - } -} - -struct RunningTurn { - /// Holds the task that handles agent interaction until the end of the turn. - /// Survives across multiple requests as the model performs tool calls and - /// we run tools, report their results. - _task: Task<()>, - /// The current event stream for the running turn. Used to report a final - /// cancellation event if we cancel the turn. - event_stream: ThreadEventStream, - /// The tools that were enabled for this turn. - tools: BTreeMap>, -} - -impl RunningTurn { - fn cancel(self) { - log::debug!("Cancelling in progress turn"); - self.event_stream.send_canceled(); - } -} - -pub struct TokenUsageUpdated(pub Option); - -impl EventEmitter for Thread {} - -pub struct TitleUpdated; - -impl EventEmitter for Thread {} - -pub trait AgentTool -where - Self: 'static + Sized, -{ - type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; - type Output: for<'de> Deserialize<'de> + Serialize + Into; - - fn name() -> &'static str; - - fn description(&self) -> SharedString { - let schema = schemars::schema_for!(Self::Input); - SharedString::new( - schema - .get("description") - .and_then(|description| description.as_str()) - .unwrap_or_default(), - ) - } - - fn kind() -> acp::ToolKind; - - /// The initial tool title to display. Can be updated during the tool run. - fn initial_title( - &self, - input: Result, - cx: &mut App, - ) -> SharedString; - - /// Returns the JSON schema that describes the tool's input. - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema { - crate::tool_schema::root_schema_for::(format) - } - - /// Some tools rely on a provider for the underlying billing or other reasons. - /// Allow the tool to check if they are compatible, or should be filtered out. - fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { - true - } - - /// Runs the tool with the provided input. - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task>; - - /// Emits events for a previous execution of the tool. - fn replay( - &self, - _input: Self::Input, - _output: Self::Output, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Result<()> { - Ok(()) - } - - fn erase(self) -> Arc { - Arc::new(Erased(Arc::new(self))) - } -} - -pub struct Erased(T); - -pub struct AgentToolOutput { - pub llm_output: LanguageModelToolResultContent, - pub raw_output: serde_json::Value, -} - -pub trait AnyAgentTool { - fn name(&self) -> SharedString; - fn description(&self) -> SharedString; - fn kind(&self) -> acp::ToolKind; - fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString; - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; - fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { - true - } - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task>; - fn replay( - &self, - input: serde_json::Value, - output: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()>; -} - -impl AnyAgentTool for Erased> -where - T: AgentTool, -{ - fn name(&self) -> SharedString { - T::name().into() - } - - fn description(&self) -> SharedString { - self.0.description() - } - - fn kind(&self) -> agent_client_protocol::ToolKind { - T::kind() - } - - fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { - let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); - self.0.initial_title(parsed_input, _cx) - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - let mut json = serde_json::to_value(self.0.input_schema(format))?; - adapt_schema_to_format(&mut json, format)?; - Ok(json) - } - - fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { - self.0.supported_provider(provider) - } - - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - cx.spawn(async move |cx| { - let input = serde_json::from_value(input)?; - let output = cx - .update(|cx| self.0.clone().run(input, event_stream, cx))? - .await?; - let raw_output = serde_json::to_value(&output)?; - Ok(AgentToolOutput { - llm_output: output.into(), - raw_output, - }) - }) - } - - fn replay( - &self, - input: serde_json::Value, - output: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()> { - let input = serde_json::from_value(input)?; - let output = serde_json::from_value(output)?; - self.0.replay(input, output, event_stream, cx) - } -} - -#[derive(Clone)] -struct ThreadEventStream(mpsc::UnboundedSender>); - -impl ThreadEventStream { - fn send_user_message(&self, message: &UserMessage) { - self.0 - .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) - .ok(); - } - - fn send_text(&self, text: &str) { - self.0 - .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string()))) - .ok(); - } - - fn send_thinking(&self, text: &str) { - self.0 - .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string()))) - .ok(); - } - - fn send_tool_call( - &self, - id: &LanguageModelToolUseId, - title: SharedString, - kind: acp::ToolKind, - input: serde_json::Value, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( - id, - title.to_string(), - kind, - input, - )))) - .ok(); - } - - fn initial_tool_call( - id: &LanguageModelToolUseId, - title: String, - kind: acp::ToolKind, - input: serde_json::Value, - ) -> acp::ToolCall { - acp::ToolCall { - meta: None, - id: acp::ToolCallId(id.to_string().into()), - title, - kind, - status: acp::ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: Some(input), - raw_output: None, - } - } - - fn update_tool_call_fields( - &self, - tool_use_id: &LanguageModelToolUseId, - fields: acp::ToolCallUpdateFields, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp::ToolCallUpdate { - meta: None, - id: acp::ToolCallId(tool_use_id.to_string().into()), - fields, - } - .into(), - ))) - .ok(); - } - - fn send_retry(&self, status: acp_thread::RetryStatus) { - self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); - } - - fn send_stop(&self, reason: acp::StopReason) { - self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); - } - - fn send_canceled(&self) { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) - .ok(); - } - - fn send_error(&self, error: impl Into) { - self.0.unbounded_send(Err(error.into())).ok(); - } -} - -#[derive(Clone)] -pub struct ToolCallEventStream { - tool_use_id: LanguageModelToolUseId, - stream: ThreadEventStream, - fs: Option>, -} - -impl ToolCallEventStream { - #[cfg(test)] - pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = mpsc::unbounded::>(); - - let stream = ToolCallEventStream::new("test_id".into(), ThreadEventStream(events_tx), None); - - (stream, ToolCallEventStreamReceiver(events_rx)) - } - - fn new( - tool_use_id: LanguageModelToolUseId, - stream: ThreadEventStream, - fs: Option>, - ) -> Self { - Self { - tool_use_id, - stream, - fs, - } - } - - pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) { - self.stream - .update_tool_call_fields(&self.tool_use_id, fields); - } - - pub fn update_diff(&self, diff: Entity) { - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp_thread::ToolCallUpdateDiff { - id: acp::ToolCallId(self.tool_use_id.to_string().into()), - diff, - } - .into(), - ))) - .ok(); - } - - pub fn authorize(&self, title: impl Into, cx: &mut App) -> Task> { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return Task::ready(Ok(())); - } - - let (response_tx, response_rx) = oneshot::channel(); - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization( - ToolCallAuthorization { - tool_call: acp::ToolCallUpdate { - meta: None, - id: acp::ToolCallId(self.tool_use_id.to_string().into()), - fields: acp::ToolCallUpdateFields { - title: Some(title.into()), - ..Default::default() - }, - }, - options: vec![ - acp::PermissionOption { - id: acp::PermissionOptionId("always_allow".into()), - name: "Always Allow".into(), - kind: acp::PermissionOptionKind::AllowAlways, - meta: None, - }, - acp::PermissionOption { - id: acp::PermissionOptionId("allow".into()), - name: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - meta: None, - }, - acp::PermissionOption { - id: acp::PermissionOptionId("deny".into()), - name: "Deny".into(), - kind: acp::PermissionOptionKind::RejectOnce, - meta: None, - }, - ], - response: response_tx, - }, - ))) - .ok(); - let fs = self.fs.clone(); - cx.spawn(async move |cx| match response_rx.await?.0.as_ref() { - "always_allow" => { - if let Some(fs) = fs.clone() { - cx.update(|cx| { - update_settings_file(fs, cx, |settings, _| { - settings - .agent - .get_or_insert_default() - .set_always_allow_tool_actions(true); - }); - })?; - } - - Ok(()) - } - "allow" => Ok(()), - _ => Err(anyhow!("Permission to run tool denied by user")), - }) - } -} - -#[cfg(test)] -pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); - -#[cfg(test)] -impl ToolCallEventStreamReceiver { - pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event { - auth - } else { - panic!("Expected ToolCallAuthorization but got: {:?}", event); - } - } - - pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - update, - )))) = event - { - update.fields - } else { - panic!("Expected update fields but got: {:?}", event); - } - } - - pub async fn expect_diff(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( - update, - )))) = event - { - update.diff - } else { - panic!("Expected diff but got: {:?}", event); - } - } - - pub async fn expect_terminal(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( - update, - )))) = event - { - update.terminal - } else { - panic!("Expected terminal but got: {:?}", event); - } - } -} - -#[cfg(test)] -impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(test)] -impl std::ops::DerefMut for ToolCallEventStreamReceiver { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From<&str> for UserMessageContent { - fn from(text: &str) -> Self { - Self::Text(text.into()) - } -} - -impl From for UserMessageContent { - fn from(value: acp::ContentBlock) -> Self { - match value { - acp::ContentBlock::Text(text_content) => Self::Text(text_content.text), - acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)), - acp::ContentBlock::Audio(_) => { - // TODO - Self::Text("[audio]".to_string()) - } - acp::ContentBlock::ResourceLink(resource_link) => { - match MentionUri::parse(&resource_link.uri) { - Ok(uri) => Self::Mention { - uri, - content: String::new(), - }, - Err(err) => { - log::error!("Failed to parse mention link: {}", err); - Self::Text(format!("[{}]({})", resource_link.name, resource_link.uri)) - } - } - } - acp::ContentBlock::Resource(resource) => match resource.resource { - acp::EmbeddedResourceResource::TextResourceContents(resource) => { - match MentionUri::parse(&resource.uri) { - Ok(uri) => Self::Mention { - uri, - content: resource.text, - }, - Err(err) => { - log::error!("Failed to parse mention link: {}", err); - Self::Text( - MarkdownCodeBlock { - tag: &resource.uri, - text: &resource.text, - } - .to_string(), - ) - } - } - } - acp::EmbeddedResourceResource::BlobResourceContents(_) => { - // TODO - Self::Text("[blob]".to_string()) - } - }, - } - } -} - -impl From for acp::ContentBlock { - fn from(content: UserMessageContent) -> Self { - match content { - UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent { - text, - annotations: None, - meta: None, - }), - UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent { - data: image.source.to_string(), - mime_type: "image/png".to_string(), - meta: None, - annotations: None, - uri: None, - }), - UserMessageContent::Mention { uri, content } => { - acp::ContentBlock::Resource(acp::EmbeddedResource { - meta: None, - resource: acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - meta: None, - mime_type: None, - text: content, - uri: uri.to_uri().to_string(), - }, - ), - annotations: None, - }) - } - } - } -} - -fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { - LanguageModelImage { - source: image_content.data.into(), - // TODO: make this optional? - size: gpui::Size::new(0.into(), 0.into()), - } -} diff --git a/crates/agent2/src/tool_schema.rs b/crates/agent2/src/tool_schema.rs deleted file mode 100644 index f608336b416a72885e52abba58ef472029421e4f..0000000000000000000000000000000000000000 --- a/crates/agent2/src/tool_schema.rs +++ /dev/null @@ -1,43 +0,0 @@ -use language_model::LanguageModelToolSchemaFormat; -use schemars::{ - JsonSchema, Schema, - generate::SchemaSettings, - transform::{Transform, transform_subschemas}, -}; - -pub(crate) fn root_schema_for(format: LanguageModelToolSchemaFormat) -> Schema { - let mut generator = match format { - LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), - LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() - .with(|settings| { - settings.meta_schema = None; - settings.inline_subschemas = true; - }) - .with_transform(ToJsonSchemaSubsetTransform) - .into_generator(), - }; - generator.root_schema_for::() -} - -#[derive(Debug, Clone)] -struct ToJsonSchemaSubsetTransform; - -impl Transform for ToJsonSchemaSubsetTransform { - fn transform(&mut self, schema: &mut Schema) { - // Ensure that the type field is not an array, this happens when we use - // Option, the type will be [T, "null"]. - if let Some(type_field) = schema.get_mut("type") - && let Some(types) = type_field.as_array() - && let Some(first_type) = types.first() - { - *type_field = first_type.clone(); - } - - // oneOf is not supported, use anyOf instead - if let Some(one_of) = schema.remove("oneOf") { - schema.insert("anyOf".to_string(), one_of); - } - - transform_subschemas(self, schema); - } -} diff --git a/crates/agent2/src/tools.rs b/crates/agent2/src/tools.rs deleted file mode 100644 index bcca7eecd185b9381afded26fb573d14f50bc5be..0000000000000000000000000000000000000000 --- a/crates/agent2/src/tools.rs +++ /dev/null @@ -1,60 +0,0 @@ -mod context_server_registry; -mod copy_path_tool; -mod create_directory_tool; -mod delete_path_tool; -mod diagnostics_tool; -mod edit_file_tool; -mod fetch_tool; -mod find_path_tool; -mod grep_tool; -mod list_directory_tool; -mod move_path_tool; -mod now_tool; -mod open_tool; -mod read_file_tool; -mod terminal_tool; -mod thinking_tool; -mod web_search_tool; - -/// A list of all built in tool names, for use in deduplicating MCP tool names -pub fn default_tool_names() -> impl Iterator { - [ - CopyPathTool::name(), - CreateDirectoryTool::name(), - DeletePathTool::name(), - DiagnosticsTool::name(), - EditFileTool::name(), - FetchTool::name(), - FindPathTool::name(), - GrepTool::name(), - ListDirectoryTool::name(), - MovePathTool::name(), - NowTool::name(), - OpenTool::name(), - ReadFileTool::name(), - TerminalTool::name(), - ThinkingTool::name(), - WebSearchTool::name(), - ] - .into_iter() -} - -pub use context_server_registry::*; -pub use copy_path_tool::*; -pub use create_directory_tool::*; -pub use delete_path_tool::*; -pub use diagnostics_tool::*; -pub use edit_file_tool::*; -pub use fetch_tool::*; -pub use find_path_tool::*; -pub use grep_tool::*; -pub use list_directory_tool::*; -pub use move_path_tool::*; -pub use now_tool::*; -pub use open_tool::*; -pub use read_file_tool::*; -pub use terminal_tool::*; -pub use thinking_tool::*; -pub use web_search_tool::*; - -use crate::AgentTool; diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index ec05c95672fa29b6e4813207e3e592fff9d3be15..adab899fadf3b36d199dd13ee19dc8421da9da8f 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -15,10 +15,9 @@ use settings::{ pub use crate::agent_profile::*; -pub const SUMMARIZE_THREAD_PROMPT: &str = - include_str!("../../agent/src/prompts/summarize_thread_prompt.txt"); +pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt"); pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = - include_str!("../../agent/src/prompts/summarize_thread_detailed_prompt.txt"); + include_str!("prompts/summarize_thread_detailed_prompt.txt"); pub fn init(cx: &mut App) { AgentSettings::register(cx); diff --git a/crates/agent/src/prompts/summarize_thread_detailed_prompt.txt b/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt similarity index 100% rename from crates/agent/src/prompts/summarize_thread_detailed_prompt.txt rename to crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt diff --git a/crates/agent/src/prompts/summarize_thread_prompt.txt b/crates/agent_settings/src/prompts/summarize_thread_prompt.txt similarity index 100% rename from crates/agent/src/prompts/summarize_thread_prompt.txt rename to crates/agent_settings/src/prompts/summarize_thread_prompt.txt diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 47d9f6d6a27a2ad5102e831094912208e66a9b43..d8f495c79614ff1aaf23c017160516c1e54065ab 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -20,7 +20,6 @@ acp_thread.workspace = true action_log.workspace = true agent-client-protocol.workspace = true agent.workspace = true -agent2.workspace = true agent_servers.workspace = true agent_settings.workspace = true ai_onboarding.workspace = true @@ -29,7 +28,6 @@ arrayvec.workspace = true assistant_context.workspace = true assistant_slash_command.workspace = true assistant_slash_commands.workspace = true -assistant_tool.workspace = true audio.workspace = true buffer_diff.workspace = true chrono.workspace = true @@ -71,6 +69,7 @@ postage.workspace = true project.workspace = true prompt_store.workspace = true proto.workspace = true +ref-cast.workspace = true release_channel.workspace = true rope.workspace = true rules_library.workspace = true @@ -104,9 +103,7 @@ zed_actions.workspace = true [dev-dependencies] acp_thread = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] } -agent2 = { workspace = true, features = ["test-support"] } assistant_context = { workspace = true, features = ["test-support"] } -assistant_tools.workspace = true buffer_diff = { workspace = true, features = ["test-support"] } db = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 7588e9f53b32302b3a078f44b3cf85be56ca1b4b..73f0622df878c2abc1d2feef945ef2e771dceaf9 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -6,8 +6,8 @@ use std::sync::Arc; use std::sync::atomic::AtomicBool; use acp_thread::MentionUri; +use agent::{HistoryEntry, HistoryStore}; use agent_client_protocol as acp; -use agent2::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId}; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -32,6 +32,7 @@ use crate::context_picker::file_context_picker::{FileMatch, search_files}; use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules}; use crate::context_picker::symbol_context_picker::SymbolMatch; use crate::context_picker::symbol_context_picker::search_symbols; +use crate::context_picker::thread_context_picker::search_threads; use crate::context_picker::{ ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges, }; @@ -938,42 +939,6 @@ impl CompletionProvider for ContextPickerCompletionProvider { } } -pub(crate) fn search_threads( - query: String, - cancellation_flag: Arc, - history_store: &Entity, - cx: &mut App, -) -> Task> { - let threads = history_store.read(cx).entries().collect(); - if query.is_empty() { - return Task::ready(threads); - } - - let executor = cx.background_executor().clone(); - cx.background_spawn(async move { - let candidates = threads - .iter() - .enumerate() - .map(|(id, thread)| StringMatchCandidate::new(id, thread.title())) - .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &cancellation_flag, - executor, - ) - .await; - - matches - .into_iter() - .map(|mat| threads[mat.candidate_id].clone()) - .collect() - }) -} - fn confirm_completion_callback( crease_text: SharedString, start: Anchor, diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index ee506b98810ba51d0fb933a2ca21e650d0cacc0b..8123c4a422b9d95a2da45e75ceb4079675d845fd 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -1,8 +1,8 @@ use std::{cell::RefCell, ops::Range, rc::Rc}; use acp_thread::{AcpThread, AgentThreadEntry}; +use agent::HistoryStore; use agent_client_protocol::{self as acp, ToolCallId}; -use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ @@ -399,9 +399,9 @@ mod tests { use std::{path::Path, rc::Rc}; use acp_thread::{AgentConnection, StubAgentConnection}; + use agent::HistoryStore; use agent_client_protocol as acp; use agent_settings::AgentSettings; - use agent2::HistoryStore; use assistant_context::ContextStore; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::{EditorSettings, RowInfo}; diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index be1c205cee661d401d577b0bcb2d50dc62b4e38c..57157e59c6b48541ff82bdc417bc119ed01bb997 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -3,12 +3,11 @@ use crate::{ context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content}, }; use acp_thread::{MentionUri, selection_name}; +use agent::{HistoryStore, outline}; use agent_client_protocol as acp; use agent_servers::{AgentServer, AgentServerDelegate}; -use agent2::HistoryStore; use anyhow::{Result, anyhow}; use assistant_slash_commands::codeblock_fence_for_path; -use assistant_tool::outline; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, @@ -230,7 +229,7 @@ impl MessageEditor { pub fn insert_thread_summary( &mut self, - thread: agent2::DbThreadMetadata, + thread: agent::DbThreadMetadata, window: &mut Window, cx: &mut Context, ) { @@ -599,7 +598,7 @@ impl MessageEditor { id: acp::SessionId, cx: &mut Context, ) -> Task> { - let server = Rc::new(agent2::NativeAgentServer::new( + let server = Rc::new(agent::NativeAgentServer::new( self.project.read(cx).fs().clone(), self.history_store.clone(), )); @@ -612,7 +611,7 @@ impl MessageEditor { let connection = server.connect(None, delegate, cx); cx.spawn(async move |_, cx| { let (agent, _) = connection.await?; - let agent = agent.downcast::().unwrap(); + let agent = agent.downcast::().unwrap(); let summary = agent .0 .update(cx, |agent, cx| agent.thread_summary(id, cx))? @@ -629,8 +628,8 @@ impl MessageEditor { path: PathBuf, cx: &mut Context, ) -> Task> { - let context = self.history_store.update(cx, |text_thread_store, cx| { - text_thread_store.load_text_thread(path.as_path().into(), cx) + let context = self.history_store.update(cx, |store, cx| { + store.load_text_thread(path.as_path().into(), cx) }); cx.spawn(async move |_, cx| { let context = context.await?; @@ -1589,10 +1588,9 @@ mod tests { use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc}; use acp_thread::MentionUri; + use agent::{HistoryStore, outline}; use agent_client_protocol as acp; - use agent2::HistoryStore; use assistant_context::ContextStore; - use assistant_tool::outline; use editor::{AnchorRangeExt as _, Editor, EditorMode}; use fs::FakeFs; use futures::StreamExt as _; diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index cd696f33fa44976e0784c79d1945b548feb20a50..ee280eb9a123e46ba5cf3b75cdeaf67c4b98b71c 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -1,6 +1,6 @@ use crate::acp::AcpThreadView; use crate::{AgentPanel, RemoveSelectedThread}; -use agent2::{HistoryEntry, HistoryStore}; +use agent::{HistoryEntry, HistoryStore}; use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::StringMatchCandidate; @@ -23,11 +23,8 @@ pub struct AcpThreadHistory { hovered_index: Option, search_editor: Entity, search_query: SharedString, - visible_items: Vec, - local_timezone: UtcOffset, - _update_task: Task<()>, _subscriptions: Vec, } @@ -62,7 +59,7 @@ impl EventEmitter for AcpThreadHistory {} impl AcpThreadHistory { pub(crate) fn new( - history_store: Entity, + history_store: Entity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -642,7 +639,7 @@ impl RenderOnce for AcpHistoryEntryElement { if let Some(panel) = workspace.read(cx).panel::(cx) { panel.update(cx, |panel, cx| { panel - .open_saved_prompt_editor( + .open_saved_text_thread( context.path.clone(), window, cx, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index a5af5b2521894b2051e6edfbe8677aa86177f6f1..cb2e8be2701c2152ef889f7bdc9925f8014f9519 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5,10 +5,10 @@ use acp_thread::{ }; use acp_thread::{AgentConnection, Plan}; use action_log::ActionLog; +use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; use agent_client_protocol::{self as acp, PromptCapabilities}; use agent_servers::{AgentServer, AgentServerDelegate}; use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; -use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; use anyhow::{Result, anyhow, bail}; use arrayvec::ArrayVec; use audio::{Audio, Sound}; @@ -117,7 +117,7 @@ impl ThreadError { } } -impl ProfileProvider for Entity { +impl ProfileProvider for Entity { fn profile_id(&self, cx: &App) -> AgentProfileId { self.read(cx).profile().clone() } @@ -529,7 +529,7 @@ impl AcpThreadView { let result = if let Some(native_agent) = connection .clone() - .downcast::() + .downcast::() && let Some(resume) = resume_thread.clone() { cx.update(|_, cx| { @@ -3106,7 +3106,7 @@ impl AcpThreadView { let render_history = self .agent .clone() - .downcast::() + .downcast::() .is_some() && self .history_store @@ -4011,12 +4011,12 @@ impl AcpThreadView { pub(crate) fn as_native_connection( &self, cx: &App, - ) -> Option> { + ) -> Option> { let acp_thread = self.thread()?.read(cx); acp_thread.connection().clone().downcast() } - pub(crate) fn as_native_thread(&self, cx: &App) -> Option> { + pub(crate) fn as_native_thread(&self, cx: &App) -> Option> { let acp_thread = self.thread()?.read(cx); self.as_native_connection(cx)? .thread(acp_thread.session_id(), cx) @@ -4404,7 +4404,7 @@ impl AcpThreadView { if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel - .open_saved_prompt_editor(path.as_path().into(), window, cx) + .open_saved_text_thread(path.as_path().into(), window, cx) .detach_and_log_err(cx); }); } @@ -5137,7 +5137,7 @@ impl AcpThreadView { if self .agent .clone() - .downcast::() + .downcast::() .is_some() { // Native agent - use the model name diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 386eeaca1924ace86e4138fdfc283bfe0c20fae0..ef0d4735d2d7690111ee2549cdee8ab31e32196e 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -6,8 +6,8 @@ mod tool_picker; use std::{ops::Range, sync::Arc}; +use agent::ContextServerRegistry; use anyhow::Result; -use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::{Plan, PlanV1, PlanV2}; use collections::HashMap; use context_server::ContextServerId; @@ -17,7 +17,7 @@ use extension_host::ExtensionStore; use fs::Fs; use gpui::{ Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable, - Hsla, ScrollHandle, Subscription, Task, WeakEntity, + ScrollHandle, Subscription, Task, WeakEntity, }; use language::LanguageRegistry; use language_model::{ @@ -54,9 +54,8 @@ pub struct AgentConfiguration { focus_handle: FocusHandle, configuration_views_by_provider: HashMap, context_server_store: Entity, - expanded_context_server_tools: HashMap, expanded_provider_configurations: HashMap, - tools: Entity, + context_server_registry: Entity, _registry_subscription: Subscription, scroll_handle: ScrollHandle, _check_for_gemini: Task<()>, @@ -67,7 +66,7 @@ impl AgentConfiguration { fs: Arc, agent_server_store: Entity, context_server_store: Entity, - tools: Entity, + context_server_registry: Entity, language_registry: Arc, workspace: WeakEntity, window: &mut Window, @@ -103,9 +102,8 @@ impl AgentConfiguration { configuration_views_by_provider: HashMap::default(), agent_server_store, context_server_store, - expanded_context_server_tools: HashMap::default(), expanded_provider_configurations: HashMap::default(), - tools, + context_server_registry, _registry_subscription: registry_subscription, scroll_handle: ScrollHandle::new(), _check_for_gemini: Task::ready(()), @@ -438,10 +436,6 @@ impl AgentConfiguration { } } - fn card_item_border_color(&self, cx: &mut Context) -> Hsla { - cx.theme().colors().border.opacity(0.6) - } - fn render_context_servers_section( &mut self, window: &mut Window, @@ -567,7 +561,6 @@ impl AgentConfiguration { window: &mut Window, cx: &mut Context, ) -> impl use<> + IntoElement { - let tools_by_source = self.tools.read(cx).tools_by_source(cx); let server_status = self .context_server_store .read(cx) @@ -596,17 +589,11 @@ impl AgentConfiguration { None }; - let are_tools_expanded = self - .expanded_context_server_tools - .get(&context_server_id) - .copied() - .unwrap_or_default(); - let tools = tools_by_source - .get(&ToolSource::ContextServer { - id: context_server_id.0.clone().into(), - }) - .map_or([].as_slice(), |tools| tools.as_slice()); - let tool_count = tools.len(); + let tool_count = self + .context_server_registry + .read(cx) + .tools_for_server(&context_server_id) + .count(); let (source_icon, source_tooltip) = if is_from_extension { ( @@ -660,7 +647,7 @@ impl AgentConfiguration { let language_registry = self.language_registry.clone(); let context_server_store = self.context_server_store.clone(); let workspace = self.workspace.clone(); - let tools = self.tools.clone(); + let context_server_registry = self.context_server_registry.clone(); move |window, cx| { Some(ContextMenu::build(window, cx, |menu, _window, _cx| { @@ -678,20 +665,16 @@ impl AgentConfiguration { ) .detach_and_log_err(cx); } - }).when(tool_count >= 1, |this| this.entry("View Tools", None, { + }).when(tool_count > 0, |this| this.entry("View Tools", None, { let context_server_id = context_server_id.clone(); - let tools = tools.clone(); + let context_server_registry = context_server_registry.clone(); let workspace = workspace.clone(); - move |window, cx| { let context_server_id = context_server_id.clone(); - let tools = tools.clone(); - let workspace = workspace.clone(); - workspace.update(cx, |workspace, cx| { ConfigureContextServerToolsModal::toggle( context_server_id, - tools, + context_server_registry.clone(), workspace, window, cx, @@ -773,14 +756,6 @@ impl AgentConfiguration { .child( h_flex() .justify_between() - .when( - error.is_none() && are_tools_expanded && tool_count >= 1, - |element| { - element - .border_b_1() - .border_color(self.card_item_border_color(cx)) - }, - ) .child( h_flex() .flex_1() @@ -904,11 +879,6 @@ impl AgentConfiguration { ), ); } - - if !are_tools_expanded || tools.is_empty() { - return parent; - } - parent }) } diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs index 5a59806972ecf1b6cbc0702809c98acf1a86b387..3fe0b8d1b1400b4362192261995ed5b6bd1cb662 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs @@ -1,4 +1,5 @@ -use assistant_tool::{ToolSource, ToolWorkingSet}; +use agent::ContextServerRegistry; +use collections::HashMap; use context_server::ContextServerId; use gpui::{ DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Window, prelude::*, @@ -8,37 +9,37 @@ use workspace::{ModalView, Workspace}; pub struct ConfigureContextServerToolsModal { context_server_id: ContextServerId, - tools: Entity, + context_server_registry: Entity, focus_handle: FocusHandle, - expanded_tools: std::collections::HashMap, + expanded_tools: HashMap, scroll_handle: ScrollHandle, } impl ConfigureContextServerToolsModal { fn new( context_server_id: ContextServerId, - tools: Entity, + context_server_registry: Entity, _window: &mut Window, cx: &mut Context, ) -> Self { Self { context_server_id, - tools, + context_server_registry, focus_handle: cx.focus_handle(), - expanded_tools: std::collections::HashMap::new(), + expanded_tools: HashMap::default(), scroll_handle: ScrollHandle::new(), } } pub fn toggle( context_server_id: ContextServerId, - tools: Entity, + context_server_registry: Entity, workspace: &mut Workspace, window: &mut Window, cx: &mut Context, ) { workspace.toggle_modal(window, cx, |window, cx| { - Self::new(context_server_id, tools, window, cx) + Self::new(context_server_id, context_server_registry, window, cx) }); } @@ -51,13 +52,11 @@ impl ConfigureContextServerToolsModal { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let tools_by_source = self.tools.read(cx).tools_by_source(cx); - let server_tools = tools_by_source - .get(&ToolSource::ContextServer { - id: self.context_server_id.0.clone().into(), - }) - .map(|tools| tools.as_slice()) - .unwrap_or(&[]); + let tools = self + .context_server_registry + .read(cx) + .tools_for_server(&self.context_server_id) + .collect::>(); div() .size_full() @@ -70,11 +69,11 @@ impl ConfigureContextServerToolsModal { .max_h_128() .overflow_y_scroll() .track_scroll(&self.scroll_handle) - .children(server_tools.iter().enumerate().flat_map(|(index, tool)| { + .children(tools.iter().enumerate().flat_map(|(index, tool)| { let tool_name = tool.name(); let is_expanded = self .expanded_tools - .get(&tool_name) + .get(tool_name.as_ref()) .copied() .unwrap_or(false); @@ -110,7 +109,7 @@ impl ConfigureContextServerToolsModal { move |this, _event, _window, _cx| { let current = this .expanded_tools - .get(&tool_name) + .get(tool_name.as_ref()) .copied() .unwrap_or(false); this.expanded_tools @@ -127,7 +126,7 @@ impl ConfigureContextServerToolsModal { .into_any_element(), ]; - if index < server_tools.len() - 1 { + if index < tools.len() - 1 { items.push( h_flex() .w_full() diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index 9a7f0ed602a52d3b27dde565383453f2c5c325fb..fc4bde2c784894b94b7ce35e6e262e52865ffcd1 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -2,8 +2,8 @@ mod profile_modal_header; use std::sync::Arc; +use agent::ContextServerRegistry; use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profiles}; -use assistant_tool::ToolWorkingSet; use editor::Editor; use fs::Fs; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*}; @@ -17,8 +17,6 @@ use crate::agent_configuration::manage_profiles_modal::profile_modal_header::Pro use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate}; use crate::{AgentPanel, ManageProfiles}; -use super::tool_picker::ToolPickerMode; - enum Mode { ChooseProfile(ChooseProfileMode), NewProfile(NewProfileMode), @@ -97,7 +95,7 @@ pub struct NewProfileMode { pub struct ManageProfilesModal { fs: Arc, - tools: Entity, + context_server_registry: Entity, focus_handle: FocusHandle, mode: Mode, } @@ -111,10 +109,9 @@ impl ManageProfilesModal { workspace.register_action(|workspace, action: &ManageProfiles, window, cx| { if let Some(panel) = workspace.panel::(cx) { let fs = workspace.app_state().fs.clone(); - let thread_store = panel.read(cx).thread_store(); - let tools = thread_store.read(cx).tools(); + let context_server_registry = panel.read(cx).context_server_registry().clone(); workspace.toggle_modal(window, cx, |window, cx| { - let mut this = Self::new(fs, tools, window, cx); + let mut this = Self::new(fs, context_server_registry, window, cx); if let Some(profile_id) = action.customize_tools.clone() { this.configure_builtin_tools(profile_id, window, cx); @@ -128,7 +125,7 @@ impl ManageProfilesModal { pub fn new( fs: Arc, - tools: Entity, + context_server_registry: Entity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -136,7 +133,7 @@ impl ManageProfilesModal { Self { fs, - tools, + context_server_registry, focus_handle, mode: Mode::choose_profile(window, cx), } @@ -193,10 +190,9 @@ impl ManageProfilesModal { }; let tool_picker = cx.new(|cx| { - let delegate = ToolPickerDelegate::new( - ToolPickerMode::McpTools, + let delegate = ToolPickerDelegate::mcp_tools( + &self.context_server_registry, self.fs.clone(), - self.tools.clone(), profile_id.clone(), profile, cx, @@ -230,10 +226,12 @@ impl ManageProfilesModal { }; let tool_picker = cx.new(|cx| { - let delegate = ToolPickerDelegate::new( - ToolPickerMode::BuiltinTools, + let delegate = ToolPickerDelegate::builtin_tools( + //todo: This causes the web search tool to show up even it only works when using zed hosted models + agent::built_in_tool_names() + .map(|s| s.into()) + .collect::>(), self.fs.clone(), - self.tools.clone(), profile_id.clone(), profile, cx, diff --git a/crates/agent_ui/src/agent_configuration/tool_picker.rs b/crates/agent_ui/src/agent_configuration/tool_picker.rs index c624948944c0624e75e385d1b4b15aa77fea9bcd..6b84205e1bd6336d70751090d8f0451b1b1925b0 100644 --- a/crates/agent_ui/src/agent_configuration/tool_picker.rs +++ b/crates/agent_ui/src/agent_configuration/tool_picker.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, sync::Arc}; +use agent::ContextServerRegistry; use agent_settings::{AgentProfileId, AgentProfileSettings}; -use assistant_tool::{ToolSource, ToolWorkingSet}; use fs::Fs; use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window}; use picker::{Picker, PickerDelegate}; @@ -14,7 +14,7 @@ pub struct ToolPicker { } #[derive(Clone, Copy, Debug, PartialEq)] -pub enum ToolPickerMode { +enum ToolPickerMode { BuiltinTools, McpTools, } @@ -76,59 +76,79 @@ pub struct ToolPickerDelegate { } impl ToolPickerDelegate { - pub fn new( - mode: ToolPickerMode, + pub fn builtin_tools( + tool_names: Vec>, fs: Arc, - tool_set: Entity, profile_id: AgentProfileId, profile_settings: AgentProfileSettings, cx: &mut Context, ) -> Self { - let items = Arc::new(Self::resolve_items(mode, &tool_set, cx)); + Self::new( + Arc::new( + tool_names + .into_iter() + .map(|name| PickerItem::Tool { + name, + server_id: None, + }) + .collect(), + ), + ToolPickerMode::BuiltinTools, + fs, + profile_id, + profile_settings, + cx, + ) + } + pub fn mcp_tools( + registry: &Entity, + fs: Arc, + profile_id: AgentProfileId, + profile_settings: AgentProfileSettings, + cx: &mut Context, + ) -> Self { + let mut items = Vec::new(); + + for (id, tools) in registry.read(cx).servers() { + let server_id = id.clone().0; + items.push(PickerItem::ContextServer { + server_id: server_id.clone(), + }); + items.extend(tools.keys().map(|tool_name| PickerItem::Tool { + name: tool_name.clone().into(), + server_id: Some(server_id.clone()), + })); + } + + Self::new( + Arc::new(items), + ToolPickerMode::McpTools, + fs, + profile_id, + profile_settings, + cx, + ) + } + + fn new( + items: Arc>, + mode: ToolPickerMode, + fs: Arc, + profile_id: AgentProfileId, + profile_settings: AgentProfileSettings, + cx: &mut Context, + ) -> Self { Self { tool_picker: cx.entity().downgrade(), + mode, fs, items, profile_id, profile_settings, filtered_items: Vec::new(), selected_index: 0, - mode, - } - } - - fn resolve_items( - mode: ToolPickerMode, - tool_set: &Entity, - cx: &mut App, - ) -> Vec { - let mut items = Vec::new(); - for (source, tools) in tool_set.read(cx).tools_by_source(cx) { - match source { - ToolSource::Native => { - if mode == ToolPickerMode::BuiltinTools { - items.extend(tools.into_iter().map(|tool| PickerItem::Tool { - name: tool.name().into(), - server_id: None, - })); - } - } - ToolSource::ContextServer { id } => { - if mode == ToolPickerMode::McpTools && !tools.is_empty() { - let server_id: Arc = id.clone().into(); - items.push(PickerItem::ContextServer { - server_id: server_id.clone(), - }); - items.extend(tools.into_iter().map(|tool| PickerItem::Tool { - name: tool.name().into(), - server_id: Some(server_id.clone()), - })); - } - } - } } - items } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index bcba02e3cf2056a27b58b53ab6947b8775e4bfda..2def41c74dd715637f269e572342d04b944505b5 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::sync::Arc; use acp_thread::AcpThread; -use agent2::{DbThreadMetadata, HistoryEntry}; +use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore}; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use project::agent_server_store::{ AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME, @@ -17,6 +17,7 @@ use zed_actions::OpenBrowser; use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; +use crate::context_store::ContextStore; use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::{ AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant, @@ -32,16 +33,11 @@ use crate::{ use crate::{ ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command, }; -use agent::{ - context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, -}; use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; -use assistant_tool::ToolWorkingSet; use client::{UserStore, zed_urls}; use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; @@ -118,7 +114,7 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &NewTextThread, window, cx| { if let Some(panel) = workspace.panel::(cx) { workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); + panel.update(cx, |panel, cx| panel.new_text_thread(window, cx)); } }) .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { @@ -281,7 +277,7 @@ impl ActiveView { pub fn native_agent( fs: Arc, prompt_store: Option>, - acp_history_store: Entity, + history_store: Entity, project: Entity, workspace: WeakEntity, window: &mut Window, @@ -289,12 +285,12 @@ impl ActiveView { ) -> Self { let thread_view = cx.new(|cx| { crate::acp::AcpThreadView::new( - ExternalAgent::NativeAgent.server(fs, acp_history_store.clone()), + ExternalAgent::NativeAgent.server(fs, history_store.clone()), None, None, workspace, project, - acp_history_store, + history_store, prompt_store, window, cx, @@ -304,9 +300,9 @@ impl ActiveView { Self::ExternalAgentThread { thread_view } } - pub fn prompt_editor( + pub fn text_thread( context_editor: Entity, - acp_history_store: Entity, + acp_history_store: Entity, language_registry: Arc, window: &mut Window, cx: &mut App, @@ -379,7 +375,7 @@ impl ActiveView { .replace_recently_opened_text_thread(old_path, new_path, cx); } else { history_store.push_recently_opened_entry( - agent2::HistoryEntryId::TextThread(new_path.clone()), + agent::HistoryEntryId::TextThread(new_path.clone()), cx, ); } @@ -412,11 +408,11 @@ pub struct AgentPanel { project: Entity, fs: Arc, language_registry: Arc, - thread_store: Entity, acp_history: Entity, - history_store: Entity, - context_store: Entity, + history_store: Entity, + text_thread_store: Entity, prompt_store: Option>, + context_server_registry: Entity, inline_assist_context_store: Entity, configuration: Option>, configuration_subscription: Option, @@ -424,8 +420,8 @@ pub struct AgentPanel { previous_view: Option, new_thread_menu_handle: PopoverMenuHandle, agent_panel_menu_handle: PopoverMenuHandle, - assistant_navigation_menu_handle: PopoverMenuHandle, - assistant_navigation_menu: Option>, + agent_navigation_menu_handle: PopoverMenuHandle, + agent_navigation_menu: Option>, width: Option, height: Option, zoomed: bool, @@ -463,33 +459,6 @@ impl AgentPanel { Ok(prompt_store) => prompt_store.await.ok(), Err(_) => None, }; - let tools = cx.new(|_| ToolWorkingSet::default())?; - let thread_store = workspace - .update(cx, |workspace, cx| { - let project = workspace.project().clone(); - ThreadStore::load( - project, - tools.clone(), - prompt_store.clone(), - prompt_builder.clone(), - cx, - ) - })? - .await?; - - let slash_commands = Arc::new(SlashCommandWorkingSet::default()); - let context_store = workspace - .update(cx, |workspace, cx| { - let project = workspace.project().clone(); - assistant_context::ContextStore::new( - project, - prompt_builder.clone(), - slash_commands, - cx, - ) - })? - .await?; - let serialized_panel = if let Some(panel) = cx .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) }) .await @@ -501,17 +470,22 @@ impl AgentPanel { None }; - let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| { - Self::new( - workspace, - thread_store, - context_store, - prompt_store, - window, + let slash_commands = Arc::new(SlashCommandWorkingSet::default()); + let text_thread_store = workspace + .update(cx, |workspace, cx| { + let project = workspace.project().clone(); + assistant_context::ContextStore::new( + project, + prompt_builder, + slash_commands, cx, ) - }); + })? + .await?; + + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = + cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx)); panel.as_mut(cx).loading = true; if let Some(serialized_panel) = serialized_panel { @@ -538,8 +512,7 @@ impl AgentPanel { fn new( workspace: &Workspace, - thread_store: Entity, - context_store: Entity, + text_thread_store: Entity, prompt_store: Option>, window: &mut Window, cx: &mut Context, @@ -551,10 +524,11 @@ impl AgentPanel { let client = workspace.client().clone(); let workspace = workspace.weak_handle(); - let inline_assist_context_store = - cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); + let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade())); + let context_server_registry = + cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx)); + let history_store = cx.new(|cx| agent::HistoryStore::new(text_thread_store.clone(), cx)); let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx)); cx.subscribe_in( &acp_history, @@ -570,7 +544,7 @@ impl AgentPanel { ); } ThreadHistoryEvent::Open(HistoryEntry::TextThread(thread)) => { - this.open_saved_prompt_editor(thread.path.clone(), window, cx) + this.open_saved_text_thread(thread.path.clone(), window, cx) .detach_and_log_err(cx); } }, @@ -589,8 +563,7 @@ impl AgentPanel { cx, ), DefaultView::TextThread => { - let context = - context_store.update(cx, |context_store, cx| context_store.create(cx)); + let context = text_thread_store.update(cx, |store, cx| store.create(cx)); let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap(); let context_editor = cx.new(|cx| { let mut editor = TextThreadEditor::for_context( @@ -605,7 +578,7 @@ impl AgentPanel { editor.insert_default_prompt(window, cx); editor }); - ActiveView::prompt_editor( + ActiveView::text_thread( context_editor, history_store.clone(), language_registry.clone(), @@ -619,7 +592,7 @@ impl AgentPanel { window.defer(cx, move |window, cx| { let panel = weak_panel.clone(); - let assistant_navigation_menu = + let agent_navigation_menu = ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| { if let Some(panel) = panel.upgrade() { menu = Self::populate_recently_opened_menu_section(menu, panel, cx); @@ -633,7 +606,7 @@ impl AgentPanel { weak_panel .update(cx, |panel, cx| { cx.subscribe_in( - &assistant_navigation_menu, + &agent_navigation_menu, window, |_, menu, _: &DismissEvent, window, cx| { menu.update(cx, |menu, _| { @@ -643,7 +616,7 @@ impl AgentPanel { }, ) .detach(); - panel.assistant_navigation_menu = Some(assistant_navigation_menu); + panel.agent_navigation_menu = Some(agent_navigation_menu); }) .ok(); }); @@ -666,17 +639,17 @@ impl AgentPanel { project: project.clone(), fs: fs.clone(), language_registry, - thread_store: thread_store.clone(), - context_store, + text_thread_store, prompt_store, configuration: None, configuration_subscription: None, + context_server_registry, inline_assist_context_store, previous_view: None, new_thread_menu_handle: PopoverMenuHandle::default(), agent_panel_menu_handle: PopoverMenuHandle::default(), - assistant_navigation_menu_handle: PopoverMenuHandle::default(), - assistant_navigation_menu: None, + agent_navigation_menu_handle: PopoverMenuHandle::default(), + agent_navigation_menu: None, width: None, height: None, zoomed: false, @@ -711,12 +684,12 @@ impl AgentPanel { &self.inline_assist_context_store } - pub(crate) fn thread_store(&self) -> &Entity { - &self.thread_store + pub(crate) fn thread_store(&self) -> &Entity { + &self.history_store } - pub(crate) fn text_thread_store(&self) -> &Entity { - &self.context_store + pub(crate) fn context_server_registry(&self) -> &Entity { + &self.context_server_registry } fn active_thread_view(&self) -> Option<&Entity> { @@ -753,11 +726,11 @@ impl AgentPanel { ); } - fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context) { + fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context) { telemetry::event!("Agent Thread Started", agent = "zed-text"); let context = self - .context_store + .text_thread_store .update(cx, |context_store, cx| context_store.create(cx)); let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx) .log_err() @@ -783,7 +756,7 @@ impl AgentPanel { } self.set_active_view( - ActiveView::prompt_editor( + ActiveView::text_thread( context_editor.clone(), self.history_store.clone(), self.language_registry.clone(), @@ -921,32 +894,29 @@ impl AgentPanel { self.set_active_view(previous_view, window, cx); } } else { - self.thread_store - .update(cx, |thread_store, cx| thread_store.reload(cx)) - .detach_and_log_err(cx); self.set_active_view(ActiveView::History, window, cx); } cx.notify(); } - pub(crate) fn open_saved_prompt_editor( + pub(crate) fn open_saved_text_thread( &mut self, path: Arc, window: &mut Window, cx: &mut Context, ) -> Task> { let context = self - .context_store - .update(cx, |store, cx| store.open_local_context(path, cx)); + .history_store + .update(cx, |store, cx| store.load_text_thread(path, cx)); cx.spawn_in(window, async move |this, cx| { let context = context.await?; this.update_in(cx, |this, window, cx| { - this.open_prompt_editor(context, window, cx); + this.open_text_thread(context, window, cx); }) }) } - pub(crate) fn open_prompt_editor( + pub(crate) fn open_text_thread( &mut self, context: Entity, window: &mut Window, @@ -973,7 +943,7 @@ impl AgentPanel { } self.set_active_view( - ActiveView::prompt_editor( + ActiveView::text_thread( editor, self.history_store.clone(), self.language_registry.clone(), @@ -1013,7 +983,7 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - self.assistant_navigation_menu_handle.toggle(window, cx); + self.agent_navigation_menu_handle.toggle(window, cx); } pub fn toggle_options_menu( @@ -1106,7 +1076,6 @@ impl AgentPanel { pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context) { let agent_server_store = self.project.read(cx).agent_server_store().clone(); let context_server_store = self.project.read(cx).context_server_store(); - let tools = self.thread_store.read(cx).tools(); let fs = self.fs.clone(); self.set_active_view(ActiveView::Configuration, window, cx); @@ -1115,7 +1084,7 @@ impl AgentPanel { fs, agent_server_store, context_server_store, - tools, + self.context_server_registry.clone(), self.language_registry.clone(), self.workspace.clone(), window, @@ -1183,7 +1152,7 @@ impl AgentPanel { }); } - self.new_thread(&NewThread::default(), window, cx); + self.new_thread(&NewThread, window, cx); if let Some((thread, model)) = self .active_native_agent_thread(cx) .zip(provider.default_model(cx)) @@ -1205,7 +1174,7 @@ impl AgentPanel { } } - pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option> { + pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option> { match &self.active_view { ActiveView::ExternalAgentThread { thread_view, .. } => { thread_view.read(cx).as_native_thread(cx) @@ -1241,7 +1210,7 @@ impl AgentPanel { self.history_store.update(cx, |store, cx| { if let Some(path) = context_editor.read(cx).context().read(cx).path() { store.push_recently_opened_entry( - agent2::HistoryEntryId::TextThread(path.clone()), + agent::HistoryEntryId::TextThread(path.clone()), cx, ) } @@ -1295,15 +1264,15 @@ impl AgentPanel { let entry = entry.clone(); panel .update(cx, move |this, cx| match &entry { - agent2::HistoryEntry::AcpThread(entry) => this.external_thread( + agent::HistoryEntry::AcpThread(entry) => this.external_thread( Some(ExternalAgent::NativeAgent), Some(entry.clone()), None, window, cx, ), - agent2::HistoryEntry::TextThread(entry) => this - .open_saved_prompt_editor(entry.path.clone(), window, cx) + agent::HistoryEntry::TextThread(entry) => this + .open_saved_text_thread(entry.path.clone(), window, cx) .detach_and_log_err(cx), }) .ok(); @@ -1730,9 +1699,9 @@ impl AgentPanel { }, ) .anchor(corner) - .with_handle(self.assistant_navigation_menu_handle.clone()) + .with_handle(self.agent_navigation_menu_handle.clone()) .menu({ - let menu = self.assistant_navigation_menu.clone(); + let menu = self.agent_navigation_menu.clone(); move |window, cx| { telemetry::event!("View Thread History Clicked"); @@ -1832,7 +1801,7 @@ impl AgentPanel { }) .item( ContextMenuEntry::new("New Thread") - .action(NewThread::default().boxed_clone()) + .action(NewThread.boxed_clone()) .icon(IconName::Thread) .icon_color(Color::Muted) .handler({ @@ -2278,7 +2247,7 @@ impl AgentPanel { } } - fn render_prompt_editor( + fn render_text_thread( &self, context_editor: &Entity, buffer_search_bar: &Entity, @@ -2409,8 +2378,8 @@ impl AgentPanel { let mut key_context = KeyContext::new_with_defaults(); key_context.add("AgentPanel"); match &self.active_view { - ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"), - ActiveView::TextThread { .. } => key_context.add("prompt_editor"), + ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"), + ActiveView::TextThread { .. } => key_context.add("text_thread"), ActiveView::History | ActiveView::Configuration => {} } key_context @@ -2487,7 +2456,7 @@ impl Render for AgentPanel { this } }) - .child(self.render_prompt_editor( + .child(self.render_text_thread( context_editor, buffer_search_bar, window, @@ -2538,8 +2507,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { }; let prompt_store = None; let thread_store = None; - let text_thread_store = None; - let context_store = cx.new(|_| ContextStore::new(project.clone(), None)); + let context_store = cx.new(|_| ContextStore::new(project.clone())); assistant.assist( prompt_editor, self.workspace.clone(), @@ -2547,7 +2515,6 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { project, prompt_store, thread_store, - text_thread_store, initial_prompt, window, cx, @@ -2590,7 +2557,7 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { }; panel.update(cx, |panel, cx| { - panel.open_saved_prompt_editor(path, window, cx) + panel.open_saved_text_thread(path, window, cx) }) } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 26d37378776b52be5fb88f3dad820986fb812d07..7c31500c937a6513c932c66560cf8754cbafbf1c 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -4,8 +4,10 @@ mod agent_diff; mod agent_model_selector; mod agent_panel; mod buffer_codegen; +mod context; mod context_picker; mod context_server_configuration; +mod context_store; mod context_strip; mod inline_assistant; mod inline_prompt_editor; @@ -22,7 +24,6 @@ mod ui; use std::rc::Rc; use std::sync::Arc; -use agent::ThreadId; use agent_settings::{AgentProfileId, AgentSettings}; use assistant_slash_command::SlashCommandRegistry; use client::Client; @@ -139,10 +140,7 @@ pub struct QuoteSelection; #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] #[serde(deny_unknown_fields)] -pub struct NewThread { - #[serde(default)] - from_thread_id: Option, -} +pub struct NewThread; /// Creates a new external agent conversation thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] @@ -196,13 +194,13 @@ impl ExternalAgent { pub fn server( &self, fs: Arc, - history: Entity, + history: Entity, ) -> Rc { match self { Self::Gemini => Rc::new(agent_servers::Gemini), Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode), Self::Codex => Rc::new(agent_servers::Codex), - Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)), + Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)), Self::Custom { name, command: _ } => { Rc::new(agent_servers::CustomAgentServer::new(name.clone())) } @@ -266,7 +264,6 @@ pub fn init( init_language_model_settings(cx); } assistant_slash_command::init(cx); - agent::init(fs.clone(), cx); agent_panel::init(cx); context_server_configuration::init(language_registry.clone(), fs.clone(), cx); TextThreadEditor::init(cx); diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 2309aad754aee55af5ad040c39d22304486446a4..215e2a74d7be9cbcb18442dcefa1581d08eec7b2 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -1,7 +1,5 @@ -use crate::inline_prompt_editor::CodegenStatus; -use agent::{ - ContextStore, - context::{ContextLoadResult, load_context}, +use crate::{ + context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus, }; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; @@ -434,16 +432,16 @@ impl CodegenAlternative { .generate_inline_transformation_prompt(user_prompt, language_name, buffer, range) .context("generating content prompt")?; - let context_task = self.context_store.as_ref().map(|context_store| { + let context_task = self.context_store.as_ref().and_then(|context_store| { if let Some(project) = self.project.upgrade() { let context = context_store .read(cx) .context() .cloned() .collect::>(); - load_context(context, &project, &self.prompt_store, cx) + Some(load_context(context, &project, &self.prompt_store, cx)) } else { - Task::ready(ContextLoadResult::default()) + None } }); @@ -459,7 +457,6 @@ impl CodegenAlternative { if let Some(context_task) = context_task { context_task .await - .loaded_context .add_to_request_message(&mut request_message); } diff --git a/crates/agent/src/context.rs b/crates/agent_ui/src/context.rs similarity index 90% rename from crates/agent/src/context.rs rename to crates/agent_ui/src/context.rs index 3b2922087a94c497c07f1df67a8d4d9adf759909..3d0600605153fd8343205f3889953c100bde7a7a 100644 --- a/crates/agent/src/context.rs +++ b/crates/agent_ui/src/context.rs @@ -1,11 +1,8 @@ -use crate::thread::Thread; +use agent::outline; use assistant_context::AssistantContext; -use assistant_tool::outline; -use collections::HashSet; use futures::future; use futures::{FutureExt, future::Shared}; use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task}; -use icons::IconName; use language::Buffer; use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent}; use project::{Project, ProjectEntryId, ProjectPath, Worktree}; @@ -17,6 +14,7 @@ use std::hash::{Hash, Hasher}; use std::path::PathBuf; use std::{ops::Range, path::Path, sync::Arc}; use text::{Anchor, OffsetRangeExt as _}; +use ui::IconName; use util::markdown::MarkdownCodeBlock; use util::rel_path::RelPath; use util::{ResultExt as _, post_inc}; @@ -181,7 +179,7 @@ impl FileContextHandle { }) } - fn load(self, cx: &App) -> Task>)>> { + fn load(self, cx: &App) -> Task> { let buffer_ref = self.buffer.read(cx); let Some(file) = buffer_ref.file() else { log::error!("file context missing path"); @@ -206,7 +204,7 @@ impl FileContextHandle { text: buffer_content.text.into(), is_outline: buffer_content.is_outline, }); - Some((context, vec![buffer])) + Some(context) }) } } @@ -256,11 +254,7 @@ impl DirectoryContextHandle { self.entry_id.hash(state) } - fn load( - self, - project: Entity, - cx: &mut App, - ) -> Task>)>> { + fn load(self, project: Entity, cx: &mut App) -> Task> { let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else { return Task::ready(None); }; @@ -307,7 +301,7 @@ impl DirectoryContextHandle { }); cx.background_spawn(async move { - let (rope, buffer) = rope_task.await?; + let (rope, _buffer) = rope_task.await?; let fenced_codeblock = MarkdownCodeBlock { tag: &codeblock_tag(&full_path, None), text: &rope.to_string(), @@ -318,18 +312,22 @@ impl DirectoryContextHandle { rel_path, fenced_codeblock, }; - Some((descendant, buffer)) + Some(descendant) }) })); cx.background_spawn(async move { - let (descendants, buffers) = descendants_future.await.into_iter().flatten().unzip(); + let descendants = descendants_future + .await + .into_iter() + .flatten() + .collect::>(); let context = AgentContext::Directory(DirectoryContext { handle: self, full_path: directory_full_path, descendants, }); - Some((context, buffers)) + Some(context) }) } } @@ -397,7 +395,7 @@ impl SymbolContextHandle { .into() } - fn load(self, cx: &App) -> Task>)>> { + fn load(self, cx: &App) -> Task> { let buffer_ref = self.buffer.read(cx); let Some(file) = buffer_ref.file() else { log::error!("symbol context's file has no path"); @@ -406,14 +404,13 @@ impl SymbolContextHandle { let full_path = file.full_path(cx).to_string_lossy().into_owned(); let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot()); let text = self.text(cx); - let buffer = self.buffer.clone(); let context = AgentContext::Symbol(SymbolContext { handle: self, full_path, line_range, text, }); - Task::ready(Some((context, vec![buffer]))) + Task::ready(Some(context)) } } @@ -468,13 +465,12 @@ impl SelectionContextHandle { .into() } - fn load(self, cx: &App) -> Task>)>> { + fn load(self, cx: &App) -> Task> { let Some(full_path) = self.full_path(cx) else { log::error!("selection context's file has no path"); return Task::ready(None); }; let text = self.text(cx); - let buffer = self.buffer.clone(); let context = AgentContext::Selection(SelectionContext { full_path: full_path.to_string_lossy().into_owned(), line_range: self.line_range(cx), @@ -482,7 +478,7 @@ impl SelectionContextHandle { handle: self, }); - Task::ready(Some((context, vec![buffer]))) + Task::ready(Some(context)) } } @@ -523,8 +519,8 @@ impl FetchedUrlContext { })) } - pub fn load(self) -> Task>)>> { - Task::ready(Some((AgentContext::FetchedUrl(self), vec![]))) + pub fn load(self) -> Task> { + Task::ready(Some(AgentContext::FetchedUrl(self))) } } @@ -537,7 +533,7 @@ impl Display for FetchedUrlContext { #[derive(Debug, Clone)] pub struct ThreadContextHandle { - pub thread: Entity, + pub thread: Entity, pub context_id: ContextId, } @@ -558,22 +554,20 @@ impl ThreadContextHandle { } pub fn title(&self, cx: &App) -> SharedString { - self.thread.read(cx).summary().or_default() + self.thread.read(cx).title() } - fn load(self, cx: &App) -> Task>)>> { - cx.spawn(async move |cx| { - let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?; - let title = self - .thread - .read_with(cx, |thread, _cx| thread.summary().or_default()) - .ok()?; + fn load(self, cx: &mut App) -> Task> { + let task = self.thread.update(cx, |thread, cx| thread.summary(cx)); + let title = self.title(cx); + cx.background_spawn(async move { + let text = task.await?; let context = AgentContext::Thread(ThreadContext { title, text, handle: self, }); - Some((context, vec![])) + Some(context) }) } } @@ -612,7 +606,7 @@ impl TextThreadContextHandle { self.context.read(cx).summary().or_default() } - fn load(self, cx: &App) -> Task>)>> { + fn load(self, cx: &App) -> Task> { let title = self.title(cx); let text = self.context.read(cx).to_xml(cx); let context = AgentContext::TextThread(TextThreadContext { @@ -620,7 +614,7 @@ impl TextThreadContextHandle { text: text.into(), handle: self, }); - Task::ready(Some((context, vec![]))) + Task::ready(Some(context)) } } @@ -666,7 +660,7 @@ impl RulesContextHandle { self, prompt_store: &Option>, cx: &App, - ) -> Task>)>> { + ) -> Task> { let Some(prompt_store) = prompt_store.as_ref() else { return Task::ready(None); }; @@ -685,7 +679,7 @@ impl RulesContextHandle { title, text, }); - Some((context, vec![])) + Some(context) }) } } @@ -748,32 +742,21 @@ impl ImageContext { } } - pub fn load(self, cx: &App) -> Task>)>> { + pub fn load(self, cx: &App) -> Task> { cx.background_spawn(async move { self.image_task.clone().await; - Some((AgentContext::Image(self), vec![])) + Some(AgentContext::Image(self)) }) } } -#[derive(Debug, Clone, Default)] -pub struct ContextLoadResult { - pub loaded_context: LoadedContext, - pub referenced_buffers: HashSet>, -} - #[derive(Debug, Clone, Default)] pub struct LoadedContext { - pub contexts: Vec, pub text: String, pub images: Vec, } impl LoadedContext { - pub fn is_empty(&self) -> bool { - self.text.is_empty() && self.images.is_empty() - } - pub fn add_to_request_message(&self, request_message: &mut LanguageModelRequestMessage) { if !self.text.is_empty() { request_message @@ -804,7 +787,7 @@ pub fn load_context( project: &Entity, prompt_store: &Option>, cx: &mut App, -) -> Task { +) -> Task { let load_tasks: Vec<_> = contexts .into_iter() .map(|context| match context { @@ -823,16 +806,7 @@ pub fn load_context( cx.background_spawn(async move { let load_results = future::join_all(load_tasks).await; - let mut contexts = Vec::new(); let mut text = String::new(); - let mut referenced_buffers = HashSet::default(); - for context in load_results { - let Some((context, buffers)) = context else { - continue; - }; - contexts.push(context); - referenced_buffers.extend(buffers); - } let mut file_context = Vec::new(); let mut directory_context = Vec::new(); @@ -843,7 +817,7 @@ pub fn load_context( let mut text_thread_context = Vec::new(); let mut rules_context = Vec::new(); let mut images = Vec::new(); - for context in &contexts { + for context in load_results.into_iter().flatten() { match context { AgentContext::File(context) => file_context.push(context), AgentContext::Directory(context) => directory_context.push(context), @@ -868,14 +842,7 @@ pub fn load_context( && text_thread_context.is_empty() && rules_context.is_empty() { - return ContextLoadResult { - loaded_context: LoadedContext { - contexts, - text, - images, - }, - referenced_buffers, - }; + return LoadedContext { text, images }; } text.push_str( @@ -961,14 +928,7 @@ pub fn load_context( text.push_str("\n"); - ContextLoadResult { - loaded_context: LoadedContext { - contexts, - text, - images, - }, - referenced_buffers, - } + LoadedContext { text, images } }) } @@ -1131,11 +1091,13 @@ mod tests { assert!(content_len > outline::AUTO_OUTLINE_SIZE); - let file_context = file_context_for(large_content, cx).await; + let file_context = load_context_for("file.txt", large_content, cx).await; assert!( - file_context.is_outline, - "Large file should use outline format" + file_context + .text + .contains(&format!("# File outline for {}", path!("test/file.txt"))), + "Large files should not get an outline" ); assert!( @@ -1153,29 +1115,38 @@ mod tests { assert!(content_len < outline::AUTO_OUTLINE_SIZE); - let file_context = file_context_for(small_content.to_string(), cx).await; + let file_context = load_context_for("file.txt", small_content.to_string(), cx).await; assert!( - !file_context.is_outline, + !file_context + .text + .contains(&format!("# File outline for {}", path!("test/file.txt"))), "Small files should not get an outline" ); - assert_eq!(file_context.text, small_content); + assert!( + file_context.text.contains(small_content), + "Small files should use full content" + ); } - async fn file_context_for(content: String, cx: &mut TestAppContext) -> FileContext { + async fn load_context_for( + filename: &str, + content: String, + cx: &mut TestAppContext, + ) -> LoadedContext { // Create a test project with the file let project = create_test_project( cx, json!({ - "file.txt": content, + filename: content, }), ) .await; // Open the buffer let buffer_path = project - .read_with(cx, |project, cx| project.find_project_path("file.txt", cx)) + .read_with(cx, |project, cx| project.find_project_path(filename, cx)) .unwrap(); let buffer = project @@ -1190,16 +1161,5 @@ mod tests { cx.update(|cx| load_context(vec![context_handle], &project, &None, cx)) .await - .loaded_context - .contexts - .into_iter() - .find_map(|ctx| { - if let AgentContext::File(file_ctx) = ctx { - Some(file_ctx) - } else { - None - } - }) - .expect("Should have found a file context") } } diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index 58edecdf3da6b16bca82a7d4c0e73dcac3969e03..cfb2ce0a60441c18d62965dddf6a626c4b4a4243 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -9,6 +9,8 @@ use std::ops::Range; use std::path::PathBuf; use std::sync::Arc; +use agent::{HistoryEntry, HistoryEntryId, HistoryStore}; +use agent_client_protocol as acp; use anyhow::{Result, anyhow}; use collections::HashSet; pub use completion_provider::ContextPickerCompletionProvider; @@ -27,9 +29,7 @@ use project::ProjectPath; use prompt_store::PromptStore; use rules_context_picker::{RulesContextEntry, RulesContextPicker}; use symbol_context_picker::SymbolContextPicker; -use thread_context_picker::{ - ThreadContextEntry, ThreadContextPicker, render_thread_context_entry, unordered_thread_entries, -}; +use thread_context_picker::render_thread_context_entry; use ui::{ ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*, }; @@ -37,12 +37,8 @@ use util::paths::PathStyle; use util::rel_path::RelPath; use workspace::{Workspace, notifications::NotifyResultExt}; -use agent::{ - ThreadId, - context::RULES_ICON, - context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, -}; +use crate::context_picker::thread_context_picker::ThreadContextPicker; +use crate::{context::RULES_ICON, context_store::ContextStore}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ContextPickerEntry { @@ -168,17 +164,16 @@ pub(super) struct ContextPicker { mode: ContextPickerState, workspace: WeakEntity, context_store: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, - prompt_store: Option>, + thread_store: Option>, + prompt_store: Option>, _subscriptions: Vec, } impl ContextPicker { pub fn new( workspace: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, + prompt_store: Option>, context_store: WeakEntity, window: &mut Window, cx: &mut Context, @@ -199,13 +194,6 @@ impl ContextPicker { ) .collect::>(); - let prompt_store = thread_store.as_ref().and_then(|thread_store| { - thread_store - .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) - .ok() - .flatten() - }); - ContextPicker { mode: ContextPickerState::Default(ContextMenu::build( window, @@ -215,7 +203,6 @@ impl ContextPicker { workspace, context_store, thread_store, - text_thread_store, prompt_store, _subscriptions: subscriptions, } @@ -355,17 +342,13 @@ impl ContextPicker { })); } ContextPickerMode::Thread => { - if let Some((thread_store, text_thread_store)) = self - .thread_store - .as_ref() - .zip(self.text_thread_store.as_ref()) - { + if let Some(thread_store) = self.thread_store.clone() { self.mode = ContextPickerState::Thread(cx.new(|cx| { ThreadContextPicker::new( - thread_store.clone(), - text_thread_store.clone(), + thread_store, context_picker.clone(), self.context_store.clone(), + self.workspace.clone(), window, cx, ) @@ -480,16 +463,23 @@ impl ContextPicker { fn add_recent_thread( &self, - entry: ThreadContextEntry, - window: &mut Window, + entry: HistoryEntry, + _window: &mut Window, cx: &mut Context, ) -> Task> { let Some(context_store) = self.context_store.upgrade() else { return Task::ready(Err(anyhow!("context store not available"))); }; + let Some(project) = self + .workspace + .upgrade() + .map(|workspace| workspace.read(cx).project().clone()) + else { + return Task::ready(Err(anyhow!("project not available"))); + }; match entry { - ThreadContextEntry::Thread { id, .. } => { + HistoryEntry::AcpThread(thread) => { let Some(thread_store) = self .thread_store .as_ref() @@ -497,28 +487,28 @@ impl ContextPicker { else { return Task::ready(Err(anyhow!("thread store not available"))); }; - - let open_thread_task = - thread_store.update(cx, |this, cx| this.open_thread(&id, window, cx)); + let load_thread_task = + agent::load_agent_thread(thread.id, thread_store, project, cx); cx.spawn(async move |this, cx| { - let thread = open_thread_task.await?; + let thread = load_thread_task.await?; context_store.update(cx, |context_store, cx| { context_store.add_thread(thread, true, cx); })?; this.update(cx, |_this, cx| cx.notify()) }) } - ThreadContextEntry::Context { path, .. } => { - let Some(text_thread_store) = self - .text_thread_store + HistoryEntry::TextThread(thread) => { + let Some(thread_store) = self + .thread_store .as_ref() .and_then(|thread_store| thread_store.upgrade()) else { return Task::ready(Err(anyhow!("text thread store not available"))); }; - let task = text_thread_store - .update(cx, |this, cx| this.open_local_context(path.clone(), cx)); + let task = thread_store.update(cx, |this, cx| { + this.load_text_thread(thread.path.clone(), cx) + }); cx.spawn(async move |this, cx| { let thread = task.await?; context_store.update(cx, |context_store, cx| { @@ -542,7 +532,6 @@ impl ContextPicker { recent_context_picker_entries_with_store( context_store, self.thread_store.clone(), - self.text_thread_store.clone(), workspace, None, cx, @@ -599,12 +588,12 @@ pub(crate) enum RecentEntry { project_path: ProjectPath, path_prefix: Arc, }, - Thread(ThreadContextEntry), + Thread(HistoryEntry), } pub(crate) fn available_context_picker_entries( - prompt_store: &Option>, - thread_store: &Option>, + prompt_store: &Option>, + thread_store: &Option>, workspace: &Entity, cx: &mut App, ) -> Vec { @@ -639,8 +628,7 @@ pub(crate) fn available_context_picker_entries( fn recent_context_picker_entries_with_store( context_store: Entity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, workspace: Entity, exclude_path: Option, cx: &App, @@ -657,22 +645,14 @@ fn recent_context_picker_entries_with_store( let exclude_threads = context_store.read(cx).thread_ids(); - recent_context_picker_entries( - thread_store, - text_thread_store, - workspace, - &exclude_paths, - exclude_threads, - cx, - ) + recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx) } pub(crate) fn recent_context_picker_entries( - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, workspace: Entity, exclude_paths: &HashSet, - _exclude_threads: &HashSet, + exclude_threads: &HashSet, cx: &App, ) -> Vec { let mut recent = Vec::with_capacity(6); @@ -698,30 +678,21 @@ pub(crate) fn recent_context_picker_entries( }), ); - if let Some((thread_store, text_thread_store)) = thread_store - .and_then(|store| store.upgrade()) - .zip(text_thread_store.and_then(|store| store.upgrade())) - { - let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx) - .filter(|(_, thread)| match thread { - ThreadContextEntry::Thread { .. } => false, - ThreadContextEntry::Context { .. } => true, - }) - .collect::>(); - - const RECENT_COUNT: usize = 2; - if threads.len() > RECENT_COUNT { - threads.select_nth_unstable_by_key(RECENT_COUNT - 1, |(updated_at, _)| { - std::cmp::Reverse(*updated_at) - }); - threads.truncate(RECENT_COUNT); - } - threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at)); - + if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) { + const RECENT_THREADS_COUNT: usize = 2; recent.extend( - threads - .into_iter() - .map(|(_, thread)| RecentEntry::Thread(thread)), + thread_store + .read(cx) + .recently_opened_entries(cx) + .iter() + .filter(|e| match e.id() { + HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id), + HistoryEntryId::TextThread(path) => { + !exclude_paths.contains(&path.to_path_buf()) + } + }) + .take(RECENT_THREADS_COUNT) + .map(|thread| RecentEntry::Thread(thread.clone())), ); } @@ -915,17 +886,21 @@ impl MentionLink { ) } - pub fn for_thread(thread: &ThreadContextEntry) -> String { + pub fn for_thread(thread: &HistoryEntry) -> String { match thread { - ThreadContextEntry::Thread { id, title } => { - format!("[@{}]({}:{})", title, Self::THREAD, id) + HistoryEntry::AcpThread(thread) => { + format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id) } - ThreadContextEntry::Context { path, title } => { - let filename = path.file_name().unwrap_or_default().to_string_lossy(); + HistoryEntry::TextThread(thread) => { + let filename = thread + .path + .file_name() + .unwrap_or_default() + .to_string_lossy(); let escaped_filename = urlencoding::encode(&filename); format!( "[@{}]({}:{}{})", - title, + thread.title, Self::THREAD, Self::TEXT_THREAD_URL_PREFIX, escaped_filename diff --git a/crates/agent_ui/src/context_picker/completion_provider.rs b/crates/agent_ui/src/context_picker/completion_provider.rs index e030779eb8c37347410507a74d27299dbcdfbf7d..56444141f12903db4868f9e154cccdb872b48514 100644 --- a/crates/agent_ui/src/context_picker/completion_provider.rs +++ b/crates/agent_ui/src/context_picker/completion_provider.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use agent::context_store::ContextStore; +use agent::{HistoryEntry, HistoryStore}; use anyhow::Result; use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _}; use file_icons::FileIcons; @@ -15,8 +15,8 @@ use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId}; use lsp::CompletionContext; use project::lsp_store::SymbolLocation; use project::{ - Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, ProjectPath, - Symbol, WorktreeId, + Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project, + ProjectPath, Symbol, WorktreeId, }; use prompt_store::PromptStore; use rope::Point; @@ -27,10 +27,9 @@ use util::paths::PathStyle; use util::rel_path::RelPath; use workspace::Workspace; -use agent::{ - Thread, +use crate::{ context::{AgentContextHandle, AgentContextKey, RULES_ICON}, - thread_store::{TextThreadStore, ThreadStore}, + context_store::ContextStore, }; use super::fetch_context_picker::fetch_url_content; @@ -38,7 +37,7 @@ use super::file_context_picker::{FileMatch, search_files}; use super::rules_context_picker::{RulesContextEntry, search_rules}; use super::symbol_context_picker::SymbolMatch; use super::symbol_context_picker::search_symbols; -use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads}; +use super::thread_context_picker::search_threads; use super::{ ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry, available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges, @@ -48,7 +47,8 @@ use crate::message_editor::ContextCreasesAddon; pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), - Thread(ThreadMatch), + Thread(HistoryEntry), + RecentThread(HistoryEntry), Fetch(SharedString), Rules(RulesContextEntry), Entry(EntryMatch), @@ -65,6 +65,7 @@ impl Match { Match::File(file) => file.mat.score, Match::Entry(mode) => mode.mat.as_ref().map(|mat| mat.score).unwrap_or(1.), Match::Thread(_) => 1., + Match::RecentThread(_) => 1., Match::Symbol(_) => 1., Match::Fetch(_) => 1., Match::Rules(_) => 1., @@ -77,9 +78,8 @@ fn search( query: String, cancellation_flag: Arc, recent_entries: Vec, - prompt_store: Option>, - thread_store: Option>, - text_thread_context_store: Option>, + prompt_store: Option>, + thread_store: Option>, workspace: Entity, cx: &mut App, ) -> Task> { @@ -107,13 +107,9 @@ fn search( } Some(ContextPickerMode::Thread) => { - if let Some((thread_store, context_store)) = thread_store - .as_ref() - .and_then(|t| t.upgrade()) - .zip(text_thread_context_store.as_ref().and_then(|t| t.upgrade())) - { + if let Some(thread_store) = thread_store.as_ref().and_then(|t| t.upgrade()) { let search_threads_task = - search_threads(query, cancellation_flag, thread_store, context_store, cx); + search_threads(query, cancellation_flag, &thread_store, cx); cx.background_spawn(async move { search_threads_task .await @@ -135,8 +131,8 @@ fn search( } Some(ContextPickerMode::Rules) => { - if let Some(prompt_store) = prompt_store.as_ref() { - let search_rules_task = search_rules(query, cancellation_flag, prompt_store, cx); + if let Some(prompt_store) = prompt_store.as_ref().and_then(|p| p.upgrade()) { + let search_rules_task = search_rules(query, cancellation_flag, &prompt_store, cx); cx.background_spawn(async move { search_rules_task .await @@ -169,12 +165,7 @@ fn search( }, is_recent: true, }), - super::RecentEntry::Thread(thread_context_entry) => { - Match::Thread(ThreadMatch { - thread: thread_context_entry, - is_recent: true, - }) - } + super::RecentEntry::Thread(entry) => Match::RecentThread(entry), }) .collect::>(); @@ -245,8 +236,8 @@ fn search( pub struct ContextPickerCompletionProvider { workspace: WeakEntity, context_store: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, + prompt_store: Option>, editor: WeakEntity, excluded_buffer: Option>, } @@ -255,8 +246,8 @@ impl ContextPickerCompletionProvider { pub fn new( workspace: WeakEntity, context_store: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, + prompt_store: Option>, editor: WeakEntity, exclude_buffer: Option>, ) -> Self { @@ -264,7 +255,7 @@ impl ContextPickerCompletionProvider { workspace, context_store, thread_store, - text_thread_store, + prompt_store, editor, excluded_buffer: exclude_buffer, } @@ -406,14 +397,14 @@ impl ContextPickerCompletionProvider { } fn completion_for_thread( - thread_entry: ThreadContextEntry, + thread_entry: HistoryEntry, excerpt_id: ExcerptId, source_range: Range, recent: bool, editor: Entity, context_store: Entity, - thread_store: Entity, - text_thread_store: Entity, + thread_store: Entity, + project: Entity, ) -> Completion { let icon_for_completion = if recent { IconName::HistoryRerun @@ -439,18 +430,16 @@ impl ContextPickerCompletionProvider { editor, context_store.clone(), move |window, cx| match &thread_entry { - ThreadContextEntry::Thread { id, .. } => { - let thread_id = id.clone(); + HistoryEntry::AcpThread(thread) => { let context_store = context_store.clone(); - let thread_store = thread_store.clone(); + let load_thread_task = agent::load_agent_thread( + thread.id.clone(), + thread_store.clone(), + project.clone(), + cx, + ); window.spawn::<_, Option<_>>(cx, async move |cx| { - let thread: Entity = thread_store - .update_in(cx, |thread_store, window, cx| { - thread_store.open_thread(&thread_id, window, cx) - }) - .ok()? - .await - .log_err()?; + let thread = load_thread_task.await.log_err()?; let context = context_store .update(cx, |context_store, cx| { context_store.add_thread(thread, false, cx) @@ -459,13 +448,13 @@ impl ContextPickerCompletionProvider { Some(context) }) } - ThreadContextEntry::Context { path, .. } => { - let path = path.clone(); + HistoryEntry::TextThread(thread) => { + let path = thread.path.clone(); let context_store = context_store.clone(); - let text_thread_store = text_thread_store.clone(); + let thread_store = thread_store.clone(); cx.spawn::<_, Option<_>>(async move |cx| { - let thread = text_thread_store - .update(cx, |store, cx| store.open_local_context(path, cx)) + let thread = thread_store + .update(cx, |store, cx| store.load_text_thread(path, cx)) .ok()? .await .log_err()?; @@ -774,7 +763,7 @@ impl CompletionProvider for ContextPickerCompletionProvider { ..snapshot.anchor_after(state.source_range.end); let thread_store = self.thread_store.clone(); - let text_thread_store = self.text_thread_store.clone(); + let prompt_store = self.prompt_store.clone(); let editor = self.editor.clone(); let http_client = workspace.read(cx).client().http_client(); let path_style = workspace.read(cx).path_style(cx); @@ -792,19 +781,11 @@ impl CompletionProvider for ContextPickerCompletionProvider { let recent_entries = recent_context_picker_entries_with_store( context_store.clone(), thread_store.clone(), - text_thread_store.clone(), workspace.clone(), excluded_path.clone(), cx, ); - let prompt_store = thread_store.as_ref().and_then(|thread_store| { - thread_store - .read_with(cx, |thread_store, _cx| thread_store.prompt_store().clone()) - .ok() - .flatten() - }); - let search_task = search( mode, query, @@ -812,14 +793,14 @@ impl CompletionProvider for ContextPickerCompletionProvider { recent_entries, prompt_store, thread_store.clone(), - text_thread_store.clone(), workspace.clone(), cx, ); + let project = workspace.read(cx).project().downgrade(); cx.spawn(async move |_, cx| { let matches = search_task.await; - let Some(editor) = editor.upgrade() else { + let Some((editor, project)) = editor.upgrade().zip(project.upgrade()) else { return Ok(Vec::new()); }; @@ -860,25 +841,32 @@ impl CompletionProvider for ContextPickerCompletionProvider { workspace.clone(), cx, ), - - Match::Thread(ThreadMatch { - thread, is_recent, .. - }) => { + Match::Thread(thread) => { let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?; - let text_thread_store = - text_thread_store.as_ref().and_then(|t| t.upgrade())?; Some(Self::completion_for_thread( thread, excerpt_id, source_range.clone(), - is_recent, + false, editor.clone(), context_store.clone(), thread_store, - text_thread_store, + project.clone(), + )) + } + Match::RecentThread(thread) => { + let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?; + Some(Self::completion_for_thread( + thread, + excerpt_id, + source_range.clone(), + true, + editor.clone(), + context_store.clone(), + thread_store, + project.clone(), )) } - Match::Rules(user_rules) => Some(Self::completion_for_rules( user_rules, excerpt_id, @@ -1281,7 +1269,7 @@ mod tests { editor }); - let context_store = cx.new(|_| ContextStore::new(project.downgrade(), None)); + let context_store = cx.new(|_| ContextStore::new(project.downgrade())); let editor_entity = editor.downgrade(); editor.update_in(&mut cx, |editor, window, cx| { diff --git a/crates/agent_ui/src/context_picker/fetch_context_picker.rs b/crates/agent_ui/src/context_picker/fetch_context_picker.rs index dd558b2a1c88f60e68313b208b076a0974b30f85..31fc45aca3ccbf561793769939169d214aaa2d99 100644 --- a/crates/agent_ui/src/context_picker/fetch_context_picker.rs +++ b/crates/agent_ui/src/context_picker/fetch_context_picker.rs @@ -2,7 +2,6 @@ use std::cell::RefCell; use std::rc::Rc; use std::sync::Arc; -use agent::context_store::ContextStore; use anyhow::{Context as _, Result, bail}; use futures::AsyncReadExt as _; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; @@ -12,7 +11,7 @@ use picker::{Picker, PickerDelegate}; use ui::{Context, ListItem, Window, prelude::*}; use workspace::Workspace; -use crate::context_picker::ContextPicker; +use crate::{context_picker::ContextPicker, context_store::ContextStore}; pub struct FetchContextPicker { picker: Entity>, diff --git a/crates/agent_ui/src/context_picker/file_context_picker.rs b/crates/agent_ui/src/context_picker/file_context_picker.rs index 4f7a4308406f9d9fbdfa42cc86adc1ffe7593396..8d1e5cb46dfba7bc89770356334fb08a7bf7a0c5 100644 --- a/crates/agent_ui/src/context_picker/file_context_picker.rs +++ b/crates/agent_ui/src/context_picker/file_context_picker.rs @@ -12,8 +12,10 @@ use ui::{ListItem, Tooltip, prelude::*}; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; use workspace::Workspace; -use crate::context_picker::ContextPicker; -use agent::context_store::{ContextStore, FileInclusion}; +use crate::{ + context_picker::ContextPicker, + context_store::{ContextStore, FileInclusion}, +}; pub struct FileContextPicker { picker: Entity>, diff --git a/crates/agent_ui/src/context_picker/rules_context_picker.rs b/crates/agent_ui/src/context_picker/rules_context_picker.rs index 677011577aef23296a34203acdb10e5228ca7cd7..68f4917a4fd5689aab1a418dd78d2c8a322cd717 100644 --- a/crates/agent_ui/src/context_picker/rules_context_picker.rs +++ b/crates/agent_ui/src/context_picker/rules_context_picker.rs @@ -7,9 +7,11 @@ use prompt_store::{PromptId, PromptStore, UserPromptId}; use ui::{ListItem, prelude::*}; use util::ResultExt as _; -use crate::context_picker::ContextPicker; -use agent::context::RULES_ICON; -use agent::context_store::{self, ContextStore}; +use crate::{ + context::RULES_ICON, + context_picker::ContextPicker, + context_store::{self, ContextStore}, +}; pub struct RulesContextPicker { picker: Entity>, @@ -17,7 +19,7 @@ pub struct RulesContextPicker { impl RulesContextPicker { pub fn new( - prompt_store: Entity, + prompt_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, window: &mut Window, @@ -49,7 +51,7 @@ pub struct RulesContextEntry { } pub struct RulesContextPickerDelegate { - prompt_store: Entity, + prompt_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, matches: Vec, @@ -58,7 +60,7 @@ pub struct RulesContextPickerDelegate { impl RulesContextPickerDelegate { pub fn new( - prompt_store: Entity, + prompt_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, ) -> Self { @@ -102,12 +104,10 @@ impl PickerDelegate for RulesContextPickerDelegate { window: &mut Window, cx: &mut Context>, ) -> Task<()> { - let search_task = search_rules( - query, - Arc::new(AtomicBool::default()), - &self.prompt_store, - cx, - ); + let Some(prompt_store) = self.prompt_store.upgrade() else { + return Task::ready(()); + }; + let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx); cx.spawn_in(window, async move |this, cx| { let matches = search_task.await; this.update(cx, |this, cx| { diff --git a/crates/agent_ui/src/context_picker/symbol_context_picker.rs b/crates/agent_ui/src/context_picker/symbol_context_picker.rs index 5b89f09de884067a94832c7bf474a2949e78c420..fbce71d94efd84b1acc6e0b5d4ea11cb2b9243d5 100644 --- a/crates/agent_ui/src/context_picker/symbol_context_picker.rs +++ b/crates/agent_ui/src/context_picker/symbol_context_picker.rs @@ -15,9 +15,9 @@ use ui::{ListItem, prelude::*}; use util::ResultExt as _; use workspace::Workspace; -use crate::context_picker::ContextPicker; -use agent::context::AgentContextHandle; -use agent::context_store::ContextStore; +use crate::{ + context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore, +}; pub struct SymbolContextPicker { picker: Entity>, diff --git a/crates/agent_ui/src/context_picker/thread_context_picker.rs b/crates/agent_ui/src/context_picker/thread_context_picker.rs index 9e843779c2216a89fe23dce514553e50043b8187..d6a3a270742fe28c483d2d7d39894eb9e3c021ea 100644 --- a/crates/agent_ui/src/context_picker/thread_context_picker.rs +++ b/crates/agent_ui/src/context_picker/thread_context_picker.rs @@ -1,19 +1,16 @@ -use std::path::Path; use std::sync::Arc; use std::sync::atomic::AtomicBool; -use chrono::{DateTime, Utc}; +use crate::{ + context_picker::ContextPicker, + context_store::{self, ContextStore}, +}; +use agent::{HistoryEntry, HistoryStore}; use fuzzy::StringMatchCandidate; use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity}; use picker::{Picker, PickerDelegate}; use ui::{ListItem, prelude::*}; - -use crate::context_picker::ContextPicker; -use agent::{ - ThreadId, - context_store::{self, ContextStore}, - thread_store::{TextThreadStore, ThreadStore}, -}; +use workspace::Workspace; pub struct ThreadContextPicker { picker: Entity>, @@ -21,18 +18,18 @@ pub struct ThreadContextPicker { impl ThreadContextPicker { pub fn new( - thread_store: WeakEntity, - text_thread_context_store: WeakEntity, + thread_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, + workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { let delegate = ThreadContextPickerDelegate::new( thread_store, - text_thread_context_store, context_picker, context_store, + workspace, ); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); @@ -52,48 +49,27 @@ impl Render for ThreadContextPicker { } } -#[derive(Debug, Clone)] -pub enum ThreadContextEntry { - Thread { - id: ThreadId, - title: SharedString, - }, - Context { - path: Arc, - title: SharedString, - }, -} - -impl ThreadContextEntry { - pub fn title(&self) -> &SharedString { - match self { - Self::Thread { title, .. } => title, - Self::Context { title, .. } => title, - } - } -} - pub struct ThreadContextPickerDelegate { - thread_store: WeakEntity, - text_thread_store: WeakEntity, + thread_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, - matches: Vec, + workspace: WeakEntity, + matches: Vec, selected_index: usize, } impl ThreadContextPickerDelegate { pub fn new( - thread_store: WeakEntity, - text_thread_store: WeakEntity, + thread_store: WeakEntity, context_picker: WeakEntity, context_store: WeakEntity, + workspace: WeakEntity, ) -> Self { ThreadContextPickerDelegate { thread_store, context_picker, context_store, - text_thread_store, + workspace, matches: Vec::new(), selected_index: 0, } @@ -130,25 +106,15 @@ impl PickerDelegate for ThreadContextPickerDelegate { window: &mut Window, cx: &mut Context>, ) -> Task<()> { - let Some((thread_store, text_thread_context_store)) = self - .thread_store - .upgrade() - .zip(self.text_thread_store.upgrade()) - else { + let Some(thread_store) = self.thread_store.upgrade() else { return Task::ready(()); }; - let search_task = search_threads( - query, - Arc::new(AtomicBool::default()), - thread_store, - text_thread_context_store, - cx, - ); + let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx); cx.spawn_in(window, async move |this, cx| { let matches = search_task.await; this.update(cx, |this, cx| { - this.delegate.matches = matches.into_iter().map(|mat| mat.thread).collect(); + this.delegate.matches = matches; this.delegate.selected_index = 0; cx.notify(); }) @@ -156,21 +122,29 @@ impl PickerDelegate for ThreadContextPickerDelegate { }) } - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - let Some(entry) = self.matches.get(self.selected_index) else { + fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context>) { + let Some(project) = self + .workspace + .upgrade() + .map(|w| w.read(cx).project().clone()) + else { + return; + }; + let Some((entry, thread_store)) = self + .matches + .get(self.selected_index) + .zip(self.thread_store.upgrade()) + else { return; }; match entry { - ThreadContextEntry::Thread { id, .. } => { - let Some(thread_store) = self.thread_store.upgrade() else { - return; - }; - let open_thread_task = - thread_store.update(cx, |this, cx| this.open_thread(id, window, cx)); + HistoryEntry::AcpThread(thread) => { + let load_thread_task = + agent::load_agent_thread(thread.id.clone(), thread_store, project, cx); cx.spawn(async move |this, cx| { - let thread = open_thread_task.await?; + let thread = load_thread_task.await?; this.update(cx, |this, cx| { this.delegate .context_store @@ -182,12 +156,10 @@ impl PickerDelegate for ThreadContextPickerDelegate { }) .detach_and_log_err(cx); } - ThreadContextEntry::Context { path, .. } => { - let Some(text_thread_store) = self.text_thread_store.upgrade() else { - return; - }; - let task = text_thread_store - .update(cx, |this, cx| this.open_local_context(path.clone(), cx)); + HistoryEntry::TextThread(thread) => { + let task = thread_store.update(cx, |this, cx| { + this.load_text_thread(thread.path.clone(), cx) + }); cx.spawn(async move |this, cx| { let thread = task.await?; @@ -229,17 +201,17 @@ impl PickerDelegate for ThreadContextPickerDelegate { } pub fn render_thread_context_entry( - entry: &ThreadContextEntry, + entry: &HistoryEntry, context_store: WeakEntity, cx: &mut App, ) -> Div { let is_added = match entry { - ThreadContextEntry::Thread { id, .. } => context_store + HistoryEntry::AcpThread(thread) => context_store .upgrade() - .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(id)), - ThreadContextEntry::Context { path, .. } => context_store + .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)), + HistoryEntry::TextThread(thread) => context_store .upgrade() - .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(path)), + .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)), }; h_flex() @@ -271,91 +243,38 @@ pub fn render_thread_context_entry( }) } -#[derive(Clone)] -pub struct ThreadMatch { - pub thread: ThreadContextEntry, - pub is_recent: bool, -} - -pub fn unordered_thread_entries( - thread_store: Entity, - text_thread_store: Entity, - cx: &App, -) -> impl Iterator, ThreadContextEntry)> { - let threads = thread_store - .read(cx) - .reverse_chronological_threads() - .map(|thread| { - ( - thread.updated_at, - ThreadContextEntry::Thread { - id: thread.id.clone(), - title: thread.summary.clone(), - }, - ) - }); - - let text_threads = text_thread_store - .read(cx) - .unordered_contexts() - .map(|context| { - ( - context.mtime.to_utc(), - ThreadContextEntry::Context { - path: context.path.clone(), - title: context.title.clone(), - }, - ) - }); - - threads.chain(text_threads) -} - pub(crate) fn search_threads( query: String, cancellation_flag: Arc, - thread_store: Entity, - text_thread_store: Entity, + thread_store: &Entity, cx: &mut App, -) -> Task> { - let mut threads = - unordered_thread_entries(thread_store, text_thread_store, cx).collect::>(); - threads.sort_unstable_by_key(|(updated_at, _)| std::cmp::Reverse(*updated_at)); +) -> Task> { + let threads = thread_store.read(cx).entries().collect(); + if query.is_empty() { + return Task::ready(threads); + } let executor = cx.background_executor().clone(); cx.background_spawn(async move { - if query.is_empty() { - threads - .into_iter() - .map(|(_, thread)| ThreadMatch { - thread, - is_recent: false, - }) - .collect() - } else { - let candidates = threads - .iter() - .enumerate() - .map(|(id, (_, thread))| StringMatchCandidate::new(id, thread.title())) - .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &cancellation_flag, - executor, - ) - .await; + let candidates = threads + .iter() + .enumerate() + .map(|(id, thread)| StringMatchCandidate::new(id, thread.title())) + .collect::>(); + let matches = fuzzy::match_strings( + &candidates, + &query, + false, + true, + 100, + &cancellation_flag, + executor, + ) + .await; - matches - .into_iter() - .map(|mat| ThreadMatch { - thread: threads[mat.candidate_id].1.clone(), - is_recent: false, - }) - .collect() - } + matches + .into_iter() + .map(|mat| threads[mat.candidate_id].clone()) + .collect() }) } diff --git a/crates/agent/src/context_store.rs b/crates/agent_ui/src/context_store.rs similarity index 87% rename from crates/agent/src/context_store.rs rename to crates/agent_ui/src/context_store.rs index cf35840cc4215695a966931701257c838c00af18..e2ee1cd0c94fd6132719ffcc0bd352865b5f9cf9 100644 --- a/crates/agent/src/context_store.rs +++ b/crates/agent_ui/src/context_store.rs @@ -1,12 +1,9 @@ -use crate::{ - context::{ - AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle, - FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, - SelectionContextHandle, SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle, - }, - thread::{MessageId, Thread, ThreadId}, - thread_store::ThreadStore, +use crate::context::{ + AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle, + FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle, + SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle, }; +use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; use assistant_context::AssistantContext; use collections::{HashSet, IndexSet}; @@ -29,10 +26,9 @@ use text::{Anchor, OffsetRangeExt}; pub struct ContextStore { project: WeakEntity, - thread_store: Option>, next_context_id: ContextId, context_set: IndexSet, - context_thread_ids: HashSet, + context_thread_ids: HashSet, context_text_thread_paths: HashSet>, } @@ -43,13 +39,9 @@ pub enum ContextStoreEvent { impl EventEmitter for ContextStore {} impl ContextStore { - pub fn new( - project: WeakEntity, - thread_store: Option>, - ) -> Self { + pub fn new(project: WeakEntity) -> Self { Self { project, - thread_store, next_context_id: ContextId::zero(), context_set: IndexSet::default(), context_thread_ids: HashSet::default(), @@ -67,29 +59,6 @@ impl ContextStore { cx.notify(); } - pub fn new_context_for_thread( - &self, - thread: &Thread, - exclude_messages_from_id: Option, - ) -> Vec { - let existing_context = thread - .messages() - .take_while(|message| exclude_messages_from_id.is_none_or(|id| message.id != id)) - .flat_map(|message| { - message - .loaded_context - .contexts - .iter() - .map(|context| AgentContextKey(context.handle())) - }) - .collect::>(); - self.context_set - .iter() - .filter(|context| !existing_context.contains(context)) - .map(|entry| entry.0.clone()) - .collect::>() - } - pub fn add_file_from_path( &mut self, project_path: ProjectPath, @@ -209,7 +178,7 @@ impl ContextStore { pub fn add_thread( &mut self, - thread: Entity, + thread: Entity, remove_if_exists: bool, cx: &mut Context, ) -> Option { @@ -384,15 +353,15 @@ impl ContextStore { ); }; } - SuggestedContext::Thread { thread, name: _ } => { - if let Some(thread) = thread.upgrade() { - let context_id = self.next_context_id.post_inc(); - self.insert_context( - AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }), - cx, - ); - } - } + // SuggestedContext::Thread { thread, name: _ } => { + // if let Some(thread) = thread.upgrade() { + // let context_id = self.next_context_id.post_inc(); + // self.insert_context( + // AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }), + // cx, + // ); + // } + // } SuggestedContext::TextThread { context, name: _ } => { if let Some(context) = context.upgrade() { let context_id = self.next_context_id.post_inc(); @@ -410,17 +379,17 @@ impl ContextStore { fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context) -> bool { match &context { - AgentContextHandle::Thread(thread_context) => { - if let Some(thread_store) = self.thread_store.clone() { - thread_context.thread.update(cx, |thread, cx| { - thread.start_generating_detailed_summary_if_needed(thread_store, cx); - }); - self.context_thread_ids - .insert(thread_context.thread.read(cx).id().clone()); - } else { - return false; - } - } + // AgentContextHandle::Thread(thread_context) => { + // if let Some(thread_store) = self.thread_store.clone() { + // thread_context.thread.update(cx, |thread, cx| { + // thread.start_generating_detailed_summary_if_needed(thread_store, cx); + // }); + // self.context_thread_ids + // .insert(thread_context.thread.read(cx).id().clone()); + // } else { + // return false; + // } + // } AgentContextHandle::TextThread(text_thread_context) => { self.context_text_thread_paths .extend(text_thread_context.context.read(cx).path().cloned()); @@ -514,7 +483,7 @@ impl ContextStore { }) } - pub fn includes_thread(&self, thread_id: &ThreadId) -> bool { + pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool { self.context_thread_ids.contains(thread_id) } @@ -547,9 +516,9 @@ impl ContextStore { } AgentContextHandle::Directory(_) | AgentContextHandle::Symbol(_) + | AgentContextHandle::Thread(_) | AgentContextHandle::Selection(_) | AgentContextHandle::FetchedUrl(_) - | AgentContextHandle::Thread(_) | AgentContextHandle::TextThread(_) | AgentContextHandle::Rules(_) | AgentContextHandle::Image(_) => None, @@ -557,7 +526,7 @@ impl ContextStore { .collect() } - pub fn thread_ids(&self) -> &HashSet { + pub fn thread_ids(&self) -> &HashSet { &self.context_thread_ids } } @@ -569,10 +538,10 @@ pub enum SuggestedContext { icon_path: Option, buffer: WeakEntity, }, - Thread { - name: SharedString, - thread: WeakEntity, - }, + // Thread { + // name: SharedString, + // thread: WeakEntity, + // }, TextThread { name: SharedString, context: WeakEntity, @@ -583,7 +552,7 @@ impl SuggestedContext { pub fn name(&self) -> &SharedString { match self { Self::File { name, .. } => name, - Self::Thread { name, .. } => name, + // Self::Thread { name, .. } => name, Self::TextThread { name, .. } => name, } } @@ -591,7 +560,7 @@ impl SuggestedContext { pub fn icon_path(&self) -> Option { match self { Self::File { icon_path, .. } => icon_path.clone(), - Self::Thread { .. } => None, + // Self::Thread { .. } => None, Self::TextThread { .. } => None, } } @@ -599,7 +568,7 @@ impl SuggestedContext { pub fn kind(&self) -> ContextKind { match self { Self::File { .. } => ContextKind::File, - Self::Thread { .. } => ContextKind::Thread, + // Self::Thread { .. } => ContextKind::Thread, Self::TextThread { .. } => ContextKind::TextThread, } } diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index b75b933de40f19557d9dfa83c874c3427773445b..1f40da3d945df5f066289932b83065dc33d8e169 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -4,12 +4,11 @@ use crate::{ context_picker::ContextPicker, ui::{AddedContext, ContextPill}, }; -use agent::context_store::SuggestedContext; -use agent::{ +use crate::{ context::AgentContextHandle, - context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, + context_store::{ContextStore, SuggestedContext}, }; +use agent::HistoryStore; use collections::HashSet; use editor::Editor; use gpui::{ @@ -18,6 +17,7 @@ use gpui::{ }; use itertools::Itertools; use project::ProjectItem; +use prompt_store::PromptStore; use rope::Point; use std::rc::Rc; use text::ToPoint as _; @@ -33,7 +33,7 @@ pub struct ContextStrip { focus_handle: FocusHandle, suggest_context_kind: SuggestContextKind, workspace: WeakEntity, - thread_store: Option>, + prompt_store: Option>, _subscriptions: Vec, focused_index: Option, children_bounds: Option>>, @@ -44,8 +44,8 @@ impl ContextStrip { pub fn new( context_store: Entity, workspace: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, + prompt_store: Option>, context_picker_menu_handle: PopoverMenuHandle, suggest_context_kind: SuggestContextKind, model_usage_context: ModelUsageContext, @@ -56,7 +56,7 @@ impl ContextStrip { ContextPicker::new( workspace.clone(), thread_store.clone(), - text_thread_store, + prompt_store.clone(), context_store.downgrade(), window, cx, @@ -79,7 +79,7 @@ impl ContextStrip { focus_handle, suggest_context_kind, workspace, - thread_store, + prompt_store, _subscriptions: subscriptions, focused_index: None, children_bounds: None, @@ -96,11 +96,7 @@ impl ContextStrip { fn added_contexts(&self, cx: &App) -> Vec { if let Some(workspace) = self.workspace.upgrade() { let project = workspace.read(cx).project().read(cx); - let prompt_store = self - .thread_store - .as_ref() - .and_then(|thread_store| thread_store.upgrade()) - .and_then(|thread_store| thread_store.read(cx).prompt_store().as_ref()); + let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade()); let current_model = self.model_usage_context.language_model(cx); @@ -110,7 +106,7 @@ impl ContextStrip { .flat_map(|context| { AddedContext::new_pending( context.clone(), - prompt_store, + prompt_store.as_ref(), project, current_model.as_ref(), cx, @@ -339,7 +335,7 @@ impl ContextStrip { let context = text_thread_context.context.clone(); window.defer(cx, move |window, cx| { panel.update(cx, |panel, cx| { - panel.open_prompt_editor(context, window, cx) + panel.open_text_thread(context, window, cx) }); }); } diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 3d25e614ad69d264700476d52ddc0407590b9e9c..4c09e475b10881ab9bc2327b5b18b1c66e2ba4ad 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -7,13 +7,11 @@ use std::sync::Arc; use crate::{ AgentPanel, buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent}, + context_store::ContextStore, inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent}, terminal_inline_assistant::TerminalInlineAssistant, }; -use agent::{ - context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, -}; +use agent::HistoryStore; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; @@ -209,24 +207,21 @@ impl InlineAssistant { window: &mut Window, cx: &mut App, ) { - let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai; + let is_ai_enabled = !DisableAiSettings::get_global(cx).disable_ai; if let Some(editor) = item.act_as::(cx) { editor.update(cx, |editor, cx| { - if is_assistant2_enabled { + if is_ai_enabled { let panel = workspace.read(cx).panel::(cx); let thread_store = panel .as_ref() .map(|agent_panel| agent_panel.read(cx).thread_store().downgrade()); - let text_thread_store = panel - .map(|agent_panel| agent_panel.read(cx).text_thread_store().downgrade()); editor.add_code_action_provider( Rc::new(AssistantCodeActionProvider { editor: cx.entity().downgrade(), workspace: workspace.downgrade(), thread_store, - text_thread_store, }), window, cx, @@ -283,7 +278,6 @@ impl InlineAssistant { let prompt_store = agent_panel.prompt_store().as_ref().cloned(); let thread_store = Some(agent_panel.thread_store().downgrade()); - let text_thread_store = Some(agent_panel.text_thread_store().downgrade()); let context_store = agent_panel.inline_assist_context_store().clone(); let handle_assist = @@ -297,7 +291,6 @@ impl InlineAssistant { workspace.project().downgrade(), prompt_store, thread_store, - text_thread_store, action.prompt.clone(), window, cx, @@ -312,7 +305,6 @@ impl InlineAssistant { workspace.project().downgrade(), prompt_store, thread_store, - text_thread_store, action.prompt.clone(), window, cx, @@ -365,8 +357,7 @@ impl InlineAssistant { context_store: Entity, project: WeakEntity, prompt_store: Option>, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, initial_prompt: Option, window: &mut Window, cx: &mut App, @@ -517,7 +508,7 @@ impl InlineAssistant { context_store.clone(), workspace.clone(), thread_store.clone(), - text_thread_store.clone(), + prompt_store.as_ref().map(|s| s.downgrade()), window, cx, ) @@ -589,8 +580,7 @@ impl InlineAssistant { focus: bool, workspace: Entity, prompt_store: Option>, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, window: &mut Window, cx: &mut App, ) -> InlineAssistId { @@ -608,7 +598,7 @@ impl InlineAssistant { } let project = workspace.read(cx).project().downgrade(); - let context_store = cx.new(|_cx| ContextStore::new(project.clone(), thread_store.clone())); + let context_store = cx.new(|_cx| ContextStore::new(project.clone())); let codegen = cx.new(|cx| { BufferCodegen::new( @@ -617,7 +607,7 @@ impl InlineAssistant { initial_transaction_id, context_store.clone(), project, - prompt_store, + prompt_store.clone(), self.telemetry.clone(), self.prompt_builder.clone(), cx, @@ -636,7 +626,7 @@ impl InlineAssistant { context_store, workspace.downgrade(), thread_store, - text_thread_store, + prompt_store.map(|s| s.downgrade()), window, cx, ) @@ -1773,8 +1763,7 @@ struct InlineAssistDecorations { struct AssistantCodeActionProvider { editor: WeakEntity, workspace: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, } const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2"; @@ -1846,7 +1835,6 @@ impl CodeActionProvider for AssistantCodeActionProvider { let editor = self.editor.clone(); let workspace = self.workspace.clone(); let thread_store = self.thread_store.clone(); - let text_thread_store = self.text_thread_store.clone(); let prompt_store = PromptStore::global(cx); window.spawn(cx, async move |cx| { let workspace = workspace.upgrade().context("workspace was released")?; @@ -1894,7 +1882,6 @@ impl CodeActionProvider for AssistantCodeActionProvider { workspace, prompt_store, thread_store, - text_thread_store, window, cx, ); diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index f6347dcb6b80c1b5c939a5c4cd650b9fadf92c62..70d6009e466e3e2f6ba3cd65076f77f7d12b22e0 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -1,7 +1,5 @@ -use agent::{ - context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, -}; +use crate::context_store::ContextStore; +use agent::HistoryStore; use collections::VecDeque; use editor::actions::Paste; use editor::display_map::EditorMargins; @@ -16,6 +14,7 @@ use gpui::{ }; use language_model::{LanguageModel, LanguageModelRegistry}; use parking_lot::Mutex; +use prompt_store::PromptStore; use settings::Settings; use std::cmp; use std::rc::Rc; @@ -777,8 +776,8 @@ impl PromptEditor { fs: Arc, context_store: Entity, workspace: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, + prompt_store: Option>, window: &mut Window, cx: &mut Context>, ) -> PromptEditor { @@ -823,7 +822,7 @@ impl PromptEditor { workspace.clone(), context_store.downgrade(), thread_store.clone(), - text_thread_store.clone(), + prompt_store.clone(), prompt_editor_entity, codegen_buffer.as_ref().map(Entity::downgrade), )))); @@ -837,7 +836,7 @@ impl PromptEditor { context_store.clone(), workspace.clone(), thread_store.clone(), - text_thread_store.clone(), + prompt_store, context_picker_menu_handle.clone(), SuggestContextKind::Thread, ModelUsageContext::InlineAssistant, @@ -949,8 +948,8 @@ impl PromptEditor { fs: Arc, context_store: Entity, workspace: WeakEntity, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, + prompt_store: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -988,7 +987,7 @@ impl PromptEditor { workspace.clone(), context_store.downgrade(), thread_store.clone(), - text_thread_store.clone(), + prompt_store.clone(), prompt_editor_entity, None, )))); @@ -1002,7 +1001,7 @@ impl PromptEditor { context_store.clone(), workspace.clone(), thread_store.clone(), - text_thread_store.clone(), + prompt_store.clone(), context_picker_menu_handle.clone(), SuggestContextKind::Thread, ModelUsageContext::InlineAssistant, diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index a1311f39233c7eaaf0b416401676fb2e43e51a26..42607833e4b5734424988d1edaa32d10bec06506 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1,31 +1,25 @@ -use agent::{context::AgentContextKey, context_store::ContextStoreEvent}; -use agent_settings::AgentProfileId; +use std::ops::Range; + use collections::HashMap; use editor::display_map::CreaseId; use editor::{Addon, AnchorRangeExt, Editor}; -use gpui::{App, Entity, Subscription}; +use gpui::{Entity, Subscription}; use ui::prelude::*; -use crate::context_picker::crease_for_mention; -use crate::profile_selector::ProfileProvider; -use agent::{MessageCrease, Thread, context_store::ContextStore}; - -impl ProfileProvider for Entity { - fn profiles_supported(&self, cx: &App) -> bool { - self.read(cx) - .configured_model() - .is_some_and(|model| model.model.supports_tools()) - } - - fn profile_id(&self, cx: &App) -> AgentProfileId { - self.read(cx).profile().id().clone() - } +use crate::{ + context::{AgentContextHandle, AgentContextKey}, + context_picker::crease_for_mention, + context_store::{ContextStore, ContextStoreEvent}, +}; - fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { - self.update(cx, |this, cx| { - this.set_profile(profile_id, cx); - }); - } +/// Stored information that can be used to resurrect a context crease when creating an editor for a past message. +#[derive(Clone, Debug)] +pub struct MessageCrease { + pub range: Range, + pub icon_path: SharedString, + pub label: SharedString, + /// None for a deserialized message, Some otherwise. + pub context: Option, } #[derive(Default)] diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 4385d2420511c8a148b2a7a58fa8845bd2c19a07..9e653dcce1dcf1487af9998662b57ea4f998c7de 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -1,12 +1,12 @@ -use crate::inline_prompt_editor::{ - CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId, -}; -use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen}; -use agent::{ +use crate::{ context::load_context, context_store::ContextStore, - thread_store::{TextThreadStore, ThreadStore}, + inline_prompt_editor::{ + CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId, + }, + terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen}, }; +use agent::HistoryStore; use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; @@ -74,8 +74,7 @@ impl TerminalInlineAssistant { workspace: WeakEntity, project: WeakEntity, prompt_store: Option>, - thread_store: Option>, - text_thread_store: Option>, + thread_store: Option>, initial_prompt: Option, window: &mut Window, cx: &mut App, @@ -88,7 +87,7 @@ impl TerminalInlineAssistant { cx, ) }); - let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone())); + let context_store = cx.new(|_cx| ContextStore::new(project)); let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone())); let prompt_editor = cx.new(|cx| { @@ -101,7 +100,7 @@ impl TerminalInlineAssistant { context_store.clone(), workspace.clone(), thread_store.clone(), - text_thread_store.clone(), + prompt_store.as_ref().map(|s| s.downgrade()), window, cx, ) @@ -282,7 +281,6 @@ impl TerminalInlineAssistant { context_load_task .await - .loaded_context .add_to_request_message(&mut request_message); request_message.content.push(prompt.into()); diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index f85a06455439d8e52a7b4272bc7f8069f36548ac..ea1f1136794e1ac3a23e2caeaa3006acccf9bce0 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -11,13 +11,13 @@ use project::Project; use prompt_store::PromptStore; use rope::Point; use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container}; +use util::paths::PathStyle; -use agent::context::{ +use crate::context::{ AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext, FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle, SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle, }; -use util::paths::PathStyle; #[derive(IntoElement)] pub enum ContextPill { @@ -466,7 +466,7 @@ impl AddedContext { parent: None, tooltip: None, icon_path: None, - status: if handle.thread.read(cx).is_generating_detailed_summary() { + status: if handle.thread.read(cx).is_generating_summary() { ContextStatus::Loading { message: "Summarizing…".into(), } @@ -476,7 +476,11 @@ impl AddedContext { render_hover: { let thread = handle.thread.clone(); Some(Rc::new(move |_, cx| { - let text = thread.read(cx).latest_detailed_summary_or_text(); + let text = thread + .update(cx, |thread, cx| thread.summary(cx)) + .now_or_never() + .flatten() + .unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown())); ContextPillHover::new_text(text, cx).into() })) }, diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml deleted file mode 100644 index c95695052a4778209010b2f9e7a4a57be4cb6cf7..0000000000000000000000000000000000000000 --- a/crates/assistant_tool/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "assistant_tool" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/assistant_tool.rs" - -[dependencies] -action_log.workspace = true -anyhow.workspace = true -collections.workspace = true -derive_more.workspace = true -gpui.workspace = true -icons.workspace = true -language.workspace = true -language_model.workspace = true -log.workspace = true -parking_lot.workspace = true -project.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -text.workspace = true -util.workspace = true -workspace.workspace = true -workspace-hack.workspace = true - -[dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, features = ["test-support"] } -log.workspace = true -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -settings = { workspace = true, features = ["test-support"] } -text = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/assistant_tool/LICENSE-GPL b/crates/assistant_tool/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/assistant_tool/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_tool/src/assistant_tool.rs b/crates/assistant_tool/src/assistant_tool.rs deleted file mode 100644 index 9c5825d0f0ecc9c31277bfff5123d3d80501511b..0000000000000000000000000000000000000000 --- a/crates/assistant_tool/src/assistant_tool.rs +++ /dev/null @@ -1,269 +0,0 @@ -pub mod outline; -mod tool_registry; -mod tool_schema; -mod tool_working_set; - -use std::fmt; -use std::fmt::Debug; -use std::fmt::Formatter; -use std::ops::Deref; -use std::sync::Arc; - -use action_log::ActionLog; -use anyhow::Result; -use gpui::AnyElement; -use gpui::AnyWindowHandle; -use gpui::Context; -use gpui::IntoElement; -use gpui::Window; -use gpui::{App, Entity, SharedString, Task, WeakEntity}; -use icons::IconName; -use language_model::LanguageModel; -use language_model::LanguageModelImage; -use language_model::LanguageModelRequest; -use language_model::LanguageModelToolSchemaFormat; -use project::Project; -use workspace::Workspace; - -pub use crate::tool_registry::*; -pub use crate::tool_schema::*; -pub use crate::tool_working_set::*; - -pub fn init(cx: &mut App) { - ToolRegistry::default_global(cx); -} - -#[derive(Debug, Clone)] -pub enum ToolUseStatus { - InputStillStreaming, - NeedsConfirmation, - Pending, - Running, - Finished(SharedString), - Error(SharedString), -} - -impl ToolUseStatus { - pub fn text(&self) -> SharedString { - match self { - ToolUseStatus::NeedsConfirmation => "".into(), - ToolUseStatus::InputStillStreaming => "".into(), - ToolUseStatus::Pending => "".into(), - ToolUseStatus::Running => "".into(), - ToolUseStatus::Finished(out) => out.clone(), - ToolUseStatus::Error(out) => out.clone(), - } - } - - pub fn error(&self) -> Option { - match self { - ToolUseStatus::Error(out) => Some(out.clone()), - _ => None, - } - } -} - -#[derive(Debug)] -pub struct ToolResultOutput { - pub content: ToolResultContent, - pub output: Option, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ToolResultContent { - Text(String), - Image(LanguageModelImage), -} - -impl ToolResultContent { - pub fn len(&self) -> usize { - match self { - ToolResultContent::Text(str) => str.len(), - ToolResultContent::Image(image) => image.len(), - } - } - - pub fn is_empty(&self) -> bool { - match self { - ToolResultContent::Text(str) => str.is_empty(), - ToolResultContent::Image(image) => image.is_empty(), - } - } - - pub fn as_str(&self) -> Option<&str> { - match self { - ToolResultContent::Text(str) => Some(str), - ToolResultContent::Image(_) => None, - } - } -} - -impl From for ToolResultOutput { - fn from(value: String) -> Self { - ToolResultOutput { - content: ToolResultContent::Text(value), - output: None, - } - } -} - -impl Deref for ToolResultOutput { - type Target = ToolResultContent; - - fn deref(&self) -> &Self::Target { - &self.content - } -} - -/// The result of running a tool, containing both the asynchronous output -/// and an optional card view that can be rendered immediately. -pub struct ToolResult { - /// The asynchronous task that will eventually resolve to the tool's output - pub output: Task>, - /// An optional view to present the output of the tool. - pub card: Option, -} - -pub trait ToolCard: 'static + Sized { - fn render( - &mut self, - status: &ToolUseStatus, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement; -} - -#[derive(Clone)] -pub struct AnyToolCard { - entity: gpui::AnyEntity, - render: fn( - entity: gpui::AnyEntity, - status: &ToolUseStatus, - window: &mut Window, - workspace: WeakEntity, - cx: &mut App, - ) -> AnyElement, -} - -impl From> for AnyToolCard { - fn from(entity: Entity) -> Self { - fn downcast_render( - entity: gpui::AnyEntity, - status: &ToolUseStatus, - window: &mut Window, - workspace: WeakEntity, - cx: &mut App, - ) -> AnyElement { - let entity = entity.downcast::().unwrap(); - entity.update(cx, |entity, cx| { - entity - .render(status, window, workspace, cx) - .into_any_element() - }) - } - - Self { - entity: entity.into(), - render: downcast_render::, - } - } -} - -impl AnyToolCard { - pub fn render( - &self, - status: &ToolUseStatus, - window: &mut Window, - workspace: WeakEntity, - cx: &mut App, - ) -> AnyElement { - (self.render)(self.entity.clone(), status, window, workspace, cx) - } -} - -impl From>> for ToolResult { - /// Convert from a task to a ToolResult with no card - fn from(output: Task>) -> Self { - Self { output, card: None } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] -pub enum ToolSource { - /// A native tool built-in to Zed. - Native, - /// A tool provided by a context server. - ContextServer { id: SharedString }, -} - -/// A tool that can be used by a language model. -pub trait Tool: 'static + Send + Sync { - /// Returns the name of the tool. - fn name(&self) -> String; - - /// Returns the description of the tool. - fn description(&self) -> String; - - /// Returns the icon for the tool. - fn icon(&self) -> IconName; - - /// Returns the source of the tool. - fn source(&self) -> ToolSource { - ToolSource::Native - } - - /// Returns true if the tool needs the users's confirmation - /// before having permission to run. - fn needs_confirmation( - &self, - input: &serde_json::Value, - project: &Entity, - cx: &App, - ) -> bool; - - /// Returns true if the tool may perform edits. - fn may_perform_edits(&self) -> bool; - - /// Returns the JSON schema that describes the tool's input. - fn input_schema(&self, _: LanguageModelToolSchemaFormat) -> Result { - Ok(serde_json::Value::Object(serde_json::Map::default())) - } - - /// Returns markdown to be displayed in the UI for this tool. - fn ui_text(&self, input: &serde_json::Value) -> String; - - /// Returns markdown to be displayed in the UI for this tool, while the input JSON is still streaming - /// (so information may be missing). - fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { - self.ui_text(input) - } - - /// Runs the tool with the provided input. - fn run( - self: Arc, - input: serde_json::Value, - request: Arc, - project: Entity, - action_log: Entity, - model: Arc, - window: Option, - cx: &mut App, - ) -> ToolResult; - - fn deserialize_card( - self: Arc, - _output: serde_json::Value, - _project: Entity, - _window: &mut Window, - _cx: &mut App, - ) -> Option { - None - } -} - -impl Debug for dyn Tool { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("Tool").field("name", &self.name()).finish() - } -} diff --git a/crates/assistant_tool/src/tool_registry.rs b/crates/assistant_tool/src/tool_registry.rs deleted file mode 100644 index 26b4821a6d1af05a5e42d639f465486b9311d427..0000000000000000000000000000000000000000 --- a/crates/assistant_tool/src/tool_registry.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use gpui::Global; -use gpui::{App, ReadGlobal}; -use parking_lot::RwLock; - -use crate::Tool; - -#[derive(Default, Deref, DerefMut)] -struct GlobalToolRegistry(Arc); - -impl Global for GlobalToolRegistry {} - -#[derive(Default)] -struct ToolRegistryState { - tools: HashMap, Arc>, -} - -#[derive(Default)] -pub struct ToolRegistry { - state: RwLock, -} - -impl ToolRegistry { - /// Returns the global [`ToolRegistry`]. - pub fn global(cx: &App) -> Arc { - GlobalToolRegistry::global(cx).0.clone() - } - - /// Returns the global [`ToolRegistry`]. - /// - /// Inserts a default [`ToolRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Arc { - cx.default_global::().0.clone() - } - - pub fn new() -> Arc { - Arc::new(Self { - state: RwLock::new(ToolRegistryState { - tools: HashMap::default(), - }), - }) - } - - /// Registers the provided [`Tool`]. - pub fn register_tool(&self, tool: impl Tool) { - let mut state = self.state.write(); - let tool_name: Arc = tool.name().into(); - state.tools.insert(tool_name, Arc::new(tool)); - } - - /// Unregisters the provided [`Tool`]. - pub fn unregister_tool(&self, tool: impl Tool) { - self.unregister_tool_by_name(tool.name().as_str()) - } - - /// Unregisters the tool with the given name. - pub fn unregister_tool_by_name(&self, tool_name: &str) { - let mut state = self.state.write(); - state.tools.remove(tool_name); - } - - /// Returns the list of tools in the registry. - pub fn tools(&self) -> Vec> { - self.state.read().tools.values().cloned().collect() - } - - /// Returns the [`Tool`] with the given name. - pub fn tool(&self, name: &str) -> Option> { - self.state.read().tools.get(name).cloned() - } -} diff --git a/crates/assistant_tool/src/tool_working_set.rs b/crates/assistant_tool/src/tool_working_set.rs deleted file mode 100644 index 61f57affc76aad9e4d2185665b539f9092e3491c..0000000000000000000000000000000000000000 --- a/crates/assistant_tool/src/tool_working_set.rs +++ /dev/null @@ -1,415 +0,0 @@ -use std::{borrow::Borrow, sync::Arc}; - -use crate::{Tool, ToolRegistry, ToolSource}; -use collections::{HashMap, HashSet, IndexMap}; -use gpui::{App, SharedString}; -use util::debug_panic; - -#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)] -pub struct ToolId(usize); - -/// A unique identifier for a tool within a working set. -#[derive(Clone, PartialEq, Eq, Hash, Default)] -pub struct UniqueToolName(SharedString); - -impl Borrow for UniqueToolName { - fn borrow(&self) -> &str { - &self.0 - } -} - -impl From for UniqueToolName { - fn from(value: String) -> Self { - UniqueToolName(SharedString::new(value)) - } -} - -impl Into for UniqueToolName { - fn into(self) -> String { - self.0.into() - } -} - -impl std::fmt::Debug for UniqueToolName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl std::fmt::Display for UniqueToolName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.as_ref()) - } -} - -/// A working set of tools for use in one instance of the Assistant Panel. -#[derive(Default)] -pub struct ToolWorkingSet { - context_server_tools_by_id: HashMap>, - context_server_tools_by_name: HashMap>, - next_tool_id: ToolId, -} - -impl ToolWorkingSet { - pub fn tool(&self, name: &str, cx: &App) -> Option> { - self.context_server_tools_by_name - .get(name) - .cloned() - .or_else(|| ToolRegistry::global(cx).tool(name)) - } - - pub fn tools(&self, cx: &App) -> Vec<(UniqueToolName, Arc)> { - let mut tools = ToolRegistry::global(cx) - .tools() - .into_iter() - .map(|tool| (UniqueToolName(tool.name().into()), tool)) - .collect::>(); - tools.extend(self.context_server_tools_by_name.clone()); - tools - } - - pub fn tools_by_source(&self, cx: &App) -> IndexMap>> { - let mut tools_by_source = IndexMap::default(); - - for (_, tool) in self.tools(cx) { - tools_by_source - .entry(tool.source()) - .or_insert_with(Vec::new) - .push(tool); - } - - for tools in tools_by_source.values_mut() { - tools.sort_by_key(|tool| tool.name()); - } - - tools_by_source.sort_unstable_keys(); - - tools_by_source - } - - pub fn insert(&mut self, tool: Arc, cx: &App) -> ToolId { - let tool_id = self.register_tool(tool); - self.tools_changed(cx); - tool_id - } - - pub fn extend(&mut self, tools: impl Iterator>, cx: &App) -> Vec { - let ids = tools.map(|tool| self.register_tool(tool)).collect(); - self.tools_changed(cx); - ids - } - - pub fn remove(&mut self, tool_ids_to_remove: &[ToolId], cx: &App) { - self.context_server_tools_by_id - .retain(|id, _| !tool_ids_to_remove.contains(id)); - self.tools_changed(cx); - } - - fn register_tool(&mut self, tool: Arc) -> ToolId { - let tool_id = self.next_tool_id; - self.next_tool_id.0 += 1; - self.context_server_tools_by_id - .insert(tool_id, tool.clone()); - tool_id - } - - fn tools_changed(&mut self, cx: &App) { - self.context_server_tools_by_name = resolve_context_server_tool_name_conflicts( - &self - .context_server_tools_by_id - .values() - .cloned() - .collect::>(), - &ToolRegistry::global(cx).tools(), - ); - } -} - -fn resolve_context_server_tool_name_conflicts( - context_server_tools: &[Arc], - native_tools: &[Arc], -) -> HashMap> { - fn resolve_tool_name(tool: &Arc) -> String { - let mut tool_name = tool.name(); - tool_name.truncate(MAX_TOOL_NAME_LENGTH); - tool_name - } - - const MAX_TOOL_NAME_LENGTH: usize = 64; - - let mut duplicated_tool_names = HashSet::default(); - let mut seen_tool_names = HashSet::default(); - seen_tool_names.extend(native_tools.iter().map(|tool| tool.name())); - for tool in context_server_tools { - let tool_name = resolve_tool_name(tool); - if seen_tool_names.contains(&tool_name) { - debug_assert!( - tool.source() != ToolSource::Native, - "Expected MCP tool but got a native tool: {}", - tool_name - ); - duplicated_tool_names.insert(tool_name); - } else { - seen_tool_names.insert(tool_name); - } - } - - if duplicated_tool_names.is_empty() { - return context_server_tools - .iter() - .map(|tool| (resolve_tool_name(tool).into(), tool.clone())) - .collect(); - } - - context_server_tools - .iter() - .filter_map(|tool| { - let mut tool_name = resolve_tool_name(tool); - if !duplicated_tool_names.contains(&tool_name) { - return Some((tool_name.into(), tool.clone())); - } - match tool.source() { - ToolSource::Native => { - debug_panic!("Expected MCP tool but got a native tool: {}", tool_name); - // Built-in tools always keep their original name - Some((tool_name.into(), tool.clone())) - } - ToolSource::ContextServer { id } => { - // Context server tools are prefixed with the context server ID, and truncated if necessary - tool_name.insert(0, '_'); - if tool_name.len() + id.len() > MAX_TOOL_NAME_LENGTH { - let len = MAX_TOOL_NAME_LENGTH - tool_name.len(); - let mut id = id.to_string(); - id.truncate(len); - tool_name.insert_str(0, &id); - } else { - tool_name.insert_str(0, &id); - } - - tool_name.truncate(MAX_TOOL_NAME_LENGTH); - - if seen_tool_names.contains(&tool_name) { - log::error!("Cannot resolve tool name conflict for tool {}", tool.name()); - None - } else { - Some((tool_name.into(), tool.clone())) - } - } - } - }) - .collect() -} -#[cfg(test)] -mod tests { - use gpui::{AnyWindowHandle, Entity, Task, TestAppContext}; - use language_model::{LanguageModel, LanguageModelRequest}; - use project::Project; - - use crate::{ActionLog, ToolResult}; - - use super::*; - - #[gpui::test] - fn test_unique_tool_names(cx: &mut TestAppContext) { - fn assert_tool( - tool_working_set: &ToolWorkingSet, - unique_name: &str, - expected_name: &str, - expected_source: ToolSource, - cx: &App, - ) { - let tool = tool_working_set.tool(unique_name, cx).unwrap(); - assert_eq!(tool.name(), expected_name); - assert_eq!(tool.source(), expected_source); - } - - let tool_registry = cx.update(ToolRegistry::default_global); - tool_registry.register_tool(TestTool::new("tool1", ToolSource::Native)); - tool_registry.register_tool(TestTool::new("tool2", ToolSource::Native)); - - let mut tool_working_set = ToolWorkingSet::default(); - cx.update(|cx| { - tool_working_set.extend( - vec![ - Arc::new(TestTool::new( - "tool2", - ToolSource::ContextServer { id: "mcp-1".into() }, - )) as Arc, - Arc::new(TestTool::new( - "tool2", - ToolSource::ContextServer { id: "mcp-2".into() }, - )) as Arc, - ] - .into_iter(), - cx, - ); - }); - - cx.update(|cx| { - assert_tool(&tool_working_set, "tool1", "tool1", ToolSource::Native, cx); - assert_tool(&tool_working_set, "tool2", "tool2", ToolSource::Native, cx); - assert_tool( - &tool_working_set, - "mcp-1_tool2", - "tool2", - ToolSource::ContextServer { id: "mcp-1".into() }, - cx, - ); - assert_tool( - &tool_working_set, - "mcp-2_tool2", - "tool2", - ToolSource::ContextServer { id: "mcp-2".into() }, - cx, - ); - }) - } - - #[gpui::test] - fn test_resolve_context_server_tool_name_conflicts() { - assert_resolve_context_server_tool_name_conflicts( - vec![ - TestTool::new("tool1", ToolSource::Native), - TestTool::new("tool2", ToolSource::Native), - ], - vec![TestTool::new( - "tool3", - ToolSource::ContextServer { id: "mcp-1".into() }, - )], - vec!["tool3"], - ); - - assert_resolve_context_server_tool_name_conflicts( - vec![ - TestTool::new("tool1", ToolSource::Native), - TestTool::new("tool2", ToolSource::Native), - ], - vec![ - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }), - ], - vec!["mcp-1_tool3", "mcp-2_tool3"], - ); - - assert_resolve_context_server_tool_name_conflicts( - vec![ - TestTool::new("tool1", ToolSource::Native), - TestTool::new("tool2", ToolSource::Native), - TestTool::new("tool3", ToolSource::Native), - ], - vec![ - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-1".into() }), - TestTool::new("tool3", ToolSource::ContextServer { id: "mcp-2".into() }), - ], - vec!["mcp-1_tool3", "mcp-2_tool3"], - ); - - // Test deduplication of tools with very long names, in this case the mcp server name should be truncated - assert_resolve_context_server_tool_name_conflicts( - vec![TestTool::new( - "tool-with-very-very-very-long-name", - ToolSource::Native, - )], - vec![TestTool::new( - "tool-with-very-very-very-long-name", - ToolSource::ContextServer { - id: "mcp-with-very-very-very-long-name".into(), - }, - )], - vec!["mcp-with-very-very-very-long-_tool-with-very-very-very-long-name"], - ); - - fn assert_resolve_context_server_tool_name_conflicts( - builtin_tools: Vec, - context_server_tools: Vec, - expected: Vec<&'static str>, - ) { - let context_server_tools: Vec> = context_server_tools - .into_iter() - .map(|t| Arc::new(t) as Arc) - .collect(); - let builtin_tools: Vec> = builtin_tools - .into_iter() - .map(|t| Arc::new(t) as Arc) - .collect(); - let tools = - resolve_context_server_tool_name_conflicts(&context_server_tools, &builtin_tools); - assert_eq!(tools.len(), expected.len()); - for (i, (name, _)) in tools.into_iter().enumerate() { - assert_eq!( - name.0.as_ref(), - expected[i], - "Expected '{}' got '{}' at index {}", - expected[i], - name, - i - ); - } - } - } - - struct TestTool { - name: String, - source: ToolSource, - } - - impl TestTool { - fn new(name: impl Into, source: ToolSource) -> Self { - Self { - name: name.into(), - source, - } - } - } - - impl Tool for TestTool { - fn name(&self) -> String { - self.name.clone() - } - - fn icon(&self) -> icons::IconName { - icons::IconName::Ai - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn needs_confirmation( - &self, - _input: &serde_json::Value, - _project: &Entity, - _cx: &App, - ) -> bool { - true - } - - fn source(&self) -> ToolSource { - self.source.clone() - } - - fn description(&self) -> String { - "Test tool".to_string() - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - "Test tool".to_string() - } - - fn run( - self: Arc, - _input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - _cx: &mut App, - ) -> ToolResult { - ToolResult { - output: Task::ready(Err(anyhow::anyhow!("No content"))), - card: None, - } - } - } -} diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml deleted file mode 100644 index 9b9b8196d1c342c536d605306a1a062e73768c56..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/Cargo.toml +++ /dev/null @@ -1,92 +0,0 @@ -[package] -name = "assistant_tools" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/assistant_tools.rs" - -[features] -eval = [] - -[dependencies] -action_log.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -assistant_tool.workspace = true -buffer_diff.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -component.workspace = true -derive_more.workspace = true -diffy = "0.4.2" -editor.workspace = true -feature_flags.workspace = true -futures.workspace = true -gpui.workspace = true -handlebars = { workspace = true, features = ["rust-embed"] } -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -log.workspace = true -lsp.workspace = true -markdown.workspace = true -open.workspace = true -paths.workspace = true -portable-pty.workspace = true -project.workspace = true -prompt_store.workspace = true -regex.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -streaming_diff.workspace = true -strsim.workspace = true -task.workspace = true -terminal.workspace = true -terminal_view.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -watch.workspace = true -web_search.workspace = true -workspace-hack.workspace = true -workspace.workspace = true - -[dev-dependencies] -lsp = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -gpui_tokio.workspace = true -fs = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, features = ["test-support"] } -language_models.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -pretty_assertions.workspace = true -reqwest_client.workspace = true -settings = { workspace = true, features = ["test-support"] } -smol.workspace = true -task = { workspace = true, features = ["test-support"]} -tempfile.workspace = true -theme.workspace = true -tree-sitter-rust.workspace = true -workspace = { workspace = true, features = ["test-support"] } -unindent.workspace = true -zlog.workspace = true diff --git a/crates/assistant_tools/LICENSE-GPL b/crates/assistant_tools/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs deleted file mode 100644 index 17e2ba12f706387859ca3393aa44f5c05570e50a..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/assistant_tools.rs +++ /dev/null @@ -1,167 +0,0 @@ -mod copy_path_tool; -mod create_directory_tool; -mod delete_path_tool; -mod diagnostics_tool; -pub mod edit_agent; -mod edit_file_tool; -mod fetch_tool; -mod find_path_tool; -mod grep_tool; -mod list_directory_tool; -mod move_path_tool; -mod now_tool; -mod open_tool; -mod project_notifications_tool; -mod read_file_tool; -mod schema; -pub mod templates; -mod terminal_tool; -mod thinking_tool; -mod ui; -mod web_search_tool; - -use assistant_tool::ToolRegistry; -use copy_path_tool::CopyPathTool; -use gpui::{App, Entity}; -use http_client::HttpClientWithUrl; -use language_model::LanguageModelRegistry; -use move_path_tool::MovePathTool; -use std::sync::Arc; -use web_search_tool::WebSearchTool; - -pub(crate) use templates::*; - -use crate::create_directory_tool::CreateDirectoryTool; -use crate::delete_path_tool::DeletePathTool; -use crate::diagnostics_tool::DiagnosticsTool; -use crate::edit_file_tool::EditFileTool; -use crate::fetch_tool::FetchTool; -use crate::list_directory_tool::ListDirectoryTool; -use crate::now_tool::NowTool; -use crate::thinking_tool::ThinkingTool; - -pub use edit_file_tool::{EditFileMode, EditFileToolInput}; -pub use find_path_tool::*; -pub use grep_tool::{GrepTool, GrepToolInput}; -pub use open_tool::OpenTool; -pub use project_notifications_tool::ProjectNotificationsTool; -pub use read_file_tool::{ReadFileTool, ReadFileToolInput}; -pub use terminal_tool::TerminalTool; - -pub fn init(http_client: Arc, cx: &mut App) { - assistant_tool::init(cx); - - let registry = ToolRegistry::global(cx); - registry.register_tool(TerminalTool); - registry.register_tool(CreateDirectoryTool); - registry.register_tool(CopyPathTool); - registry.register_tool(DeletePathTool); - registry.register_tool(MovePathTool); - registry.register_tool(DiagnosticsTool); - registry.register_tool(ListDirectoryTool); - registry.register_tool(NowTool); - registry.register_tool(OpenTool); - registry.register_tool(ProjectNotificationsTool); - registry.register_tool(FindPathTool); - registry.register_tool(ReadFileTool); - registry.register_tool(GrepTool); - registry.register_tool(ThinkingTool); - registry.register_tool(FetchTool::new(http_client)); - registry.register_tool(EditFileTool); - - register_web_search_tool(&LanguageModelRegistry::global(cx), cx); - cx.subscribe( - &LanguageModelRegistry::global(cx), - move |registry, event, cx| { - if let language_model::Event::DefaultModelChanged = event { - register_web_search_tool(®istry, cx); - } - }, - ) - .detach(); -} - -fn register_web_search_tool(registry: &Entity, cx: &mut App) { - let using_zed_provider = registry - .read(cx) - .default_model() - .is_some_and(|default| default.is_provided_by_zed()); - if using_zed_provider { - ToolRegistry::global(cx).register_tool(WebSearchTool); - } else { - ToolRegistry::global(cx).unregister_tool(WebSearchTool); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use agent_settings::AgentSettings; - use client::Client; - use clock::FakeSystemClock; - use http_client::FakeHttpClient; - use schemars::JsonSchema; - use serde::Serialize; - use settings::Settings; - - #[test] - fn test_json_schema() { - #[derive(Serialize, JsonSchema)] - struct GetWeatherTool { - location: String, - } - - let schema = schema::json_schema_for::( - language_model::LanguageModelToolSchemaFormat::JsonSchema, - ) - .unwrap(); - - assert_eq!( - schema, - serde_json::json!({ - "type": "object", - "properties": { - "location": { - "type": "string" - } - }, - "required": ["location"], - "additionalProperties": false - }) - ); - } - - #[gpui::test] - fn test_builtin_tool_schema_compatibility(cx: &mut App) { - settings::init(cx); - AgentSettings::register(cx); - - let client = Client::new( - Arc::new(FakeSystemClock::new()), - FakeHttpClient::with_200_response(), - cx, - ); - language_model::init(client.clone(), cx); - crate::init(client.http_client(), cx); - - for tool in ToolRegistry::global(cx).tools() { - let actual_schema = tool - .input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset) - .unwrap(); - let mut expected_schema = actual_schema.clone(); - assistant_tool::adapt_schema_to_format( - &mut expected_schema, - language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset, - ) - .unwrap(); - - let error_message = format!( - "Tool schema for `{}` is not compatible with `language_model::LanguageModelToolSchemaFormat::JsonSchemaSubset` (Gemini Models).\n\ - Are you using `schema::json_schema_for(format)` to generate the schema?", - tool.name(), - ); - - assert_eq!(actual_schema, expected_schema, "{}", error_message) - } - } -} diff --git a/crates/assistant_tools/src/copy_path_tool.rs b/crates/assistant_tools/src/copy_path_tool.rs deleted file mode 100644 index 572eddcb1079557b464ba29d125aa44929409cc5..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/copy_path_tool.rs +++ /dev/null @@ -1,123 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::AnyWindowHandle; -use gpui::{App, AppContext, Entity, Task}; -use language_model::LanguageModel; -use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use ui::IconName; -use util::markdown::MarkdownInlineCode; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct CopyPathToolInput { - /// The source path of the file or directory to copy. - /// If a directory is specified, its contents will be copied recursively (like `cp -r`). - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can copy the first file by providing a source_path of "directory1/a/something.txt" - /// - pub source_path: String, - - /// The destination path where the file or directory should be copied to. - /// - /// - /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", - /// provide a destination_path of "directory2/b/copy.txt" - /// - pub destination_path: String, -} - -pub struct CopyPathTool; - -impl Tool for CopyPathTool { - fn name(&self) -> String { - "copy_path".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - true - } - - fn description(&self) -> String { - include_str!("./copy_path_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolCopy - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let src = MarkdownInlineCode(&input.source_path); - let dest = MarkdownInlineCode(&input.destination_path); - format!("Copy {src} to {dest}") - } - Err(_) => "Copy path".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - let copy_task = project.update(cx, |project, cx| { - match project - .find_project_path(&input.source_path, cx) - .and_then(|project_path| project.entry_for_path(&project_path, cx)) - { - Some(entity) => match project.find_project_path(&input.destination_path, cx) { - Some(project_path) => project.copy_entry(entity.id, project_path, cx), - None => Task::ready(Err(anyhow!( - "Destination path {} was outside the project.", - input.destination_path - ))), - }, - None => Task::ready(Err(anyhow!( - "Source path {} was not found in the project.", - input.source_path - ))), - } - }); - - cx.background_spawn(async move { - let _ = copy_task.await.with_context(|| { - format!( - "Copying {} to {}", - input.source_path, input.destination_path - ) - })?; - Ok(format!("Copied {} to {}", input.source_path, input.destination_path).into()) - }) - .into() - } -} diff --git a/crates/assistant_tools/src/copy_path_tool/description.md b/crates/assistant_tools/src/copy_path_tool/description.md deleted file mode 100644 index a5105e6f18c705e93aa9c30b9588f84dd8db542a..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/copy_path_tool/description.md +++ /dev/null @@ -1,6 +0,0 @@ -Copies a file or directory in the project, and returns confirmation that the copy succeeded. -Directory contents will be copied recursively (like `cp -r`). - -This tool should be used when it's desirable to create a copy of a file or directory without modifying the original. -It's much more efficient than doing this by separately reading and then writing the file or directory's contents, -so this tool should be preferred over that approach whenever copying is the goal. diff --git a/crates/assistant_tools/src/create_directory_tool.rs b/crates/assistant_tools/src/create_directory_tool.rs deleted file mode 100644 index 85eea463dc1dfd429dd70ded8c18faf6ee8421c5..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/create_directory_tool.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::AnyWindowHandle; -use gpui::{App, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use ui::IconName; -use util::markdown::MarkdownInlineCode; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct CreateDirectoryToolInput { - /// The path of the new directory. - /// - /// - /// If the project has the following structure: - /// - /// - directory1/ - /// - directory2/ - /// - /// You can create a new directory by providing a path of "directory1/new_directory" - /// - pub path: String, -} - -pub struct CreateDirectoryTool; - -impl Tool for CreateDirectoryTool { - fn name(&self) -> String { - "create_directory".into() - } - - fn description(&self) -> String { - include_str!("./create_directory_tool/description.md").into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn icon(&self) -> IconName { - IconName::ToolFolder - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - format!("Create directory {}", MarkdownInlineCode(&input.path)) - } - Err(_) => "Create directory".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - let project_path = match project.read(cx).find_project_path(&input.path, cx) { - Some(project_path) => project_path, - None => { - return Task::ready(Err(anyhow!("Path to create was outside the project"))).into(); - } - }; - let destination_path: Arc = input.path.as_str().into(); - - cx.spawn(async move |cx| { - project - .update(cx, |project, cx| { - project.create_entry(project_path.clone(), true, cx) - })? - .await - .with_context(|| format!("Creating directory {destination_path}"))?; - - Ok(format!("Created directory {destination_path}").into()) - }) - .into() - } -} diff --git a/crates/assistant_tools/src/create_directory_tool/description.md b/crates/assistant_tools/src/create_directory_tool/description.md deleted file mode 100644 index 52056518c23517bf9fd36bf7d41d7e46947b15b6..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/create_directory_tool/description.md +++ /dev/null @@ -1,3 +0,0 @@ -Creates a new directory at the specified path within the project. Returns confirmation that the directory was created. - -This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project. diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs deleted file mode 100644 index 7c85f1ed7552931822500f76bb9f3b1b1f47fd0c..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/delete_path_tool.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use futures::{SinkExt, StreamExt, channel::mpsc}; -use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::{Project, ProjectPath}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use ui::IconName; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct DeletePathToolInput { - /// The path of the file or directory to delete. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can delete the first file by providing a path of "directory1/a/something.txt" - /// - pub path: String, -} - -pub struct DeletePathTool; - -impl Tool for DeletePathTool { - fn name(&self) -> String { - "delete_path".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true - } - - fn may_perform_edits(&self) -> bool { - true - } - - fn description(&self) -> String { - include_str!("./delete_path_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolDeleteFile - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => format!("Delete “`{}`”", input.path), - Err(_) => "Delete path".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let path_str = match serde_json::from_value::(input) { - Ok(input) => input.path, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else { - return Task::ready(Err(anyhow!( - "Couldn't delete {path_str} because that path isn't in this project." - ))) - .into(); - }; - - let Some(worktree) = project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!( - "Couldn't delete {path_str} because that path isn't in this project." - ))) - .into(); - }; - - let worktree_snapshot = worktree.read(cx).snapshot(); - let (mut paths_tx, mut paths_rx) = mpsc::channel(256); - cx.background_spawn({ - let project_path = project_path.clone(); - async move { - for entry in - worktree_snapshot.traverse_from_path(true, false, false, &project_path.path) - { - if !entry.path.starts_with(&project_path.path) { - break; - } - paths_tx - .send(ProjectPath { - worktree_id: project_path.worktree_id, - path: entry.path.clone(), - }) - .await?; - } - anyhow::Ok(()) - } - }) - .detach(); - - cx.spawn(async move |cx| { - while let Some(path) = paths_rx.next().await { - if let Ok(buffer) = project - .update(cx, |project, cx| project.open_buffer(path, cx))? - .await - { - action_log.update(cx, |action_log, cx| { - action_log.will_delete_buffer(buffer.clone(), cx) - })?; - } - } - - let deletion_task = project - .update(cx, |project, cx| { - project.delete_file(project_path, false, cx) - })? - .with_context(|| { - format!("Couldn't delete {path_str} because that path isn't in this project.") - })?; - deletion_task - .await - .with_context(|| format!("Deleting {path_str}"))?; - Ok(format!("Deleted {path_str}").into()) - }) - .into() - } -} diff --git a/crates/assistant_tools/src/delete_path_tool/description.md b/crates/assistant_tools/src/delete_path_tool/description.md deleted file mode 100644 index dfd4388bf04cf32038d04cacf169e9ea4bf05c56..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/delete_path_tool/description.md +++ /dev/null @@ -1 +0,0 @@ -Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion. diff --git a/crates/assistant_tools/src/diagnostics_tool.rs b/crates/assistant_tools/src/diagnostics_tool.rs deleted file mode 100644 index 75bd683512b58d2fdb6c43fc319d266f6609f926..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/diagnostics_tool.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use language::{DiagnosticSeverity, OffsetRangeExt}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{fmt::Write, sync::Arc}; -use ui::IconName; -use util::markdown::MarkdownInlineCode; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct DiagnosticsToolInput { - /// The path to get diagnostics for. If not provided, returns a project-wide summary. - /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - lorem - /// - ipsum - /// - /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`. - /// - #[serde(deserialize_with = "deserialize_path")] - pub path: Option, -} - -fn deserialize_path<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let opt = Option::::deserialize(deserializer)?; - // The model passes an empty string sometimes - Ok(opt.filter(|s| !s.is_empty())) -} - -pub struct DiagnosticsTool; - -impl Tool for DiagnosticsTool { - fn name(&self) -> String { - "diagnostics".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./diagnostics_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolDiagnostics - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - if let Some(path) = serde_json::from_value::(input.clone()) - .ok() - .and_then(|input| match input.path { - Some(path) if !path.is_empty() => Some(path), - _ => None, - }) - { - format!("Check diagnostics for {}", MarkdownInlineCode(&path)) - } else { - "Check project diagnostics".to_string() - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - match serde_json::from_value::(input) - .ok() - .and_then(|input| input.path) - { - Some(path) if !path.is_empty() => { - let Some(project_path) = project.read(cx).find_project_path(&path, cx) else { - return Task::ready(Err(anyhow!("Could not find path {path} in project",))) - .into(); - }; - - let buffer = - project.update(cx, |project, cx| project.open_buffer(project_path, cx)); - - cx.spawn(async move |cx| { - let mut output = String::new(); - let buffer = buffer.await?; - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - for (_, group) in snapshot.diagnostic_groups(None) { - let entry = &group.entries[group.primary_ix]; - let range = entry.range.to_point(&snapshot); - let severity = match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => "error", - DiagnosticSeverity::WARNING => "warning", - _ => continue, - }; - - writeln!( - output, - "{} at line {}: {}", - severity, - range.start.row + 1, - entry.diagnostic.message - )?; - } - - if output.is_empty() { - Ok("File doesn't have errors or warnings!".to_string().into()) - } else { - Ok(output.into()) - } - }) - .into() - } - _ => { - let project = project.read(cx); - let mut output = String::new(); - let mut has_diagnostics = false; - - for (project_path, _, summary) in project.diagnostic_summaries(true, cx) { - if summary.error_count > 0 || summary.warning_count > 0 { - let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) - else { - continue; - }; - - has_diagnostics = true; - output.push_str(&format!( - "{}: {} error(s), {} warning(s)\n", - worktree.read(cx).absolutize(&project_path.path).display(), - summary.error_count, - summary.warning_count - )); - } - } - - if has_diagnostics { - Task::ready(Ok(output.into())).into() - } else { - Task::ready(Ok("No errors or warnings found in the project." - .to_string() - .into())) - .into() - } - } - } - } -} diff --git a/crates/assistant_tools/src/diagnostics_tool/description.md b/crates/assistant_tools/src/diagnostics_tool/description.md deleted file mode 100644 index 90dc00f1e408c0bd4d79de68833db9d4bafc0d2c..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/diagnostics_tool/description.md +++ /dev/null @@ -1,21 +0,0 @@ -Get errors and warnings for the project or a specific file. - -This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase. - -When a path is provided, shows all diagnostics for that specific file. -When no path is provided, shows a summary of error and warning counts for all files in the project. - - -To get diagnostics for a specific file: -{ - "path": "src/main.rs" -} - -To get a project-wide diagnostic summary: -{} - - - -- If you think you can fix a diagnostic, make 1-2 attempts and then give up. -- Don't remove code you've generated just because you can't fix an error. The user can help you fix it. - diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs deleted file mode 100644 index 840f34aaae9381882e39f8435242625022dfc26c..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ /dev/null @@ -1,2423 +0,0 @@ -use crate::{ - Templates, - edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, - schema::json_schema_for, - ui::{COLLAPSED_LINES, ToolOutputPreview}, -}; -use action_log::ActionLog; -use agent_settings; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ - AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, -}; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{ - Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines, -}; -use futures::StreamExt; -use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, - TextStyleRefinement, WeakEntity, pulsating_between, -}; -use indoc::formatdoc; -use language::{ - Anchor, Buffer, Capability, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope, - TextBuffer, - language_settings::{self, FormatOnSave, SoftWrap}, -}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use paths; -use project::{ - Project, ProjectPath, - lsp_store::{FormatTrigger, LspFormatTarget}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{ - cmp::Reverse, - collections::HashSet, - ffi::OsStr, - ops::Range, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use theme::ThemeSettings; -use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; -use util::{ResultExt, rel_path::RelPath}; -use workspace::Workspace; - -pub struct EditFileTool; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditFileToolInput { - /// A one-line, user-friendly markdown description of the edit. This will be - /// shown in the UI and also passed to another model to perform the edit. - /// - /// Be terse, but also descriptive in what you want to achieve with this - /// edit. Avoid generic instructions. - /// - /// NEVER mention the file path in this description. - /// - /// Fix API endpoint URLs - /// Update copyright year in `page_footer` - /// - /// Make sure to include this field before all the others in the input object - /// so that we can display it immediately. - pub display_description: String, - - /// The full path of the file to create or modify in the project. - /// - /// WARNING: When specifying which file path need changing, you MUST - /// start each path with one of the project's root directories. - /// - /// The following examples assume we have two root directories in the project: - /// - /a/b/backend - /// - /c/d/frontend - /// - /// - /// `backend/src/main.rs` - /// - /// Notice how the file path starts with `backend`. Without that, the path - /// would be ambiguous and the call would fail! - /// - /// - /// - /// `frontend/db.js` - /// - pub path: PathBuf, - - /// The mode of operation on the file. Possible values: - /// - 'edit': Make granular edits to an existing file. - /// - 'create': Create a new file if it doesn't exist. - /// - 'overwrite': Replace the entire contents of an existing file. - /// - /// When a file already exists or you just created it, prefer editing - /// it as opposed to recreating it from scratch. - pub mode: EditFileMode, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -pub enum EditFileMode { - Edit, - Create, - Overwrite, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditFileToolOutput { - pub original_path: PathBuf, - pub new_text: String, - pub old_text: Arc, - pub raw_output: Option, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -struct PartialInput { - #[serde(default)] - path: String, - #[serde(default)] - display_description: String, -} - -const DEFAULT_UI_TEXT: &str = "Editing file"; - -impl Tool for EditFileTool { - fn name(&self) -> String { - "edit_file".into() - } - - fn needs_confirmation( - &self, - input: &serde_json::Value, - project: &Entity, - cx: &App, - ) -> bool { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return false; - } - - let Ok(input) = serde_json::from_value::(input.clone()) else { - // If it's not valid JSON, it's going to error and confirming won't do anything. - return false; - }; - - // If any path component matches the local settings folder, then this could affect - // the editor in ways beyond the project source, so prompt. - let local_settings_folder = paths::local_settings_folder_name(); - let path = Path::new(&input.path); - if path - .components() - .any(|c| c.as_os_str() == >::as_ref(local_settings_folder)) - { - return true; - } - - // It's also possible that the global config dir is configured to be inside the project, - // so check for that edge case too. - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) - && canonical_path.starts_with(paths::config_dir()) - { - return true; - } - - // Check if path is inside the global config directory - // First check if it's already inside project - if not, try to canonicalize - let project_path = project.read(cx).find_project_path(&input.path, cx); - - // If the path is inside the project, and it's not one of the above edge cases, - // then no confirmation is necessary. Otherwise, confirmation is necessary. - project_path.is_none() - } - - fn may_perform_edits(&self) -> bool { - true - } - - fn description(&self) -> String { - include_str!("edit_file_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ToolPencil - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let path = Path::new(&input.path); - let mut description = input.display_description.clone(); - - // Add context about why confirmation may be needed - let local_settings_folder = paths::local_settings_folder_name(); - if path - .components() - .any(|c| c.as_os_str() == >::as_ref(local_settings_folder)) - { - description.push_str(" (local settings)"); - } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) - && canonical_path.starts_with(paths::config_dir()) - { - description.push_str(" (global settings)"); - } - - description - } - Err(_) => "Editing file".to_string(), - } - } - - fn still_streaming_ui_text(&self, input: &serde_json::Value) -> String { - if let Some(input) = serde_json::from_value::(input.clone()).ok() { - let description = input.display_description.trim(); - if !description.is_empty() { - return description.to_string(); - } - - let path = input.path.trim(); - if !path.is_empty() { - return path.to_string(); - } - } - - DEFAULT_UI_TEXT.to_string() - } - - fn run( - self: Arc, - input: serde_json::Value, - request: Arc, - project: Entity, - action_log: Entity, - model: Arc, - window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let project_path = match resolve_path(&input, project.clone(), cx) { - Ok(path) => path, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let card = window.and_then(|window| { - window - .update(cx, |_, window, cx| { - cx.new(|cx| { - EditFileToolCard::new(input.path.clone(), project.clone(), window, cx) - }) - }) - .ok() - }); - - let card_clone = card.clone(); - let action_log_clone = action_log.clone(); - let task = cx.spawn(async move |cx: &mut AsyncApp| { - let edit_format = EditFormat::from_model(model.clone())?; - let edit_agent = EditAgent::new( - model, - project.clone(), - action_log_clone, - Templates::new(), - edit_format, - ); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await?; - - let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let old_text = cx - .background_spawn({ - let old_snapshot = old_snapshot.clone(); - async move { Arc::new(old_snapshot.text()) } - }) - .await; - - if let Some(card) = card_clone.as_ref() { - card.update(cx, |card, cx| card.initialize(buffer.clone(), cx))?; - } - - let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { - edit_agent.edit( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - } else { - edit_agent.overwrite( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - }; - - let mut hallucinated_old_text = false; - let mut ambiguous_ranges = Vec::new(); - while let Some(event) = events.next().await { - match event { - EditAgentOutputEvent::Edited { .. } => { - if let Some(card) = card_clone.as_ref() { - card.update(cx, |card, cx| card.update_diff(cx))?; - } - } - EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, - EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, - EditAgentOutputEvent::ResolvingEditRange(range) => { - if let Some(card) = card_clone.as_ref() { - card.update(cx, |card, cx| card.reveal_range(range, cx))?; - } - } - } - } - let agent_output = output.await?; - - // If format_on_save is enabled, format the buffer - let format_on_save_enabled = buffer - .read_with(cx, |buffer, cx| { - let settings = language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); - !matches!(settings.format_on_save, FormatOnSave::Off) - }) - .unwrap_or(false); - - if format_on_save_enabled { - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - let format_task = project.update(cx, |project, cx| { - project.format( - HashSet::from_iter([buffer.clone()]), - LspFormatTarget::Buffers, - false, // Don't push to history since the tool did it. - FormatTrigger::Save, - cx, - ) - })?; - format_task.await.log_err(); - } - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? - .await?; - - // Notify the action log that we've edited the buffer (*after* formatting has completed). - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let (new_text, diff) = cx - .background_spawn({ - let new_snapshot = new_snapshot.clone(); - let old_text = old_text.clone(); - async move { - let new_text = new_snapshot.text(); - let diff = language::unified_diff(&old_text, &new_text); - - (new_text, diff) - } - }) - .await; - - let output = EditFileToolOutput { - original_path: project_path.path.as_std_path().to_owned(), - new_text, - old_text, - raw_output: Some(agent_output), - }; - - if let Some(card) = card_clone { - card.update(cx, |card, cx| { - card.update_diff(cx); - card.finalize(cx) - }) - .log_err(); - } - - let input_path = input.path.display(); - if diff.is_empty() { - anyhow::ensure!( - !hallucinated_old_text, - formatdoc! {" - Some edits were produced but none of them could be applied. - Read the relevant sections of {input_path} again so that - I can perform the requested edits. - "} - ); - anyhow::ensure!( - ambiguous_ranges.is_empty(), - { - let line_numbers = ambiguous_ranges - .iter() - .map(|range| range.start.to_string()) - .collect::>() - .join(", "); - formatdoc! {" - matches more than one position in the file (lines: {line_numbers}). Read the - relevant sections of {input_path} again and extend so - that I can perform the requested edits. - "} - } - ); - Ok(ToolResultOutput { - content: ToolResultContent::Text("No edits were made.".into()), - output: serde_json::to_value(output).ok(), - }) - } else { - Ok(ToolResultOutput { - content: ToolResultContent::Text(format!( - "Edited {}:\n\n```diff\n{}\n```", - input_path, diff - )), - output: serde_json::to_value(output).ok(), - }) - } - }); - - ToolResult { - output: task, - card: card.map(AnyToolCard::from), - } - } - - fn deserialize_card( - self: Arc, - output: serde_json::Value, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - let output = match serde_json::from_value::(output) { - Ok(output) => output, - Err(_) => return None, - }; - - let card = cx.new(|cx| { - EditFileToolCard::new(output.original_path.clone(), project.clone(), window, cx) - }); - - cx.spawn({ - let path: Arc = output.original_path.into(); - let language_registry = project.read(cx).languages().clone(); - let card = card.clone(); - async move |cx| { - let buffer = - build_buffer(output.new_text, path.clone(), &language_registry, cx).await?; - let buffer_diff = - build_buffer_diff(output.old_text.clone(), &buffer, &language_registry, cx) - .await?; - card.update(cx, |card, cx| { - card.multibuffer.update(cx, |multibuffer, cx| { - let snapshot = buffer.read(cx).snapshot(); - let diff = buffer_diff.read(cx); - let diff_hunk_ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) - .collect::>(); - - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer, - diff_hunk_ranges, - multibuffer_context_lines(cx), - cx, - ); - multibuffer.add_diff(buffer_diff, cx); - let end = multibuffer.len(cx); - card.total_lines = - Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1); - }); - - cx.notify(); - })?; - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - - Some(card.into()) - } -} - -/// Validate that the file path is valid, meaning: -/// -/// - For `edit` and `overwrite`, the path must point to an existing file. -/// - For `create`, the file must not already exist, but it's parent dir must exist. -fn resolve_path( - input: &EditFileToolInput, - project: Entity, - cx: &mut App, -) -> Result { - let project = project.read(cx); - - match input.mode { - EditFileMode::Edit | EditFileMode::Overwrite => { - let path = project - .find_project_path(&input.path, cx) - .context("Can't edit file: path not found")?; - - let entry = project - .entry_for_path(&path, cx) - .context("Can't edit file: path not found")?; - - anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); - Ok(path) - } - - EditFileMode::Create => { - if let Some(path) = project.find_project_path(&input.path, cx) { - anyhow::ensure!( - project.entry_for_path(&path, cx).is_none(), - "Can't create file: file already exists" - ); - } - - let parent_path = input - .path - .parent() - .context("Can't create file: incorrect path")?; - - let parent_project_path = project.find_project_path(&parent_path, cx); - - let parent_entry = parent_project_path - .as_ref() - .and_then(|path| project.entry_for_path(path, cx)) - .context("Can't create file: parent directory doesn't exist")?; - - anyhow::ensure!( - parent_entry.is_dir(), - "Can't create file: parent is not a directory" - ); - - let file_name = input - .path - .file_name() - .and_then(|file_name| file_name.to_str()) - .context("Can't create file: invalid filename")?; - - let new_file_path = parent_project_path.map(|parent| ProjectPath { - path: parent.path.join(RelPath::unix(file_name).unwrap()), - ..parent - }); - - new_file_path.context("Can't create file") - } - } -} - -pub struct EditFileToolCard { - path: PathBuf, - editor: Entity, - multibuffer: Entity, - project: Entity, - buffer: Option>, - base_text: Option>, - buffer_diff: Option>, - revealed_ranges: Vec>, - diff_task: Option>>, - preview_expanded: bool, - error_expanded: Option>, - full_height_expanded: bool, - total_lines: Option, -} - -impl EditFileToolCard { - pub fn new(path: PathBuf, project: Entity, window: &mut Window, cx: &mut App) -> Self { - let expand_edit_card = agent_settings::AgentSettings::get_global(cx).expand_edit_card; - let multibuffer = cx.new(|_| MultiBuffer::without_headers(Capability::ReadOnly)); - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sized_by_content: true, - }, - multibuffer.clone(), - Some(project.clone()), - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - // Keep horizontal scrollbar so user can scroll horizontally if needed - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor - }); - Self { - path, - project, - editor, - multibuffer, - buffer: None, - base_text: None, - buffer_diff: None, - revealed_ranges: Vec::new(), - diff_task: None, - preview_expanded: true, - error_expanded: None, - full_height_expanded: expand_edit_card, - total_lines: None, - } - } - - pub fn initialize(&mut self, buffer: Entity, cx: &mut App) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let base_text = buffer_snapshot.text(); - let language_registry = buffer.read(cx).language_registry(); - let text_snapshot = buffer.read(cx).text_snapshot(); - - // Create a buffer diff with the current text as the base - let buffer_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&text_snapshot, cx); - let _ = diff.set_base_text( - buffer_snapshot.clone(), - language_registry, - text_snapshot, - cx, - ); - diff - }); - - self.buffer = Some(buffer); - self.base_text = Some(base_text.into()); - self.buffer_diff = Some(buffer_diff.clone()); - - // Add the diff to the multibuffer - self.multibuffer - .update(cx, |multibuffer, cx| multibuffer.add_diff(buffer_diff, cx)); - } - - pub fn is_loading(&self) -> bool { - self.total_lines.is_none() - } - - pub fn update_diff(&mut self, cx: &mut Context) { - let Some(buffer) = self.buffer.as_ref() else { - return; - }; - let Some(buffer_diff) = self.buffer_diff.as_ref() else { - return; - }; - - let buffer = buffer.clone(); - let buffer_diff = buffer_diff.clone(); - let base_text = self.base_text.clone(); - self.diff_task = Some(cx.spawn(async move |this, cx| { - let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; - let diff_snapshot = BufferDiff::update_diff( - buffer_diff.clone(), - text_snapshot.clone(), - base_text, - false, - false, - None, - None, - cx, - ) - .await?; - buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &text_snapshot, cx) - })?; - this.update(cx, |this, cx| this.update_visible_ranges(cx)) - })); - } - - pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { - self.revealed_ranges.push(range); - self.update_visible_ranges(cx); - } - - fn update_visible_ranges(&mut self, cx: &mut Context) { - let Some(buffer) = self.buffer.as_ref() else { - return; - }; - - let ranges = self.excerpt_ranges(cx); - self.total_lines = self.multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(buffer, cx), - buffer.clone(), - ranges, - multibuffer_context_lines(cx), - cx, - ); - let end = multibuffer.len(cx); - Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1) - }); - cx.notify(); - } - - fn excerpt_ranges(&self, cx: &App) -> Vec> { - let Some(buffer) = self.buffer.as_ref() else { - return Vec::new(); - }; - let Some(diff) = self.buffer_diff.as_ref() else { - return Vec::new(); - }; - - let buffer = buffer.read(cx); - let diff = diff.read(cx); - let mut ranges = diff - .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer, cx) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>(); - ranges.extend( - self.revealed_ranges - .iter() - .map(|range| range.to_point(buffer)), - ); - ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); - - // Merge adjacent ranges - let mut ranges = ranges.into_iter().peekable(); - let mut merged_ranges = Vec::new(); - while let Some(mut range) = ranges.next() { - while let Some(next_range) = ranges.peek() { - if range.end >= next_range.start { - range.end = range.end.max(next_range.end); - ranges.next(); - } else { - break; - } - } - - merged_ranges.push(range); - } - merged_ranges - } - - pub fn finalize(&mut self, cx: &mut Context) -> Result<()> { - let ranges = self.excerpt_ranges(cx); - let buffer = self.buffer.take().context("card was already finalized")?; - let base_text = self - .base_text - .take() - .context("card was already finalized")?; - let language_registry = self.project.read(cx).languages().clone(); - - // Replace the buffer in the multibuffer with the snapshot - let buffer = cx.new(|cx| { - let language = buffer.read(cx).language().cloned(); - let buffer = TextBuffer::new_normalized( - 0, - cx.entity_id().as_non_zero_u64().into(), - buffer.read(cx).line_ending(), - buffer.read(cx).as_rope().clone(), - ); - let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); - buffer.set_language(language, cx); - buffer - }); - - let buffer_diff = cx.spawn({ - let buffer = buffer.clone(); - async move |_this, cx| { - build_buffer_diff(base_text, &buffer, &language_registry, cx).await - } - }); - - cx.spawn(async move |this, cx| { - let buffer_diff = buffer_diff.await?; - this.update(cx, |this, cx| { - this.multibuffer.update(cx, |multibuffer, cx| { - let path_key = PathKey::for_buffer(&buffer, cx); - multibuffer.clear(cx); - multibuffer.set_excerpts_for_path( - path_key, - buffer, - ranges, - multibuffer_context_lines(cx), - cx, - ); - multibuffer.add_diff(buffer_diff.clone(), cx); - }); - - cx.notify(); - }) - }) - .detach_and_log_err(cx); - Ok(()) - } -} - -impl ToolCard for EditFileToolCard { - fn render( - &mut self, - status: &ToolUseStatus, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement { - let error_message = match status { - ToolUseStatus::Error(err) => Some(err), - _ => None, - }; - - let running_or_pending = match status { - ToolUseStatus::Running | ToolUseStatus::Pending => Some(()), - _ => None, - }; - - let should_show_loading = running_or_pending.is_some() && !self.full_height_expanded; - - let path_label_button = h_flex() - .id(("edit-tool-path-label-button", self.editor.entity_id())) - .w_full() - .max_w_full() - .px_1() - .gap_0p5() - .cursor_pointer() - .rounded_sm() - .opacity(0.8) - .hover(|label| { - label - .opacity(1.) - .bg(cx.theme().colors().element_hover.opacity(0.5)) - }) - .tooltip(Tooltip::text("Jump to File")) - .child( - h_flex() - .child( - Icon::new(IconName::ToolPencil) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - div() - .text_size(rems(0.8125)) - .child(self.path.display().to_string()) - .ml_1p5() - .mr_0p5(), - ) - .child( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Small) - .color(Color::Ignored), - ), - ) - .on_click({ - let path = self.path.clone(); - move |_, window, cx| { - workspace - .update(cx, { - |workspace, cx| { - let Some(project_path) = - workspace.project().read(cx).find_project_path(&path, cx) - else { - return; - }; - let open_task = - workspace.open_path(project_path, None, true, window, cx); - window - .spawn(cx, async move |cx| { - let item = open_task.await?; - if let Some(active_editor) = item.downcast::() { - active_editor - .update_in(cx, |editor, window, cx| { - let snapshot = - editor.buffer().read(cx).snapshot(cx); - let first_hunk = editor - .diff_hunks_in_ranges( - &[editor::Anchor::min() - ..editor::Anchor::max()], - &snapshot, - ) - .next(); - if let Some(first_hunk) = first_hunk { - let first_hunk_start = - first_hunk.multi_buffer_range().start; - editor.change_selections( - Default::default(), - window, - cx, - |selections| { - selections.select_anchor_ranges([ - first_hunk_start - ..first_hunk_start, - ]); - }, - ) - } - }) - .log_err(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - }) - .ok(); - } - }) - .into_any_element(); - - let codeblock_header_bg = cx - .theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)); - - let codeblock_header = h_flex() - .flex_none() - .p_1() - .gap_1() - .justify_between() - .rounded_t_md() - .when(error_message.is_none(), |header| { - header.bg(codeblock_header_bg) - }) - .child(path_label_button) - .when(should_show_loading, |header| { - header.pr_1p5().child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Info) - .with_rotate_animation(2), - ) - }) - .when_some(error_message, |header, error_message| { - header.child( - h_flex() - .gap_1() - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .child( - Disclosure::new( - ("edit-file-error-disclosure", self.editor.entity_id()), - self.error_expanded.is_some(), - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener({ - let error_message = error_message.clone(); - - move |this, _event, _window, cx| { - if this.error_expanded.is_some() { - this.error_expanded.take(); - } else { - this.error_expanded = Some(cx.new(|cx| { - Markdown::new(error_message.clone(), None, None, cx) - })) - } - cx.notify(); - } - })), - ), - ) - }) - .when(error_message.is_none() && !self.is_loading(), |header| { - header.child( - Disclosure::new( - ("edit-file-disclosure", self.editor.entity_id()), - self.preview_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener( - move |this, _event, _window, _cx| { - this.preview_expanded = !this.preview_expanded; - }, - )), - ) - }); - - let (editor, editor_line_height) = self.editor.update(cx, |editor, cx| { - let line_height = editor - .style() - .map(|style| style.text.line_height_in_pixels(window.rem_size())) - .unwrap_or_default(); - - editor.set_text_style_refinement(TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx)) - .into(), - ), - ..TextStyleRefinement::default() - }); - let element = editor.render(window, cx); - (element.into_any_element(), line_height) - }); - - let border_color = cx.theme().colors().border.opacity(0.6); - - let waiting_for_diff = { - let styles = [ - ("w_4_5", (0.1, 0.85), 2000), - ("w_1_4", (0.2, 0.75), 2200), - ("w_2_4", (0.15, 0.64), 1900), - ("w_3_5", (0.25, 0.72), 2300), - ("w_2_5", (0.3, 0.56), 1800), - ]; - - let mut container = v_flex() - .p_3() - .gap_1() - .border_t_1() - .rounded_b_md() - .border_color(border_color) - .bg(cx.theme().colors().editor_background); - - for (width_method, pulse_range, duration_ms) in styles.iter() { - let (min_opacity, max_opacity) = *pulse_range; - let placeholder = match *width_method { - "w_4_5" => div().w_3_4(), - "w_1_4" => div().w_1_4(), - "w_2_4" => div().w_2_4(), - "w_3_5" => div().w_3_5(), - "w_2_5" => div().w_2_5(), - _ => div().w_1_2(), - } - .id("loading_div") - .h_1() - .rounded_full() - .bg(cx.theme().colors().element_active) - .with_animation( - "loading_pulsate", - Animation::new(Duration::from_millis(*duration_ms)) - .repeat() - .with_easing(pulsating_between(min_opacity, max_opacity)), - |label, delta| label.opacity(delta), - ); - - container = container.child(placeholder); - } - - container - }; - - v_flex() - .mb_2() - .border_1() - .when(error_message.is_some(), |card| card.border_dashed()) - .border_color(border_color) - .rounded_md() - .overflow_hidden() - .child(codeblock_header) - .when_some(self.error_expanded.as_ref(), |card, error_markdown| { - card.child( - v_flex() - .p_2() - .gap_1() - .border_t_1() - .border_dashed() - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .rounded_b_md() - .child( - Label::new("Error") - .size(LabelSize::XSmall) - .color(Color::Error), - ) - .child( - div() - .rounded_md() - .text_ui_sm(cx) - .bg(cx.theme().colors().editor_background) - .child(MarkdownElement::new( - error_markdown.clone(), - markdown_style(window, cx), - )), - ), - ) - }) - .when(self.is_loading() && error_message.is_none(), |card| { - card.child(waiting_for_diff) - }) - .when(self.preview_expanded && !self.is_loading(), |card| { - let editor_view = v_flex() - .relative() - .h_full() - .when(!self.full_height_expanded, |editor_container| { - editor_container.max_h(COLLAPSED_LINES as f32 * editor_line_height) - }) - .overflow_hidden() - .border_t_1() - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .child(editor); - - card.child( - ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id()) - .with_total_lines(self.total_lines.unwrap_or(0) as usize) - .toggle_state(self.full_height_expanded) - .with_collapsed_fade() - .on_toggle({ - let this = cx.entity().downgrade(); - move |is_expanded, _window, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, _cx| { - this.full_height_expanded = is_expanded; - }); - } - } - }), - ) - }) - } -} - -fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); - let ui_font_size = TextSize::Default.rems(cx); - let mut text_style = window.text_style(); - - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.ui_font.family.clone()), - font_fallbacks: theme_settings.ui_font.fallbacks.clone(), - font_features: Some(theme_settings.ui_font.features.clone()), - font_size: Some(ui_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style.clone(), - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -async fn build_buffer( - mut text: String, - path: Arc, - language_registry: &Arc, - cx: &mut AsyncApp, -) -> Result> { - let line_ending = LineEnding::detect(&text); - LineEnding::normalize(&mut text); - let text = Rope::from(text); - let language = cx - .update(|_cx| language_registry.load_language_for_file_path(&path))? - .await - .ok(); - let buffer = cx.new(|cx| { - let buffer = TextBuffer::new_normalized( - 0, - cx.entity_id().as_non_zero_u64().into(), - line_ending, - text, - ); - let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); - buffer.set_language(language, cx); - buffer - })?; - Ok(buffer) -} - -async fn build_buffer_diff( - old_text: Arc, - buffer: &Entity, - language_registry: &Arc, - cx: &mut AsyncApp, -) -> Result> { - let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; - - let old_text_rope = cx - .background_spawn({ - let old_text = old_text.clone(); - async move { Rope::from(old_text.as_str()) } - }) - .await; - let base_buffer = cx - .update(|cx| { - Buffer::build_snapshot( - old_text_rope, - buffer.language().cloned(), - Some(language_registry.clone()), - cx, - ) - })? - .await; - - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( - buffer.text.clone(), - Some(old_text), - base_buffer, - cx, - ) - })? - .await; - - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); - diff - })?; - - cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer.text, cx); - diff.set_snapshot(diff_snapshot, &buffer, cx); - diff.set_secondary_diff(secondary_diff); - diff - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use ::fs::Fs; - use client::TelemetrySettings; - use gpui::{TestAppContext, UpdateGlobal}; - use language_model::fake_provider::FakeLanguageModel; - use serde_json::json; - use settings::SettingsStore; - use std::fs; - use util::{path, rel_path::rel_path}; - - #[gpui::test] - async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({})).await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let result = cx - .update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Some edit".into(), - path: "root/nonexistent_file.txt".into(), - mode: EditFileMode::Edit, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Create; - - let result = test_resolve_path(mode, "root/new.txt", cx); - assert_resolved_path_eq(result.await, "new.txt"); - - let result = test_resolve_path(mode, "new.txt", cx); - assert_resolved_path_eq(result.await, "new.txt"); - - let result = test_resolve_path(mode, "dir/new.txt", cx); - assert_resolved_path_eq(result.await, "dir/new.txt"); - - let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: file already exists" - ); - - let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: parent directory doesn't exist" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Edit; - - let path_with_root = "root/dir/subdir/existing.txt"; - let path_without_root = "dir/subdir/existing.txt"; - let result = test_resolve_path(mode, path_with_root, cx); - assert_resolved_path_eq(result.await, path_without_root); - - let result = test_resolve_path(mode, path_without_root, cx); - assert_resolved_path_eq(result.await, path_without_root); - - let result = test_resolve_path(mode, "root/nonexistent.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - - let result = test_resolve_path(mode, "root/dir", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path is a directory" - ); - } - - async fn test_resolve_path( - mode: &EditFileMode, - path: &str, - cx: &mut TestAppContext, - ) -> anyhow::Result { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir": { - "subdir": { - "existing.txt": "hello" - } - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let input = EditFileToolInput { - display_description: "Some edit".into(), - path: path.into(), - mode: mode.clone(), - }; - - cx.update(|cx| resolve_path(&input, project, cx)) - } - - #[track_caller] - fn assert_resolved_path_eq(path: anyhow::Result, expected: &str) { - let actual = path.expect("Should return valid path").path; - assert_eq!(actual.as_ref(), rel_path(expected)); - } - - #[test] - fn still_streaming_ui_text_with_path() { - let input = json!({ - "path": "src/main.rs", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs"); - } - - #[test] - fn still_streaming_ui_text_with_description() { - let input = json!({ - "path": "", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - "Fix error handling", - ); - } - - #[test] - fn still_streaming_ui_text_with_path_and_description() { - let input = json!({ - "path": "src/main.rs", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - "Fix error handling", - ); - } - - #[test] - fn still_streaming_ui_text_no_path_or_description() { - let input = json!({ - "path": "", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - }); - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - - #[test] - fn still_streaming_ui_text_with_null() { - let input = serde_json::Value::Null; - - assert_eq!( - EditFileTool.still_streaming_ui_text(&input), - DEFAULT_UI_TEXT, - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - TelemetrySettings::register(cx); - agent_settings::AgentSettings::register(cx); - Project::init_settings(cx); - }); - } - - fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { - cx.update(|cx| { - paths::set_custom_data_dir(data_dir.to_str().unwrap()); - // Set custom data directory (config will be under data_dir/config) - - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - TelemetrySettings::register(cx); - agent_settings::AgentSettings::register(cx); - Project::init_settings(cx); - }); - } - - #[gpui::test] - async fn test_format_on_save(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Set up a Rust language with LSP formatting support - let rust_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Rust".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - )); - - // Register the language and fake LSP - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(rust_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Rust", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Create the file - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/root/src/main.rs"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; - const FORMATTED_CONTENT: &str = - "This file was formatted by the fake formatter in the test.\n"; - - // Get the fake language server and set up formatting handler - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::({ - |_, _| async move { - Ok(Some(vec![lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), - new_text: FORMATTED_CONTENT.to_string(), - }])) - } - }); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // First, test with format_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On); - settings.project.all_languages.defaults.formatter = - Some(language::language_settings::FormatterList::default()); - }); - }); - }); - - // Have the model stream unformatted content - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify it was formatted automatically - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - FORMATTED_CONTENT, - "Code should be formatted when format_on_save is enabled" - ); - - let stale_buffer_count = action_log.read_with(cx, |log, cx| log.stale_buffers(cx).count()); - - assert_eq!( - stale_buffer_count, 0, - "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ - This causes the agent to think the file was modified externally when it was just formatted.", - stale_buffer_count - ); - - // Next, test with format_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.format_on_save = - Some(FormatOnSave::Off); - }); - }); - }); - - // Stream unformatted edits again - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file was not formatted - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - UNFORMATTED_CONTENT, - "Code should not be formatted when format_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - // Create a simple file with trailing whitespace - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // First, test with remove_trailing_whitespace_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project - .all_languages - .defaults - .remove_trailing_whitespace_on_save = Some(true); - }); - }); - }); - - const CONTENT_WITH_TRAILING_WHITESPACE: &str = - "fn main() { \n println!(\"Hello!\"); \n}\n"; - - // Have the model stream content that contains trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify trailing whitespace was removed automatically - assert_eq!( - // Ignore carriage returns on Windows - fs.load(path!("/root/src/main.rs").as_ref()) - .await - .unwrap() - .replace("\r\n", "\n"), - "fn main() {\n println!(\"Hello!\");\n}\n", - "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" - ); - - // Next, test with remove_trailing_whitespace_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project - .all_languages - .defaults - .remove_trailing_whitespace_on_save = Some(false); - }); - }); - }); - - // Stream edits again with trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = serde_json::to_value(EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }) - .unwrap(); - Arc::new(EditFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file still has trailing whitespace - // Read the file again - it should still have trailing whitespace - let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - final_content.replace("\r\n", "\n"), - CONTENT_WITH_TRAILING_WHITESPACE, - "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_needs_confirmation(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({})).await; - - // Test 1: Path with .zed component should require confirmation - let input_with_zed = json!({ - "display_description": "Edit settings", - "path": ".zed/settings.json", - "mode": "edit" - }); - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_with_zed, &project, cx), - "Path with .zed component should require confirmation" - ); - }); - - // Test 2: Absolute path should require confirmation - let input_absolute = json!({ - "display_description": "Edit file", - "path": "/etc/hosts", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_absolute, &project, cx), - "Absolute path should require confirmation" - ); - }); - - // Test 3: Relative path without .zed should not require confirmation - let input_relative = json!({ - "display_description": "Edit file", - "path": "root/src/main.rs", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input_relative, &project, cx), - "Relative path without .zed should not require confirmation" - ); - }); - - // Test 4: Path with .zed in the middle should require confirmation - let input_zed_middle = json!({ - "display_description": "Edit settings", - "path": "root/.zed/tasks.json", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_zed_middle, &project, cx), - "Path with .zed in any component should require confirmation" - ); - }); - - // Test 5: When always_allow_tool_actions is enabled, no confirmation needed - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = true; - agent_settings::AgentSettings::override_global(settings, cx); - - assert!( - !tool.needs_confirmation(&input_with_zed, &project, cx), - "When always_allow_tool_actions is true, no confirmation should be needed" - ); - assert!( - !tool.needs_confirmation(&input_absolute, &project, cx), - "When always_allow_tool_actions is true, no confirmation should be needed for absolute paths" - ); - }); - } - - #[gpui::test] - async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { - // Set up a custom config directory for testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - - // Test ui_text shows context for various paths - let test_cases = vec![ - ( - json!({ - "display_description": "Update config", - "path": ".zed/settings.json", - "mode": "edit" - }), - "Update config (local settings)", - ".zed path should show local settings context", - ), - ( - json!({ - "display_description": "Fix bug", - "path": "src/.zed/local.json", - "mode": "edit" - }), - "Fix bug (local settings)", - "Nested .zed path should show local settings context", - ), - ( - json!({ - "display_description": "Update readme", - "path": "README.md", - "mode": "edit" - }), - "Update readme", - "Normal path should not show additional context", - ), - ( - json!({ - "display_description": "Edit config", - "path": "config.zed", - "mode": "edit" - }), - "Edit config", - ".zed as extension should not show context", - ), - ]; - - for (input, expected_text, description) in test_cases { - cx.update(|_cx| { - let ui_text = tool.ui_text(&input); - assert_eq!(ui_text, expected_text, "Failed for case: {}", description); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - - // Create a project in /project directory - fs.insert_tree("/project", json!({})).await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test file outside project requires confirmation - let input_outside = json!({ - "display_description": "Edit file", - "path": "/outside/file.txt", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_outside, &project, cx), - "File outside project should require confirmation" - ); - }); - - // Test file inside project doesn't require confirmation - let input_inside = json!({ - "display_description": "Edit file", - "path": "project/file.txt", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input_inside, &project, cx), - "File inside project should not require confirmation" - ); - }); - } - - #[gpui::test] - async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { - // Set up a custom data directory for testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/home/user/myproject", json!({})).await; - let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; - - // Get the actual local settings folder name - let local_settings_folder = paths::local_settings_folder_name(); - - // Test various config path patterns - let test_cases = vec![ - ( - format!("{local_settings_folder}/settings.json"), - true, - "Top-level local settings file".to_string(), - ), - ( - format!("myproject/{local_settings_folder}/settings.json"), - true, - "Local settings in project path".to_string(), - ), - ( - format!("src/{local_settings_folder}/config.toml"), - true, - "Local settings in subdirectory".to_string(), - ), - ( - ".zed.backup/file.txt".to_string(), - true, - ".zed.backup is outside project".to_string(), - ), - ( - "my.zed/file.txt".to_string(), - true, - "my.zed is outside project".to_string(), - ), - ( - "myproject/src/file.zed".to_string(), - false, - ".zed as file extension".to_string(), - ), - ( - "myproject/normal/path/file.rs".to_string(), - false, - "Normal file without config paths".to_string(), - ), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {} - path: {}", - description, - path - ); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { - // Set up a custom data directory for testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - - // Create test files in the global config directory - let global_config_dir = paths::config_dir(); - fs::create_dir_all(&global_config_dir).unwrap(); - let global_settings_path = global_config_dir.join("settings.json"); - fs::write(&global_settings_path, "{}").unwrap(); - - fs.insert_tree("/project", json!({})).await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test global config paths - let test_cases = vec![ - ( - global_settings_path.to_str().unwrap().to_string(), - true, - "Global settings file should require confirmation", - ), - ( - global_config_dir - .join("keymap.json") - .to_str() - .unwrap() - .to_string(), - true, - "Global keymap file should require confirmation", - ), - ( - "project/normal_file.rs".to_string(), - false, - "Normal project file should not require confirmation", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {}", - description - ); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - - // Create multiple worktree directories - fs.insert_tree( - "/workspace/frontend", - json!({ - "src": { - "main.js": "console.log('frontend');" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/backend", - json!({ - "src": { - "main.rs": "fn main() {}" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/shared", - json!({ - ".zed": { - "settings.json": "{}" - } - }), - ) - .await; - - // Create project with multiple worktrees - let project = Project::test( - fs.clone(), - [ - path!("/workspace/frontend").as_ref(), - path!("/workspace/backend").as_ref(), - path!("/workspace/shared").as_ref(), - ], - cx, - ) - .await; - - // Test files in different worktrees - let test_cases = vec![ - ("frontend/src/main.js", false, "File in first worktree"), - ("backend/src/main.rs", false, "File in second worktree"), - ( - "shared/.zed/settings.json", - true, - ".zed file in third worktree", - ), - ("/etc/hosts", true, "Absolute path outside all worktrees"), - ( - "../outside/file.txt", - true, - "Relative path outside worktrees", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {} - path: {}", - description, - path - ); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".zed": { - "settings.json": "{}" - }, - "src": { - ".zed": { - "local.json": "{}" - } - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test edge cases - let test_cases = vec![ - // Empty path - find_project_path returns Some for empty paths - ("", false, "Empty path is treated as project root"), - // Root directory - ("/", true, "Root directory should be outside project"), - ("project/../other", true, "Path with .. is outside project"), - ( - "project/./src/file.rs", - false, - "Path with . should work normally", - ), - // Windows-style paths (if on Windows) - #[cfg(target_os = "windows")] - ("C:\\Windows\\System32\\hosts", true, "Windows system path"), - #[cfg(target_os = "windows")] - ("project\\src\\main.rs", false, "Windows-style project path"), - ]; - - for (path, should_confirm, description) in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert_eq!( - tool.needs_confirmation(&input, &project, cx), - should_confirm, - "Failed for case: {} - path: {}", - description, - path - ); - }); - } - } - - #[gpui::test] - async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - - // Test UI text for various scenarios - let test_cases = vec![ - ( - json!({ - "display_description": "Update config", - "path": ".zed/settings.json", - "mode": "edit" - }), - "Update config (local settings)", - ".zed path should show local settings context", - ), - ( - json!({ - "display_description": "Fix bug", - "path": "src/.zed/local.json", - "mode": "edit" - }), - "Fix bug (local settings)", - "Nested .zed path should show local settings context", - ), - ( - json!({ - "display_description": "Update readme", - "path": "README.md", - "mode": "edit" - }), - "Update readme", - "Normal path should not show additional context", - ), - ( - json!({ - "display_description": "Edit config", - "path": "config.zed", - "mode": "edit" - }), - "Edit config", - ".zed as extension should not show context", - ), - ]; - - for (input, expected_text, description) in test_cases { - cx.update(|_cx| { - let ui_text = tool.ui_text(&input); - assert_eq!(ui_text, expected_text, "Failed for case: {}", description); - }); - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { - init_test(cx); - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "existing.txt": "content", - ".zed": { - "settings.json": "{}" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Test different EditFileMode values - let modes = vec![ - EditFileMode::Edit, - EditFileMode::Create, - EditFileMode::Overwrite, - ]; - - for mode in modes { - // Test .zed path with different modes - let input_zed = json!({ - "display_description": "Edit settings", - "path": "project/.zed/settings.json", - "mode": mode - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_zed, &project, cx), - ".zed path should require confirmation regardless of mode: {:?}", - mode - ); - }); - - // Test outside path with different modes - let input_outside = json!({ - "display_description": "Edit file", - "path": "/outside/file.txt", - "mode": mode - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input_outside, &project, cx), - "Outside path should require confirmation regardless of mode: {:?}", - mode - ); - }); - - // Test normal path with different modes - let input_normal = json!({ - "display_description": "Edit file", - "path": "project/normal.txt", - "mode": mode - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input_normal, &project, cx), - "Normal path should not require confirmation regardless of mode: {:?}", - mode - ); - }); - } - } - - #[gpui::test] - async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { - // Set up with custom directories for deterministic testing - let temp_dir = tempfile::tempdir().unwrap(); - init_test_with_config(cx, temp_dir.path()); - - let tool = Arc::new(EditFileTool); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({})).await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - - // Enable always_allow_tool_actions - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = true; - agent_settings::AgentSettings::override_global(settings, cx); - }); - - // Test that all paths that normally require confirmation are bypassed - let global_settings_path = paths::config_dir().join("settings.json"); - fs::create_dir_all(paths::config_dir()).unwrap(); - fs::write(&global_settings_path, "{}").unwrap(); - - let test_cases = vec![ - ".zed/settings.json", - "project/.zed/config.toml", - global_settings_path.to_str().unwrap(), - "/etc/hosts", - "/absolute/path/file.txt", - "../outside/project.txt", - ]; - - for path in test_cases { - let input = json!({ - "display_description": "Edit file", - "path": path, - "mode": "edit" - }); - cx.update(|cx| { - assert!( - !tool.needs_confirmation(&input, &project, cx), - "Path {} should not require confirmation when always_allow_tool_actions is true", - path - ); - }); - } - - // Disable always_allow_tool_actions and verify confirmation is required again - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = false; - agent_settings::AgentSettings::override_global(settings, cx); - }); - - // Verify .zed path requires confirmation again - let input = json!({ - "display_description": "Edit file", - "path": ".zed/settings.json", - "mode": "edit" - }); - cx.update(|cx| { - assert!( - tool.needs_confirmation(&input, &project, cx), - ".zed path should require confirmation when always_allow_tool_actions is false" - ); - }); - } -} diff --git a/crates/assistant_tools/src/edit_file_tool/description.md b/crates/assistant_tools/src/edit_file_tool/description.md deleted file mode 100644 index 27f8e49dd626a2d1a5266b90413a3a5f8e02e6d8..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/edit_file_tool/description.md +++ /dev/null @@ -1,8 +0,0 @@ -This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. - -Before using this tool: - -1. Use the `read_file` tool to understand the file's contents and context - -2. Verify the directory path is correct (only applicable when creating new files): - - Use the `list_directory` tool to verify the parent directory exists and is the correct location diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs deleted file mode 100644 index cc22c9fc09f73914720c4b639f8d273207d7ca53..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/fetch_tool.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; -use std::{borrow::Cow, cell::RefCell}; - -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow, bail}; -use assistant_tool::{Tool, ToolResult}; -use futures::AsyncReadExt as _; -use gpui::{AnyWindowHandle, App, AppContext as _, Entity, Task}; -use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; -use http_client::{AsyncBody, HttpClientWithUrl}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::IconName; -use util::markdown::MarkdownEscaped; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ContentType { - Html, - Plaintext, - Json, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct FetchToolInput { - /// The URL to fetch. - url: String, -} - -pub struct FetchTool { - http_client: Arc, -} - -impl FetchTool { - pub fn new(http_client: Arc) -> Self { - Self { http_client } - } - - async fn build_message(http_client: Arc, url: &str) -> Result { - let url = if !url.starts_with("https://") && !url.starts_with("http://") { - Cow::Owned(format!("https://{url}")) - } else { - Cow::Borrowed(url) - }; - - let mut response = http_client.get(&url, AsyncBody::default(), true).await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading response body")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let Some(content_type) = response.headers().get("content-type") else { - bail!("missing Content-Type header"); - }; - let content_type = content_type - .to_str() - .context("invalid Content-Type header")?; - let content_type = match content_type { - "text/html" | "application/xhtml+xml" => ContentType::Html, - "application/json" => ContentType::Json, - _ => ContentType::Plaintext, - }; - - match content_type { - ContentType::Html => { - let mut handlers: Vec = vec![ - Rc::new(RefCell::new(markdown::WebpageChromeRemover)), - Rc::new(RefCell::new(markdown::ParagraphHandler)), - Rc::new(RefCell::new(markdown::HeadingHandler)), - Rc::new(RefCell::new(markdown::ListHandler)), - Rc::new(RefCell::new(markdown::TableHandler::new())), - Rc::new(RefCell::new(markdown::StyledTextHandler)), - ]; - if url.contains("wikipedia.org") { - use html_to_markdown::structure::wikipedia; - - handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover))); - handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler))); - handlers.push(Rc::new( - RefCell::new(wikipedia::WikipediaCodeHandler::new()), - )); - } else { - handlers.push(Rc::new(RefCell::new(markdown::CodeHandler))); - } - - convert_html_to_markdown(&body[..], &mut handlers) - } - ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()), - ContentType::Json => { - let json: serde_json::Value = serde_json::from_slice(&body)?; - - Ok(format!( - "```json\n{}\n```", - serde_json::to_string_pretty(&json)? - )) - } - } - } -} - -impl Tool for FetchTool { - fn name(&self) -> String { - "fetch".to_string() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./fetch_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ToolWeb - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)), - Err(_) => "Fetch URL".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let text = cx.background_spawn({ - let http_client = self.http_client.clone(); - async move { Self::build_message(http_client, &input.url).await } - }); - - cx.foreground_executor() - .spawn(async move { - let text = text.await?; - if text.trim().is_empty() { - bail!("no textual content found"); - } - - Ok(text.into()) - }) - .into() - } -} diff --git a/crates/assistant_tools/src/fetch_tool/description.md b/crates/assistant_tools/src/fetch_tool/description.md deleted file mode 100644 index 007ba6c60864c2185740b40222a32b05d2819bf0..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/fetch_tool/description.md +++ /dev/null @@ -1 +0,0 @@ -Fetches a URL and returns the content as Markdown. diff --git a/crates/assistant_tools/src/find_path_tool.rs b/crates/assistant_tools/src/find_path_tool.rs deleted file mode 100644 index 0bc478251cb5d3d558dda4fb41df02e85eaafde2..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/find_path_tool.rs +++ /dev/null @@ -1,472 +0,0 @@ -use crate::{schema::json_schema_for, ui::ToolCallCardHeader}; -use action_log::ActionLog; -use anyhow::{Result, anyhow}; -use assistant_tool::{ - Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, -}; -use editor::Editor; -use futures::channel::oneshot::{self, Receiver}; -use gpui::{ - AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window, -}; -use language; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::{cmp, path::PathBuf, sync::Arc}; -use ui::{Disclosure, Tooltip, prelude::*}; -use util::{ResultExt, paths::PathMatcher}; -use workspace::Workspace; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct FindPathToolInput { - /// The glob to match against every path in the project. - /// - /// - /// If the project has the following root directories: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can get back the first two paths by providing a glob of "*thing*.txt" - /// - pub glob: String, - - /// Optional starting position for paginated results (0-based). - /// When not provided, starts from the beginning. - #[serde(default)] - pub offset: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -struct FindPathToolOutput { - glob: String, - paths: Vec, -} - -const RESULTS_PER_PAGE: usize = 50; - -pub struct FindPathTool; - -impl Tool for FindPathTool { - fn name(&self) -> String { - "find_path".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./find_path_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolSearch - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => format!("Find paths matching “`{}`”", input.glob), - Err(_) => "Search paths".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let (offset, glob) = match serde_json::from_value::(input) { - Ok(input) => (input.offset, input.glob), - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let (sender, receiver) = oneshot::channel(); - - let card = cx.new(|cx| FindPathToolCard::new(glob.clone(), receiver, cx)); - - let search_paths_task = search_paths(&glob, project, cx); - - let task = cx.background_spawn(async move { - let matches = search_paths_task.await?; - let paginated_matches: &[PathBuf] = &matches[cmp::min(offset, matches.len()) - ..cmp::min(offset + RESULTS_PER_PAGE, matches.len())]; - - sender.send(paginated_matches.to_vec()).log_err(); - - if matches.is_empty() { - Ok("No matches found".to_string().into()) - } else { - let mut message = format!("Found {} total matches.", matches.len()); - if matches.len() > RESULTS_PER_PAGE { - write!( - &mut message, - "\nShowing results {}-{} (provide 'offset' parameter for more results):", - offset + 1, - offset + paginated_matches.len() - ) - .unwrap(); - } - - for mat in matches.iter().skip(offset).take(RESULTS_PER_PAGE) { - write!(&mut message, "\n{}", mat.display()).unwrap(); - } - - let output = FindPathToolOutput { - glob, - paths: matches, - }; - - Ok(ToolResultOutput { - content: ToolResultContent::Text(message), - output: Some(serde_json::to_value(output)?), - }) - } - }); - - ToolResult { - output: task, - card: Some(card.into()), - } - } - - fn deserialize_card( - self: Arc, - output: serde_json::Value, - _project: Entity, - _window: &mut Window, - cx: &mut App, - ) -> Option { - let output = serde_json::from_value::(output).ok()?; - let card = cx.new(|_| FindPathToolCard::from_output(output)); - Some(card.into()) - } -} - -fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task>> { - let path_matcher = match PathMatcher::new( - [ - // Sometimes models try to search for "". In this case, return all paths in the project. - if glob.is_empty() { "*" } else { glob }, - ], - project.read(cx).path_style(cx), - ) { - Ok(matcher) => matcher, - Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))), - }; - let snapshots: Vec<_> = project - .read(cx) - .worktrees(cx) - .map(|worktree| worktree.read(cx).snapshot()) - .collect(); - - cx.background_spawn(async move { - Ok(snapshots - .iter() - .flat_map(|snapshot| { - snapshot - .entries(false, 0) - .map(move |entry| { - snapshot - .root_name() - .join(&entry.path) - .as_std_path() - .to_path_buf() - }) - .filter(|path| path_matcher.is_match(&path)) - }) - .collect()) - }) -} - -struct FindPathToolCard { - paths: Vec, - expanded: bool, - glob: String, - _receiver_task: Option>>, -} - -impl FindPathToolCard { - fn new(glob: String, receiver: Receiver>, cx: &mut Context) -> Self { - let _receiver_task = cx.spawn(async move |this, cx| { - let paths = receiver.await?; - - this.update(cx, |this, _cx| { - this.paths = paths; - }) - .log_err(); - - Ok(()) - }); - - Self { - paths: Vec::new(), - expanded: false, - glob, - _receiver_task: Some(_receiver_task), - } - } - - fn from_output(output: FindPathToolOutput) -> Self { - Self { - glob: output.glob, - paths: output.paths, - expanded: false, - _receiver_task: None, - } - } -} - -impl ToolCard for FindPathToolCard { - fn render( - &mut self, - _status: &ToolUseStatus, - _window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement { - let matches_label: SharedString = if self.paths.is_empty() { - "No matches".into() - } else if self.paths.len() == 1 { - "1 match".into() - } else { - format!("{} matches", self.paths.len()).into() - }; - - let content = if !self.paths.is_empty() && self.expanded { - Some( - v_flex() - .relative() - .ml_1p5() - .px_1p5() - .gap_0p5() - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .children(self.paths.iter().enumerate().map(|(index, path)| { - let path_clone = path.clone(); - let workspace_clone = workspace.clone(); - let button_label = path.to_string_lossy().into_owned(); - - Button::new(("path", index), button_label) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End) - .label_size(LabelSize::Small) - .color(Color::Muted) - .tooltip(Tooltip::text("Jump to File")) - .on_click(move |_, window, cx| { - workspace_clone - .update(cx, |workspace, cx| { - let path = PathBuf::from(&path_clone); - let Some(project_path) = workspace - .project() - .read(cx) - .find_project_path(&path, cx) - else { - return; - }; - let open_task = workspace.open_path( - project_path, - None, - true, - window, - cx, - ); - window - .spawn(cx, async move |cx| { - let item = open_task.await?; - if let Some(active_editor) = - item.downcast::() - { - active_editor - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new(0, 0), - window, - cx, - ); - }) - .log_err(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }) - .ok(); - }) - })) - .into_any(), - ) - } else { - None - }; - - v_flex() - .mb_2() - .gap_1() - .child( - ToolCallCardHeader::new(IconName::ToolSearch, matches_label) - .with_code_path(&self.glob) - .disclosure_slot( - Disclosure::new("path-search-disclosure", self.expanded) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .disabled(self.paths.is_empty()) - .on_click(cx.listener(move |this, _, _, _cx| { - this.expanded = !this.expanded; - })), - ), - ) - .children(content) - } -} - -impl Component for FindPathTool { - fn scope() -> ComponentScope { - ComponentScope::Agent - } - - fn sort_name() -> &'static str { - "FindPathTool" - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let successful_card = cx.new(|_| FindPathToolCard { - paths: vec![ - PathBuf::from("src/main.rs"), - PathBuf::from("src/lib.rs"), - PathBuf::from("tests/test.rs"), - ], - expanded: true, - glob: "*.rs".to_string(), - _receiver_task: None, - }); - - let empty_card = cx.new(|_| FindPathToolCard { - paths: Vec::new(), - expanded: false, - glob: "*.nonexistent".to_string(), - _receiver_task: None, - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group(vec![ - single_example( - "With Paths", - div() - .size_full() - .child(successful_card.update(cx, |tool, cx| { - tool.render( - &ToolUseStatus::Finished("".into()), - window, - WeakEntity::new_invalid(), - cx, - ) - .into_any_element() - })) - .into_any_element(), - ), - single_example( - "No Paths", - div() - .size_full() - .child(empty_card.update(cx, |tool, cx| { - tool.render( - &ToolUseStatus::Finished("".into()), - window, - WeakEntity::new_invalid(), - cx, - ) - .into_any_element() - })) - .into_any_element(), - ), - ])]) - .into_any_element(), - ) - } -} - -#[cfg(test)] -mod test { - use super::*; - use gpui::TestAppContext; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_find_path_tool(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - serde_json::json!({ - "apple": { - "banana": { - "carrot": "1", - }, - "bandana": { - "carbonara": "2", - }, - "endive": "3" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let matches = cx - .update(|cx| search_paths("root/**/car*", project.clone(), cx)) - .await - .unwrap(); - assert_eq!( - matches, - &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) - ] - ); - - let matches = cx - .update(|cx| search_paths("**/car*", project.clone(), cx)) - .await - .unwrap(); - assert_eq!( - matches, - &[ - PathBuf::from(path!("root/apple/banana/carrot")), - PathBuf::from(path!("root/apple/bandana/carbonara")) - ] - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - } -} diff --git a/crates/assistant_tools/src/find_path_tool/description.md b/crates/assistant_tools/src/find_path_tool/description.md deleted file mode 100644 index f7a697c467b2807c1f4cf1706ef660a77b9ee727..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/find_path_tool/description.md +++ /dev/null @@ -1,7 +0,0 @@ -Fast file path pattern matching tool that works with any codebase size - -- Supports glob patterns like "**/*.js" or "src/**/*.ts" -- Returns matching file paths sorted alphabetically -- Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths. -- Use this tool when you need to find files by name patterns -- Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages. diff --git a/crates/assistant_tools/src/grep_tool.rs b/crates/assistant_tools/src/grep_tool.rs deleted file mode 100644 index 609e25f338d11995ea6f587ba476e4f95274e4e9..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/grep_tool.rs +++ /dev/null @@ -1,1308 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use futures::StreamExt; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use language::{OffsetRangeExt, ParseStatus, Point}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::{ - Project, WorktreeSettings, - search::{SearchQuery, SearchResult}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{cmp, fmt::Write, sync::Arc}; -use ui::IconName; -use util::RangeExt; -use util::markdown::MarkdownInlineCode; -use util::paths::PathMatcher; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct GrepToolInput { - /// A regex pattern to search for in the entire project. Note that the regex - /// will be parsed by the Rust `regex` crate. - /// - /// Do NOT specify a path here! This will only be matched against the code **content**. - pub regex: String, - - /// A glob pattern for the paths of files to include in the search. - /// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts". - /// If omitted, all files in the project will be searched. - pub include_pattern: Option, - - /// Optional starting position for paginated results (0-based). - /// When not provided, starts from the beginning. - #[serde(default)] - pub offset: u32, - - /// Whether the regex is case-sensitive. Defaults to false (case-insensitive). - #[serde(default)] - pub case_sensitive: bool, -} - -impl GrepToolInput { - /// Which page of search results this is. - pub fn page(&self) -> u32 { - 1 + (self.offset / RESULTS_PER_PAGE) - } -} - -const RESULTS_PER_PAGE: u32 = 20; - -pub struct GrepTool; - -impl Tool for GrepTool { - fn name(&self) -> String { - "grep".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./grep_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolRegex - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let page = input.page(); - let regex_str = MarkdownInlineCode(&input.regex); - let case_info = if input.case_sensitive { - " (case-sensitive)" - } else { - "" - }; - - if page > 1 { - format!("Get page {page} of search results for regex {regex_str}{case_info}") - } else { - format!("Search files for regex {regex_str}{case_info}") - } - } - Err(_) => "Search with regex".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - const CONTEXT_LINES: u32 = 2; - const MAX_ANCESTOR_LINES: u32 = 10; - - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(error) => { - return Task::ready(Err(anyhow!("Failed to parse input: {error}"))).into(); - } - }; - - let include_matcher = match PathMatcher::new( - input - .include_pattern - .as_ref() - .into_iter() - .collect::>(), - project.read(cx).path_style(cx), - ) { - Ok(matcher) => matcher, - Err(error) => { - return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))).into(); - } - }; - - // Exclude global file_scan_exclusions and private_files settings - let exclude_matcher = { - let global_settings = WorktreeSettings::get_global(cx); - let exclude_patterns = global_settings - .file_scan_exclusions - .sources() - .iter() - .chain(global_settings.private_files.sources().iter()); - - match PathMatcher::new(exclude_patterns, project.read(cx).path_style(cx)) { - Ok(matcher) => matcher, - Err(error) => { - return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))).into(); - } - } - }; - - let query = match SearchQuery::regex( - &input.regex, - false, - input.case_sensitive, - false, - false, - include_matcher, - exclude_matcher, - true, // Always match file include pattern against *full project paths* that start with a project root. - None, - ) { - Ok(query) => query, - Err(error) => return Task::ready(Err(error)).into(), - }; - - let results = project.update(cx, |project, cx| project.search(query, cx)); - - cx.spawn(async move |cx| { - futures::pin_mut!(results); - - let mut output = String::new(); - let mut skips_remaining = input.offset; - let mut matches_found = 0; - let mut has_more_matches = false; - - 'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await { - if ranges.is_empty() { - continue; - } - - let Ok((Some(path), mut parse_status)) = buffer.read_with(cx, |buffer, cx| { - (buffer.file().map(|file| file.full_path(cx)), buffer.parse_status()) - }) else { - continue; - }; - - // Check if this file should be excluded based on its worktree settings - if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { - project.find_project_path(&path, cx) - }) - && cx.update(|cx| { - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - worktree_settings.is_path_excluded(&project_path.path) - || worktree_settings.is_path_private(&project_path.path) - }).unwrap_or(false) { - continue; - } - - while *parse_status.borrow() != ParseStatus::Idle { - parse_status.changed().await?; - } - - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - let mut ranges = ranges - .into_iter() - .map(|range| { - let matched = range.to_point(&snapshot); - let matched_end_line_len = snapshot.line_len(matched.end.row); - let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len); - let symbols = snapshot.symbols_containing(matched.start, None); - - if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) { - let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot); - let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES); - let end_col = snapshot.line_len(end_row); - let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col); - - if capped_ancestor_range.contains_inclusive(&full_lines) { - return (capped_ancestor_range, Some(full_ancestor_range), symbols) - } - } - - let mut matched = matched; - matched.start.column = 0; - matched.start.row = - matched.start.row.saturating_sub(CONTEXT_LINES); - matched.end.row = cmp::min( - snapshot.max_point().row, - matched.end.row + CONTEXT_LINES, - ); - matched.end.column = snapshot.line_len(matched.end.row); - - (matched, None, symbols) - }) - .peekable(); - - let mut file_header_written = false; - - while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){ - if skips_remaining > 0 { - skips_remaining -= 1; - continue; - } - - // We'd already found a full page of matches, and we just found one more. - if matches_found >= RESULTS_PER_PAGE { - has_more_matches = true; - break 'outer; - } - - while let Some((next_range, _, _)) = ranges.peek() { - if range.end.row >= next_range.start.row { - range.end = next_range.end; - ranges.next(); - } else { - break; - } - } - - if !file_header_written { - writeln!(output, "\n## Matches in {}", path.display())?; - file_header_written = true; - } - - let end_row = range.end.row; - output.push_str("\n### "); - - for symbol in parent_symbols { - write!(output, "{} › ", symbol.text)?; - } - - if range.start.row == end_row { - writeln!(output, "L{}", range.start.row + 1)?; - } else { - writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?; - } - - output.push_str("```\n"); - output.extend(snapshot.text_for_range(range)); - output.push_str("\n```\n"); - - if let Some(ancestor_range) = ancestor_range - && end_row < ancestor_range.end.row { - let remaining_lines = ancestor_range.end.row - end_row; - writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; - } - - matches_found += 1; - } - } - - if matches_found == 0 { - Ok("No matches found".to_string().into()) - } else if has_more_matches { - Ok(format!( - "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", - input.offset + 1, - input.offset + matches_found, - input.offset + RESULTS_PER_PAGE, - ).into()) - } else { - Ok(format!("Found {matches_found} matches:\n{output}").into()) - } - }).into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use assistant_tool::Tool; - use gpui::{AppContext, TestAppContext, UpdateGlobal}; - use language::{Language, LanguageConfig, LanguageMatcher}; - use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use unindent::Unindent; - use util::path; - - #[gpui::test] - async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "src": { - "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}", - "utils": { - "helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}", - }, - }, - "tests": { - "test_main.rs": "fn test_main() {\n assert!(true);\n}", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Test with include pattern for Rust files inside the root of the project - let input = serde_json::to_value(GrepToolInput { - regex: "println".to_string(), - include_pattern: Some("root/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!(result.contains("main.rs"), "Should find matches in main.rs"); - assert!( - result.contains("helper.rs"), - "Should find matches in helper.rs" - ); - assert!( - !result.contains("test_main.rs"), - "Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)" - ); - - // Test with include pattern for src directory only - let input = serde_json::to_value(GrepToolInput { - regex: "fn".to_string(), - include_pattern: Some("root/**/src/**".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("main.rs"), - "Should find matches in src/main.rs" - ); - assert!( - result.contains("helper.rs"), - "Should find matches in src/utils/helper.rs" - ); - assert!( - !result.contains("test_main.rs"), - "Should not include test_main.rs as it's not in src directory" - ); - - // Test with empty include pattern (should default to all files) - let input = serde_json::to_value(GrepToolInput { - regex: "fn".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!(result.contains("main.rs"), "Should find matches in main.rs"); - assert!( - result.contains("helper.rs"), - "Should find matches in helper.rs" - ); - assert!( - result.contains("test_main.rs"), - "Should include test_main.rs" - ); - } - - #[gpui::test] - async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Test case-insensitive search (default) - let input = serde_json::to_value(GrepToolInput { - regex: "uppercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("UPPERCASE"), - "Case-insensitive search should match uppercase" - ); - - // Test case-sensitive search - let input = serde_json::to_value(GrepToolInput { - regex: "uppercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - !result.contains("UPPERCASE"), - "Case-sensitive search should not match uppercase" - ); - - // Test case-sensitive search - let input = serde_json::to_value(GrepToolInput { - regex: "LOWERCASE".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - - assert!( - !result.contains("lowercase"), - "Case-sensitive search should match lowercase" - ); - - // Test case-sensitive search for lowercase pattern - let input = serde_json::to_value(GrepToolInput { - regex: "lowercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("lowercase"), - "Case-sensitive search should match lowercase text" - ); - } - - /// Helper function to set up a syntax test environment - async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity { - use unindent::Unindent; - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - - // Create test file with syntax structures - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "test_syntax.rs": r#" - fn top_level_function() { - println!("This is at the top level"); - } - - mod feature_module { - pub mod nested_module { - pub fn nested_function( - first_arg: String, - second_arg: i32, - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - } - } - - struct MyStruct { - field1: String, - field2: i32, - } - - impl MyStruct { - fn method_with_block() { - let condition = true; - if condition { - println!("Inside if block"); - } - } - - fn long_function() { - println!("Line 1"); - println!("Line 2"); - println!("Line 3"); - println!("Line 4"); - println!("Line 5"); - println!("Line 6"); - println!("Line 7"); - println!("Line 8"); - println!("Line 9"); - println!("Line 10"); - println!("Line 11"); - println!("Line 12"); - } - } - - trait Processor { - fn process(&self, input: &str) -> String; - } - - impl Processor for MyStruct { - fn process(&self, input: &str) -> String { - format!("Processed: {}", input) - } - } - "#.unindent().trim(), - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - project.update(cx, |project, _cx| { - project.languages().add(rust_lang().into()) - }); - - project - } - - #[gpui::test] - async fn test_grep_top_level_function(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line at the top level of the file - let input = serde_json::to_value(GrepToolInput { - regex: "This is at the top level".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### fn top_level_function › L1-3 - ``` - fn top_level_function() { - println!("This is at the top level"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_function_body(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line inside a function body - let input = serde_json::to_value(GrepToolInput { - regex: "Function in nested module".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14 - ``` - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_function_args_and_body(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line with a function argument - let input = serde_json::to_value(GrepToolInput { - regex: "second_arg".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14 - ``` - pub fn nested_function( - first_arg: String, - second_arg: i32, - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_if_block(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line inside an if block - let input = serde_json::to_value(GrepToolInput { - regex: "Inside if block".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn method_with_block › L26-28 - ``` - if condition { - println!("Inside if block"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_long_function_top(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line in the middle of a long function - should show message about remaining lines - let input = serde_json::to_value(GrepToolInput { - regex: "Line 5".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn long_function › L31-41 - ``` - fn long_function() { - println!("Line 1"); - println!("Line 2"); - println!("Line 3"); - println!("Line 4"); - println!("Line 5"); - println!("Line 6"); - println!("Line 7"); - println!("Line 8"); - println!("Line 9"); - println!("Line 10"); - ``` - - 3 lines remaining in ancestor node. Read the file to see all. - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_long_function_bottom(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line in the long function - let input = serde_json::to_value(GrepToolInput { - regex: "Line 12".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }) - .unwrap(); - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn long_function › L41-45 - ``` - println!("Line 10"); - println!("Line 11"); - println!("Line 12"); - } - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - async fn run_grep_tool( - input: serde_json::Value, - project: Entity, - cx: &mut TestAppContext, - ) -> String { - let tool = Arc::new(GrepTool); - let action_log = cx.new(|_cx| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let task = - cx.update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx)); - - match task.output.await { - Ok(result) => { - if cfg!(windows) { - result.content.as_str().unwrap().replace("root\\", "root/") - } else { - result.content.as_str().unwrap().to_string() - } - } - Err(e) => panic!("Failed to run grep tool: {}", e), - } - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - } - - fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_outline_query(include_str!("../../languages/src/rust/outline.scm")) - .unwrap() - } - - #[gpui::test] - async fn test_grep_security_boundaries(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/"), - json!({ - "project_root": { - "allowed_file.rs": "fn main() { println!(\"This file is in the project\"); }", - ".mysecrets": "SECRET_KEY=abc123\nfn secret() { /* private */ }", - ".secretdir": { - "config": "fn special_configuration() { /* excluded */ }" - }, - ".mymetadata": "fn custom_metadata() { /* excluded */ }", - "subdir": { - "normal_file.rs": "fn normal_file_content() { /* Normal */ }", - "special.privatekey": "fn private_key_content() { /* private */ }", - "data.mysensitive": "fn sensitive_data() { /* private */ }" - } - }, - "outside_project": { - "sensitive_file.rs": "fn outside_function() { /* This file is outside the project */ }" - } - }), - ) - .await; - - cx.update(|cx| { - use gpui::UpdateGlobal; - use settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // Searching for files outside the project worktree should return no results - let result = cx - .update(|cx| { - let input = json!({ - "regex": "outside_function" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not find files outside the project worktree" - ); - - // Searching within the project should succeed - let result = cx - .update(|cx| { - let input = json!({ - "regex": "main" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.iter().any(|p| p.contains("allowed_file.rs")), - "grep_tool should be able to search files inside worktrees" - ); - - // Searching files that match file_scan_exclusions should return no results - let result = cx - .update(|cx| { - let input = json!({ - "regex": "special_configuration" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not search files in .secretdir (file_scan_exclusions)" - ); - - let result = cx - .update(|cx| { - let input = json!({ - "regex": "custom_metadata" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not search .mymetadata files (file_scan_exclusions)" - ); - - // Searching private files should return no results - let result = cx - .update(|cx| { - let input = json!({ - "regex": "SECRET_KEY" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not search .mysecrets (private_files)" - ); - - let result = cx - .update(|cx| { - let input = json!({ - "regex": "private_key_content" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not search .privatekey files (private_files)" - ); - - let result = cx - .update(|cx| { - let input = json!({ - "regex": "sensitive_data" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not search .mysensitive files (private_files)" - ); - - // Searching a normal file should still work, even with private_files configured - let result = cx - .update(|cx| { - let input = json!({ - "regex": "normal_file_content" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.iter().any(|p| p.contains("normal_file.rs")), - "Should be able to search normal files" - ); - - // Path traversal attempts with .. in include_pattern should not escape project - let result = cx - .update(|cx| { - let input = json!({ - "regex": "outside_function", - "include_pattern": "../outside_project/**/*.rs" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let results = result.unwrap(); - let paths = extract_paths_from_results(results.content.as_str().unwrap()); - assert!( - paths.is_empty(), - "grep_tool should not allow escaping project boundaries with relative paths" - ); - } - - #[gpui::test] - async fn test_grep_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private files - fs.insert_tree( - path!("/worktree1"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs"] - }"# - }, - "src": { - "main.rs": "fn main() { let secret_key = \"hidden\"; }", - "secret.rs": "const API_KEY: &str = \"secret_value\";", - "utils.rs": "pub fn get_config() -> String { \"config\".to_string() }" - }, - "tests": { - "test.rs": "fn test_secret() { assert!(true); }", - "fixture.sql": "SELECT * FROM secret_table;" - } - }), - ) - .await; - - // Create second worktree with different private files - fs.insert_tree( - path!("/worktree2"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - }, - "lib": { - "public.js": "export function getSecret() { return 'public'; }", - "private.js": "const SECRET_KEY = \"private_value\";", - "data.json": "{\"secret_data\": \"hidden\"}" - }, - "docs": { - "README.md": "# Documentation with secret info", - "internal.md": "Internal secret documentation" - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - // Wait for worktrees to be fully scanned - cx.executor().run_until_parked(); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // Search for "secret" - should exclude files based on worktree-specific settings - let result = cx - .update(|cx| { - let input = json!({ - "regex": "secret", - "case_sensitive": false - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - let paths = extract_paths_from_results(content); - - // Should find matches in non-private files - assert!( - paths.iter().any(|p| p.contains("main.rs")), - "Should find 'secret' in worktree1/src/main.rs" - ); - assert!( - paths.iter().any(|p| p.contains("test.rs")), - "Should find 'secret' in worktree1/tests/test.rs" - ); - assert!( - paths.iter().any(|p| p.contains("public.js")), - "Should find 'secret' in worktree2/lib/public.js" - ); - assert!( - paths.iter().any(|p| p.contains("README.md")), - "Should find 'secret' in worktree2/docs/README.md" - ); - - // Should NOT find matches in private/excluded files based on worktree settings - assert!( - !paths.iter().any(|p| p.contains("secret.rs")), - "Should not search in worktree1/src/secret.rs (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("fixture.sql")), - "Should not search in worktree1/tests/fixture.sql (local file_scan_exclusions)" - ); - assert!( - !paths.iter().any(|p| p.contains("private.js")), - "Should not search in worktree2/lib/private.js (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("data.json")), - "Should not search in worktree2/lib/data.json (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("internal.md")), - "Should not search in worktree2/docs/internal.md (local file_scan_exclusions)" - ); - - // Test with `include_pattern` specific to one worktree - let result = cx - .update(|cx| { - let input = json!({ - "regex": "secret", - "include_pattern": "worktree1/**/*.rs" - }); - Arc::new(GrepTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - let paths = extract_paths_from_results(content); - - // Should only find matches in worktree1 *.rs files (excluding private ones) - assert!( - paths.iter().any(|p| p.contains("main.rs")), - "Should find match in worktree1/src/main.rs" - ); - assert!( - paths.iter().any(|p| p.contains("test.rs")), - "Should find match in worktree1/tests/test.rs" - ); - assert!( - !paths.iter().any(|p| p.contains("secret.rs")), - "Should not find match in excluded worktree1/src/secret.rs" - ); - assert!( - paths.iter().all(|p| !p.contains("worktree2")), - "Should not find any matches in worktree2" - ); - } - - // Helper function to extract file paths from grep results - fn extract_paths_from_results(results: &str) -> Vec { - results - .lines() - .filter(|line| line.starts_with("## Matches in ")) - .map(|line| { - line.strip_prefix("## Matches in ") - .unwrap() - .trim() - .to_string() - }) - .collect() - } -} diff --git a/crates/assistant_tools/src/grep_tool/description.md b/crates/assistant_tools/src/grep_tool/description.md deleted file mode 100644 index e3c0b43f31da53df49ce905e764dedcc5ea530de..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/grep_tool/description.md +++ /dev/null @@ -1,9 +0,0 @@ -Searches the contents of files in the project with a regular expression - -- Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in. -- Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.) -- Pass an `include_pattern` if you know how to narrow your search on the files system -- Never use this tool to search for paths. Only search file contents with this tool. -- Use this tool when you need to find files containing specific patterns -- Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages. -- DO NOT use HTML entities solely to escape characters in the tool parameters. diff --git a/crates/assistant_tools/src/list_directory_tool.rs b/crates/assistant_tools/src/list_directory_tool.rs deleted file mode 100644 index 7d70f41a8c5000b433d47e8caa2a60d3a8024b99..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/list_directory_tool.rs +++ /dev/null @@ -1,869 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::{Project, ProjectPath, WorktreeSettings}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{fmt::Write, sync::Arc}; -use ui::IconName; -use util::markdown::MarkdownInlineCode; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ListDirectoryToolInput { - /// The fully-qualified path of the directory to list in the project. - /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - directory1 - /// - directory2 - /// - /// You can list the contents of `directory1` by using the path `directory1`. - /// - /// - /// - /// If the project has the following root directories: - /// - /// - foo - /// - bar - /// - /// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`. - /// - pub path: String, -} - -pub struct ListDirectoryTool; - -impl Tool for ListDirectoryTool { - fn name(&self) -> String { - "list_directory".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./list_directory_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolFolder - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let path = MarkdownInlineCode(&input.path); - format!("List the {path} directory's contents") - } - Err(_) => "List directory".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let path_style = project.read(cx).path_style(cx); - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - // Sometimes models will return these even though we tell it to give a path and not a glob. - // When this happens, just list the root worktree directories. - if matches!(input.path.as_str(), "." | "" | "./" | "*") { - let output = project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - worktree.read(cx).root_entry().and_then(|entry| { - if entry.is_dir() { - Some(entry.path.display(path_style)) - } else { - None - } - }) - }) - .collect::>() - .join("\n"); - - return Task::ready(Ok(output.into())).into(); - } - - let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { - return Task::ready(Err(anyhow!("Path {} not found in project", input.path))).into(); - }; - let Some(worktree) = project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("Worktree not found"))).into(); - }; - - // Check if the directory whose contents we're listing is itself excluded or private - let global_settings = WorktreeSettings::get_global(cx); - if global_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}", - &input.path - ))) - .into(); - } - - if global_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's global `private_files` setting: {}", - &input.path - ))) - .into(); - } - - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - if worktree_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}", - &input.path - ))) - .into(); - } - - if worktree_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's worktree `private_paths` setting: {}", - &input.path - ))) - .into(); - } - - let worktree_snapshot = worktree.read(cx).snapshot(); - - let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else { - return Task::ready(Err(anyhow!("Path not found: {}", input.path))).into(); - }; - - if !entry.is_dir() { - return Task::ready(Err(anyhow!("{} is not a directory.", input.path))).into(); - } - let worktree_snapshot = worktree.read(cx).snapshot(); - - let mut folders = Vec::new(); - let mut files = Vec::new(); - - for entry in worktree_snapshot.child_entries(&project_path.path) { - // Skip private and excluded files and directories - if global_settings.is_path_private(&entry.path) - || global_settings.is_path_excluded(&entry.path) - { - continue; - } - - let project_path = ProjectPath { - worktree_id: worktree_snapshot.id(), - path: entry.path.clone(), - }; - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - - if worktree_settings.is_path_excluded(&project_path.path) - || worktree_settings.is_path_private(&project_path.path) - { - continue; - } - - let full_path = worktree_snapshot - .root_name() - .join(&entry.path) - .display(worktree_snapshot.path_style()) - .to_string(); - if entry.is_dir() { - folders.push(full_path); - } else { - files.push(full_path); - } - } - - let mut output = String::new(); - - if !folders.is_empty() { - writeln!(output, "# Folders:\n{}", folders.join("\n")).unwrap(); - } - - if !files.is_empty() { - writeln!(output, "\n# Files:\n{}", files.join("\n")).unwrap(); - } - - if output.is_empty() { - writeln!(output, "{} is empty.", input.path).unwrap(); - } - - Task::ready(Ok(output.into())).into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use assistant_tool::Tool; - use gpui::{AppContext, TestAppContext, UpdateGlobal}; - use indoc::indoc; - use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - fn platform_paths(path_str: &str) -> String { - if cfg!(target_os = "windows") { - path_str.replace("/", "\\") - } else { - path_str.to_string() - } - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - } - - #[gpui::test] - async fn test_list_directory_separates_files_and_dirs(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "src": { - "main.rs": "fn main() {}", - "lib.rs": "pub fn hello() {}", - "models": { - "user.rs": "struct User {}", - "post.rs": "struct Post {}" - }, - "utils": { - "helper.rs": "pub fn help() {}" - } - }, - "tests": { - "integration_test.rs": "#[test] fn test() {}" - }, - "README.md": "# Project", - "Cargo.toml": "[package]" - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let tool = Arc::new(ListDirectoryTool); - - // Test listing root directory - let input = json!({ - "path": "project" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert_eq!( - content, - platform_paths(indoc! {" - # Folders: - project/src - project/tests - - # Files: - project/Cargo.toml - project/README.md - "}) - ); - - // Test listing src directory - let input = json!({ - "path": "project/src" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert_eq!( - content, - platform_paths(indoc! {" - # Folders: - project/src/models - project/src/utils - - # Files: - project/src/lib.rs - project/src/main.rs - "}) - ); - - // Test listing directory with only files - let input = json!({ - "path": "project/tests" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert!(!content.contains("# Folders:")); - assert!(content.contains("# Files:")); - assert!(content.contains(&platform_paths("project/tests/integration_test.rs"))); - } - - #[gpui::test] - async fn test_list_directory_empty_directory(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "empty_dir": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let tool = Arc::new(ListDirectoryTool); - - let input = json!({ - "path": "project/empty_dir" - }); - - let result = cx - .update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx)) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert_eq!(content, "project/empty_dir is empty.\n"); - } - - #[gpui::test] - async fn test_list_directory_error_cases(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "file.txt": "content" - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let tool = Arc::new(ListDirectoryTool); - - // Test non-existent path - let input = json!({ - "path": "project/nonexistent" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Path not found")); - - // Test trying to list a file instead of directory - let input = json!({ - "path": "project/file.txt" - }); - - let result = cx - .update(|cx| tool.run(input, Arc::default(), project, action_log, model, None, cx)) - .output - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("is not a directory") - ); - } - - #[gpui::test] - async fn test_list_directory_security(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "normal_dir": { - "file1.txt": "content", - "file2.txt": "content" - }, - ".mysecrets": "SECRET_KEY=abc123", - ".secretdir": { - "config": "special configuration", - "secret.txt": "secret content" - }, - ".mymetadata": "custom metadata", - "visible_dir": { - "normal.txt": "normal content", - "special.privatekey": "private key content", - "data.mysensitive": "sensitive data", - ".hidden_subdir": { - "hidden_file.txt": "hidden content" - } - } - }), - ) - .await; - - // Configure settings explicitly - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - "**/.hidden_subdir".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let tool = Arc::new(ListDirectoryTool); - - // Listing root directory should exclude private and excluded files - let input = json!({ - "path": "project" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - - // Should include normal directories - assert!(content.contains("normal_dir"), "Should list normal_dir"); - assert!(content.contains("visible_dir"), "Should list visible_dir"); - - // Should NOT include excluded or private files - assert!( - !content.contains(".secretdir"), - "Should not list .secretdir (file_scan_exclusions)" - ); - assert!( - !content.contains(".mymetadata"), - "Should not list .mymetadata (file_scan_exclusions)" - ); - assert!( - !content.contains(".mysecrets"), - "Should not list .mysecrets (private_files)" - ); - - // Trying to list an excluded directory should fail - let input = json!({ - "path": "project/.secretdir" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!( - result.is_err(), - "Should not be able to list excluded directory" - ); - assert!( - result - .unwrap_err() - .to_string() - .contains("file_scan_exclusions"), - "Error should mention file_scan_exclusions" - ); - - // Listing a directory should exclude private files within it - let input = json!({ - "path": "project/visible_dir" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - - // Should include normal files - assert!(content.contains("normal.txt"), "Should list normal.txt"); - - // Should NOT include private files - assert!( - !content.contains("privatekey"), - "Should not list .privatekey files (private_files)" - ); - assert!( - !content.contains("mysensitive"), - "Should not list .mysensitive files (private_files)" - ); - - // Should NOT include subdirectories that match exclusions - assert!( - !content.contains(".hidden_subdir"), - "Should not list .hidden_subdir (file_scan_exclusions)" - ); - } - - #[gpui::test] - async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private files - fs.insert_tree( - path!("/worktree1"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs", "**/config.toml"] - }"# - }, - "src": { - "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", - "secret.rs": "const API_KEY: &str = \"secret_key_1\";", - "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" - }, - "tests": { - "test.rs": "mod tests { fn test_it() {} }", - "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" - } - }), - ) - .await; - - // Create second worktree with different private files - fs.insert_tree( - path!("/worktree2"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - }, - "lib": { - "public.js": "export function greet() { return 'Hello from worktree2'; }", - "private.js": "const SECRET_TOKEN = \"private_token_2\";", - "data.json": "{\"api_key\": \"json_secret_key\"}" - }, - "docs": { - "README.md": "# Public Documentation", - "internal.md": "# Internal Secrets and Configuration" - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - // Wait for worktrees to be fully scanned - cx.executor().run_until_parked(); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let tool = Arc::new(ListDirectoryTool); - - // Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings - let input = json!({ - "path": "worktree1/src" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert!(content.contains("main.rs"), "Should list main.rs"); - assert!( - !content.contains("secret.rs"), - "Should not list secret.rs (local private_files)" - ); - assert!( - !content.contains("config.toml"), - "Should not list config.toml (local private_files)" - ); - - // Test listing worktree1/tests - should exclude fixture.sql based on local settings - let input = json!({ - "path": "worktree1/tests" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert!(content.contains("test.rs"), "Should list test.rs"); - assert!( - !content.contains("fixture.sql"), - "Should not list fixture.sql (local file_scan_exclusions)" - ); - - // Test listing worktree2/lib - should exclude private.js and data.json based on local settings - let input = json!({ - "path": "worktree2/lib" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert!(content.contains("public.js"), "Should list public.js"); - assert!( - !content.contains("private.js"), - "Should not list private.js (local private_files)" - ); - assert!( - !content.contains("data.json"), - "Should not list data.json (local private_files)" - ); - - // Test listing worktree2/docs - should exclude internal.md based on local settings - let input = json!({ - "path": "worktree2/docs" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - let content = result.content.as_str().unwrap(); - assert!(content.contains("README.md"), "Should list README.md"); - assert!( - !content.contains("internal.md"), - "Should not list internal.md (local file_scan_exclusions)" - ); - - // Test trying to list an excluded directory directly - let input = json!({ - "path": "worktree1/src/secret.rs" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - // This should fail because we're trying to list a file, not a directory - assert!(result.is_err(), "Should fail when trying to list a file"); - } -} diff --git a/crates/assistant_tools/src/list_directory_tool/description.md b/crates/assistant_tools/src/list_directory_tool/description.md deleted file mode 100644 index 30dcc012ff316c944a7495dc14457cfd9df93bb7..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/list_directory_tool/description.md +++ /dev/null @@ -1 +0,0 @@ -Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase. diff --git a/crates/assistant_tools/src/move_path_tool.rs b/crates/assistant_tools/src/move_path_tool.rs deleted file mode 100644 index 22dbe9e625468d8c2688b60bdcd94a7da594730e..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/move_path_tool.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{path::Path, sync::Arc}; -use ui::IconName; -use util::markdown::MarkdownInlineCode; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct MovePathToolInput { - /// The source path of the file or directory to move/rename. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can move the first file by providing a source_path of "directory1/a/something.txt" - /// - pub source_path: String, - - /// The destination path where the file or directory should be moved/renamed to. - /// If the paths are the same except for the filename, then this will be a rename. - /// - /// - /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt", - /// provide a destination_path of "directory2/b/renamed.txt" - /// - pub destination_path: String, -} - -pub struct MovePathTool; - -impl Tool for MovePathTool { - fn name(&self) -> String { - "move_path".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - true - } - - fn description(&self) -> String { - include_str!("./move_path_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ArrowRightLeft - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let src = MarkdownInlineCode(&input.source_path); - let dest = MarkdownInlineCode(&input.destination_path); - let src_path = Path::new(&input.source_path); - let dest_path = Path::new(&input.destination_path); - - match dest_path - .file_name() - .and_then(|os_str| os_str.to_os_string().into_string().ok()) - { - Some(filename) if src_path.parent() == dest_path.parent() => { - let filename = MarkdownInlineCode(&filename); - format!("Rename {src} to {filename}") - } - _ => { - format!("Move {src} to {dest}") - } - } - } - Err(_) => "Move path".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - let rename_task = project.update(cx, |project, cx| { - match project - .find_project_path(&input.source_path, cx) - .and_then(|project_path| project.entry_for_path(&project_path, cx)) - { - Some(entity) => match project.find_project_path(&input.destination_path, cx) { - Some(project_path) => project.rename_entry(entity.id, project_path, cx), - None => Task::ready(Err(anyhow!( - "Destination path {} was outside the project.", - input.destination_path - ))), - }, - None => Task::ready(Err(anyhow!( - "Source path {} was not found in the project.", - input.source_path - ))), - } - }); - - cx.background_spawn(async move { - let _ = rename_task.await.with_context(|| { - format!("Moving {} to {}", input.source_path, input.destination_path) - })?; - Ok(format!("Moved {} to {}", input.source_path, input.destination_path).into()) - }) - .into() - } -} diff --git a/crates/assistant_tools/src/move_path_tool/description.md b/crates/assistant_tools/src/move_path_tool/description.md deleted file mode 100644 index 76bc3003d003c44afdd9036cb6691d5fc432291d..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/move_path_tool/description.md +++ /dev/null @@ -1,5 +0,0 @@ -Moves or rename a file or directory in the project, and returns confirmation that the move succeeded. -If the source and destination directories are the same, but the filename is different, this performs -a rename. Otherwise, it performs a move. - -This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all. diff --git a/crates/assistant_tools/src/now_tool.rs b/crates/assistant_tools/src/now_tool.rs deleted file mode 100644 index f50ad065d1cd320aa1a82e4ce17f744d6b04be2c..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/now_tool.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::sync::Arc; - -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use chrono::{Local, Utc}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::IconName; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Timezone { - /// Use UTC for the datetime. - Utc, - /// Use local time for the datetime. - Local, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct NowToolInput { - /// The timezone to use for the datetime. - timezone: Timezone, -} - -pub struct NowTool; - -impl Tool for NowTool { - fn name(&self) -> String { - "now".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - "Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into() - } - - fn icon(&self) -> IconName { - IconName::Info - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - "Get current time".to_string() - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - _cx: &mut App, - ) -> ToolResult { - let input: NowToolInput = match serde_json::from_value(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let now = match input.timezone { - Timezone::Utc => Utc::now().to_rfc3339(), - Timezone::Local => Local::now().to_rfc3339(), - }; - let text = format!("The current datetime is {now}."); - - Task::ready(Ok(text.into())).into() - } -} diff --git a/crates/assistant_tools/src/open_tool.rs b/crates/assistant_tools/src/open_tool.rs deleted file mode 100644 index a1aafad041364b0ffca01cc1890c2cc10b3d7b01..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/open_tool.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::{AnyWindowHandle, App, AppContext, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, sync::Arc}; -use ui::IconName; -use util::markdown::MarkdownEscaped; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct OpenToolInput { - /// The path or URL to open with the default application. - path_or_url: String, -} - -pub struct OpenTool; - -impl Tool for OpenTool { - fn name(&self) -> String { - "open".to_string() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true - } - fn may_perform_edits(&self) -> bool { - false - } - fn description(&self) -> String { - include_str!("./open_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ArrowUpRight - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => format!("Open `{}`", MarkdownEscaped(&input.path_or_url)), - Err(_) => "Open file or URL".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input: OpenToolInput = match serde_json::from_value(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - // If path_or_url turns out to be a path in the project, make it absolute. - let abs_path = to_absolute_path(&input.path_or_url, project, cx); - - cx.background_spawn(async move { - match abs_path { - Some(path) => open::that(path), - None => open::that(&input.path_or_url), - } - .context("Failed to open URL or file path")?; - - Ok(format!("Successfully opened {}", input.path_or_url).into()) - }) - .into() - } -} - -fn to_absolute_path( - potential_path: &str, - project: Entity, - cx: &mut App, -) -> Option { - let project = project.read(cx); - project - .find_project_path(PathBuf::from(potential_path), cx) - .and_then(|project_path| project.absolute_path(&project_path, cx)) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use std::path::Path; - use tempfile::TempDir; - - #[gpui::test] - async fn test_to_absolute_path(cx: &mut TestAppContext) { - init_test(cx); - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let temp_path = temp_dir.path().to_string_lossy().into_owned(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - &temp_path, - serde_json::json!({ - "src": { - "main.rs": "fn main() {}", - "lib.rs": "pub fn lib_fn() {}" - }, - "docs": { - "readme.md": "# Project Documentation" - } - }), - ) - .await; - - // Use the temp_path as the root directory, not just its filename - let project = Project::test(fs.clone(), [temp_dir.path()], cx).await; - - // Test cases where the function should return Some - cx.update(|cx| { - // Project-relative paths should return Some - // Create paths using the last segment of the temp path to simulate a project-relative path - let root_dir_name = Path::new(&temp_path) - .file_name() - .unwrap_or_else(|| std::ffi::OsStr::new("temp")) - .to_string_lossy(); - - assert!( - to_absolute_path(&format!("{root_dir_name}/src/main.rs"), project.clone(), cx) - .is_some(), - "Failed to resolve main.rs path" - ); - - assert!( - to_absolute_path( - &format!("{root_dir_name}/docs/readme.md",), - project.clone(), - cx, - ) - .is_some(), - "Failed to resolve readme.md path" - ); - - // External URL should return None - let result = to_absolute_path("https://example.com", project.clone(), cx); - assert_eq!(result, None, "External URLs should return None"); - - // Path outside project - let result = to_absolute_path("../invalid/path", project.clone(), cx); - assert_eq!(result, None, "Paths outside the project should return None"); - }); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - } -} diff --git a/crates/assistant_tools/src/open_tool/description.md b/crates/assistant_tools/src/open_tool/description.md deleted file mode 100644 index 99ccbb0524473b8c740d6ecd2d9ca9555e1e7028..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/open_tool/description.md +++ /dev/null @@ -1,9 +0,0 @@ -This tool opens a file or URL with the default application associated with it on the user's operating system: -- On macOS, it's equivalent to the `open` command -- On Windows, it's equivalent to `start` -- On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate - -For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc. - -You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that -the user would like for you to use this tool. diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs deleted file mode 100644 index e30d80207dae4de1e69efe99724a2a5343b57664..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ /dev/null @@ -1,360 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::Result; -use assistant_tool::{Tool, ToolResult}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{fmt::Write, sync::Arc}; -use ui::IconName; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ProjectUpdatesToolInput {} - -pub struct ProjectNotificationsTool; - -impl Tool for ProjectNotificationsTool { - fn name(&self) -> String { - "project_notifications".to_string() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - fn may_perform_edits(&self) -> bool { - false - } - fn description(&self) -> String { - include_str!("./project_notifications_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ToolNotification - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - "Check project notifications".into() - } - - fn run( - self: Arc, - _input: serde_json::Value, - _request: Arc, - _project: Entity, - action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let Some(user_edits_diff) = - action_log.update(cx, |log, cx| log.flush_unnotified_user_edits(cx)) - else { - return result("No new notifications"); - }; - - // NOTE: Changes to this prompt require a symmetric update in the LLM Worker - const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt"); - const MAX_BYTES: usize = 8000; - let diff = fit_patch_to_size(&user_edits_diff, MAX_BYTES); - result(&format!("{HEADER}\n\n```diff\n{diff}\n```\n").replace("\r\n", "\n")) - } -} - -fn result(response: &str) -> ToolResult { - Task::ready(Ok(response.to_string().into())).into() -} - -/// Make sure that the patch fits into the size limit (in bytes). -/// Compress the patch by omitting some parts if needed. -/// Unified diff format is assumed. -fn fit_patch_to_size(patch: &str, max_size: usize) -> String { - if patch.len() <= max_size { - return patch.to_string(); - } - - // Compression level 1: remove context lines in diff bodies, but - // leave the counts and positions of inserted/deleted lines - let mut current_size = patch.len(); - let mut file_patches = split_patch(patch); - file_patches.sort_by_key(|patch| patch.len()); - let compressed_patches = file_patches - .iter() - .rev() - .map(|patch| { - if current_size > max_size { - let compressed = compress_patch(patch).unwrap_or_else(|_| patch.to_string()); - current_size -= patch.len() - compressed.len(); - compressed - } else { - patch.to_string() - } - }) - .collect::>(); - - if current_size <= max_size { - return compressed_patches.join("\n\n"); - } - - // Compression level 2: list paths of the changed files only - let filenames = file_patches - .iter() - .map(|patch| { - let patch = diffy::Patch::from_str(patch).unwrap(); - let path = patch - .modified() - .and_then(|path| path.strip_prefix("b/")) - .unwrap_or_default(); - format!("- {path}\n") - }) - .collect::>(); - - filenames.join("") -} - -/// Split a potentially multi-file patch into multiple single-file patches -fn split_patch(patch: &str) -> Vec { - let mut result = Vec::new(); - let mut current_patch = String::new(); - - for line in patch.lines() { - if line.starts_with("---") && !current_patch.is_empty() { - result.push(current_patch.trim_end_matches('\n').into()); - current_patch = String::new(); - } - current_patch.push_str(line); - current_patch.push('\n'); - } - - if !current_patch.is_empty() { - result.push(current_patch.trim_end_matches('\n').into()); - } - - result -} - -fn compress_patch(patch: &str) -> anyhow::Result { - let patch = diffy::Patch::from_str(patch)?; - let mut out = String::new(); - - writeln!(out, "--- {}", patch.original().unwrap_or("a"))?; - writeln!(out, "+++ {}", patch.modified().unwrap_or("b"))?; - - for hunk in patch.hunks() { - writeln!(out, "@@ -{} +{} @@", hunk.old_range(), hunk.new_range())?; - writeln!(out, "[...skipped...]")?; - } - - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - use assistant_tool::ToolResultContent; - use gpui::{AppContext, TestAppContext}; - use indoc::indoc; - use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider}; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use std::sync::Arc; - use util::path; - - #[gpui::test] - async fn test_stale_buffer_notification(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/test"), - json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}), - ) - .await; - - let project = Project::test(fs, [path!("/test").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let buffer_path = project - .read_with(cx, |project, cx| { - project.find_project_path("test/code.rs", cx) - }) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(buffer_path.clone(), cx) - }) - .await - .unwrap(); - - // Start tracking the buffer - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - }); - cx.run_until_parked(); - - // Run the tool before any changes - let tool = Arc::new(ProjectNotificationsTool); - let provider = Arc::new(FakeLanguageModelProvider::default()); - let model: Arc = Arc::new(provider.test_model()); - let request = Arc::new(LanguageModelRequest::default()); - let tool_input = json!({}); - - let result = cx.update(|cx| { - tool.clone().run( - tool_input.clone(), - request.clone(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }); - cx.run_until_parked(); - - let response = result.output.await.unwrap(); - let response_text = match &response.content { - ToolResultContent::Text(text) => text.clone(), - _ => panic!("Expected text response"), - }; - assert_eq!( - response_text.as_str(), - "No new notifications", - "Tool should return 'No new notifications' when no stale buffers" - ); - - // Modify the buffer (makes it stale) - buffer.update(cx, |buffer, cx| { - buffer.edit([(1..1, "\nChange!\n")], None, cx); - }); - cx.run_until_parked(); - - // Run the tool again - let result = cx.update(|cx| { - tool.clone().run( - tool_input.clone(), - request.clone(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }); - cx.run_until_parked(); - - // This time the buffer is stale, so the tool should return a notification - let response = result.output.await.unwrap(); - let response_text = match &response.content { - ToolResultContent::Text(text) => text.clone(), - _ => panic!("Expected text response"), - }; - - assert!( - response_text.contains("These files have changed"), - "Tool should return the stale buffer notification" - ); - assert!( - response_text.contains("test/code.rs"), - "Tool should return the stale buffer notification" - ); - - // Run the tool once more without any changes - should get no new notifications - let result = cx.update(|cx| { - tool.run( - tool_input.clone(), - request.clone(), - project.clone(), - action_log, - model.clone(), - None, - cx, - ) - }); - cx.run_until_parked(); - - let response = result.output.await.unwrap(); - let response_text = match &response.content { - ToolResultContent::Text(text) => text.clone(), - _ => panic!("Expected text response"), - }; - - assert_eq!( - response_text.as_str(), - "No new notifications", - "Tool should return 'No new notifications' when running again without changes" - ); - } - - #[test] - fn test_patch_compression() { - // Given a patch that doesn't fit into the size budget - let patch = indoc! {" - --- a/dir/test.txt - +++ b/dir/test.txt - @@ -1,3 +1,3 @@ - line 1 - -line 2 - +CHANGED - line 3 - @@ -10,2 +10,2 @@ - line 10 - -line 11 - +line eleven - - - --- a/dir/another.txt - +++ b/dir/another.txt - @@ -100,1 +1,1 @@ - -before - +after - "}; - - // When the size deficit can be compensated by dropping the body, - // then the body should be trimmed for larger files first - let limit = patch.len() - 10; - let compressed = fit_patch_to_size(patch, limit); - let expected = indoc! {" - --- a/dir/test.txt - +++ b/dir/test.txt - @@ -1,3 +1,3 @@ - [...skipped...] - @@ -10,2 +10,2 @@ - [...skipped...] - - - --- a/dir/another.txt - +++ b/dir/another.txt - @@ -100,1 +1,1 @@ - -before - +after"}; - assert_eq!(compressed, expected); - - // When the size deficit is too large, then only file paths - // should be returned - let limit = 10; - let compressed = fit_patch_to_size(patch, limit); - let expected = indoc! {" - - dir/another.txt - - dir/test.txt - "}; - assert_eq!(compressed, expected); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - assistant_tool::init(cx); - }); - } -} diff --git a/crates/assistant_tools/src/project_notifications_tool/description.md b/crates/assistant_tools/src/project_notifications_tool/description.md deleted file mode 100644 index 24ff678f5e7fd728b94ad4ebce06f2a1dcc6a658..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/project_notifications_tool/description.md +++ /dev/null @@ -1,3 +0,0 @@ -This tool reports which files have been modified by the user since the agent last accessed them. - -It serves as a notification mechanism to inform the agent of recent changes. No immediate action is required in response to these updates. diff --git a/crates/assistant_tools/src/project_notifications_tool/prompt_header.txt b/crates/assistant_tools/src/project_notifications_tool/prompt_header.txt deleted file mode 100644 index f743e239c883c7456f7bdc6e089185c6b994cb44..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/project_notifications_tool/prompt_header.txt +++ /dev/null @@ -1,3 +0,0 @@ -[The following is an auto-generated notification; do not reply] - -These files have changed since the last read: diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs deleted file mode 100644 index f9f68491e5846fa1ead09d6976d1f9a9bc99b501..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/read_file_tool.rs +++ /dev/null @@ -1,1190 +0,0 @@ -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use assistant_tool::{ToolResultContent, outline}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use project::{ImageItem, image_store}; - -use assistant_tool::ToolResultOutput; -use indoc::formatdoc; -use itertools::Itertools; -use language::{Anchor, Point}; -use language_model::{ - LanguageModel, LanguageModelImage, LanguageModelRequest, LanguageModelToolSchemaFormat, -}; -use project::{AgentLocation, Project, WorktreeSettings}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::sync::Arc; -use ui::IconName; - -/// If the model requests to read a file whose size exceeds this, then -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ReadFileToolInput { - /// The relative path of the file to read. - /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - /a/b/directory1 - /// - /c/d/directory2 - /// - /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. - /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. - /// - pub path: String, - - /// Optional line number to start reading on (1-based index) - #[serde(default)] - pub start_line: Option, - - /// Optional line number to end reading on (1-based index, inclusive) - #[serde(default)] - pub end_line: Option, -} - -pub struct ReadFileTool; - -impl Tool for ReadFileTool { - fn name(&self) -> String { - "read_file".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./read_file_tool/description.md").into() - } - - fn icon(&self) -> IconName { - IconName::ToolSearch - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let path = &input.path; - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!( - "[Read file `{}` (lines {}-{})](@selection:{}:({}-{}))", - path, start, end, path, start, end - ) - } - (Some(start), None) => { - format!( - "[Read file `{}` (from line {})](@selection:{}:({}-{}))", - path, start, path, start, start - ) - } - _ => format!("[Read file `{}`](@file:{})", path, path), - } - } - Err(_) => "Read file".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - action_log: Entity, - model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let Some(project_path) = project.read(cx).find_project_path(&input.path, cx) else { - return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))).into(); - }; - - // Error out if this path is either excluded or private in global settings - let global_settings = WorktreeSettings::get_global(cx); - if global_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the global `file_scan_exclusions` setting: {}", - &input.path - ))) - .into(); - } - - if global_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the global `private_files` setting: {}", - &input.path - ))) - .into(); - } - - // Error out if this path is either excluded or private in worktree settings - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - if worktree_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the worktree `file_scan_exclusions` setting: {}", - &input.path - ))) - .into(); - } - - if worktree_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the worktree `private_files` setting: {}", - &input.path - ))) - .into(); - } - - let file_path = input.path.clone(); - - if image_store::is_image_file(&project, &project_path, cx) { - if !model.supports_images() { - return Task::ready(Err(anyhow!( - "Attempted to read an image, but Zed doesn't currently support sending images to {}.", - model.name().0 - ))) - .into(); - } - - let task = cx.spawn(async move |cx| -> Result { - let image_entity: Entity = cx - .update(|cx| { - project.update(cx, |project, cx| { - project.open_image(project_path.clone(), cx) - }) - })? - .await?; - - let image = - image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?; - - let language_model_image = cx - .update(|cx| LanguageModelImage::from_image(image, cx))? - .await - .context("processing image")?; - - Ok(ToolResultOutput { - content: ToolResultContent::Image(language_model_image), - output: None, - }) - }); - - return task.into(); - } - - cx.spawn(async move |cx| { - let buffer = cx - .update(|cx| { - project.update(cx, |project, cx| project.open_buffer(project_path, cx)) - })? - .await?; - if buffer.read_with(cx, |buffer, _| { - buffer - .file() - .as_ref() - .is_none_or(|file| !file.disk_state().exists()) - })? { - anyhow::bail!("{file_path} not found"); - } - - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: Anchor::MIN, - }), - cx, - ); - })?; - - // Check if specific line ranges are provided - if input.start_line.is_some() || input.end_line.is_some() { - let mut anchor = None; - let result = buffer.read_with(cx, |buffer, _cx| { - let text = buffer.text(); - // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. - let start = input.start_line.unwrap_or(1).max(1); - let start_row = start - 1; - if start_row <= buffer.max_point().row { - let column = buffer.line_indent_for_row(start_row).raw_len(); - anchor = Some(buffer.anchor_before(Point::new(start_row, column))); - } - - let lines = text.split('\n').skip(start_row as usize); - if let Some(end) = input.end_line { - let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line - Itertools::intersperse(lines.take(count as usize), "\n") - .collect::() - .into() - } else { - Itertools::intersperse(lines, "\n") - .collect::() - .into() - } - })?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - })?; - - if let Some(anchor) = anchor { - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: anchor, - }), - cx, - ); - })?; - } - - Ok(result) - } else { - // No line ranges specified, so check file size to see if it's too big. - let buffer_content = - outline::get_buffer_content_or_outline(buffer.clone(), Some(&file_path), cx) - .await?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer, cx); - })?; - - if buffer_content.is_outline { - Ok(formatdoc! {" - This file was too big to read all at once. - - {} - - Using the line numbers in this outline, you can call this tool again - while specifying the start_line and end_line fields to see the - implementations of symbols in the outline. - - Alternatively, you can fall back to the `grep` tool (if available) - to search the file for specific content.", buffer_content.text - } - .into()) - } else { - Ok(buffer_content.text.into()) - } - } - }) - .into() - } -} - -#[cfg(test)] -mod test { - use super::*; - use gpui::{AppContext, TestAppContext, UpdateGlobal}; - use language::{Language, LanguageConfig, LanguageMatcher}; - use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_read_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/nonexistent_file.txt" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "root/nonexistent_file.txt not found" - ); - } - - #[gpui::test] - async fn test_read_small_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "small_file.txt": "This is a small file content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/small_file.txt" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - assert_eq!( - result.unwrap().content.as_str(), - Some("This is a small file content") - ); - } - - #[gpui::test] - async fn test_read_large_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::>().join("\n") - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(Arc::new(rust_lang())); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/large_file.rs" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - let content = result.unwrap(); - let content = content.as_str().unwrap(); - assert_eq!( - content.lines().skip(4).take(6).collect::>(), - vec![ - "struct Test0 [L1-4]", - " a [L2]", - " b [L3]", - "struct Test1 [L5-8]", - " a [L6]", - " b [L7]", - ] - ); - - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/large_file.rs", - "offset": 1 - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - let content = result.unwrap(); - let expected_content = (0..1000) - .flat_map(|i| { - vec![ - format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4), - format!(" a [L{}]", i * 4 + 2), - format!(" b [L{}]", i * 4 + 3), - ] - }) - .collect::>(); - pretty_assertions::assert_eq!( - content - .as_str() - .unwrap() - .lines() - .skip(4) - .take(expected_content.len()) - .collect::>(), - expected_content - ); - } - - #[gpui::test] - async fn test_read_file_with_line_range(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/multiline.txt", - "start_line": 2, - "end_line": 4 - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - assert_eq!( - result.unwrap().content.as_str(), - Some("Line 2\nLine 3\nLine 4") - ); - } - - #[gpui::test] - async fn test_read_file_line_range_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // start_line of 0 should be treated as 1 - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/multiline.txt", - "start_line": 0, - "end_line": 2 - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert_eq!(result.unwrap().content.as_str(), Some("Line 1\nLine 2")); - - // end_line of 0 should result in at least 1 line - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/multiline.txt", - "start_line": 1, - "end_line": 0 - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert_eq!(result.unwrap().content.as_str(), Some("Line 1")); - - // when start_line > end_line, should still return at least 1 line - let result = cx - .update(|cx| { - let input = json!({ - "path": "root/multiline.txt", - "start_line": 3, - "end_line": 2 - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log, - model, - None, - cx, - ) - .output - }) - .await; - assert_eq!(result.unwrap().content.as_str(), Some("Line 3")); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - } - - fn rust_lang() -> Language { - Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_outline_query( - r#" - (line_comment) @annotation - - (struct_item - "struct" @context - name: (_) @name) @item - (enum_item - "enum" @context - name: (_) @name) @item - (enum_variant - name: (_) @name) @item - (field_declaration - name: (_) @name) @item - (impl_item - "impl" @context - trait: (_)? @name - "for"? @context - type: (_) @name - body: (_ "{" (_)* "}")) @item - (function_item - "fn" @context - name: (_) @name) @item - (mod_item - "mod" @context - name: (_) @name) @item - "#, - ) - .unwrap() - } - - #[gpui::test] - async fn test_read_file_security(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/"), - json!({ - "project_root": { - "allowed_file.txt": "This file is in the project", - ".mysecrets": "SECRET_KEY=abc123", - ".secretdir": { - "config": "special configuration" - }, - ".mymetadata": "custom metadata", - "subdir": { - "normal_file.txt": "Normal file content", - "special.privatekey": "private key content", - "data.mysensitive": "sensitive data" - } - }, - "outside_project": { - "sensitive_file.txt": "This file is outside the project" - } - }), - ) - .await; - - cx.update(|cx| { - use gpui::UpdateGlobal; - use settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - - // Reading a file outside the project worktree should fail - let result = cx - .update(|cx| { - let input = json!({ - "path": "/outside_project/sensitive_file.txt" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read an absolute path outside a worktree" - ); - - // Reading a file within the project should succeed - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/allowed_file.txt" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_ok(), - "read_file_tool should be able to read files inside worktrees" - ); - - // Reading files that match file_scan_exclusions should fail - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/.secretdir/config" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read files in .secretdir (file_scan_exclusions)" - ); - - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/.mymetadata" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mymetadata files (file_scan_exclusions)" - ); - - // Reading private files should fail - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/.mysecrets" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mysecrets (private_files)" - ); - - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/subdir/special.privatekey" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .privatekey files (private_files)" - ); - - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/subdir/data.mysensitive" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mysensitive files (private_files)" - ); - - // Reading a normal file should still work, even with private_files configured - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/subdir/normal_file.txt" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!(result.is_ok(), "Should be able to read normal files"); - assert_eq!( - result.unwrap().content.as_str().unwrap(), - "Normal file content" - ); - - // Path traversal attempts with .. should fail - let result = cx - .update(|cx| { - let input = json!({ - "path": "project_root/../outside_project/sensitive_file.txt" - }); - Arc::new(ReadFileTool) - .run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - .output - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read a relative path that resolves to outside a worktree" - ); - } - - #[gpui::test] - async fn test_read_file_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private_files setting - fs.insert_tree( - path!("/worktree1"), - json!({ - "src": { - "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", - "secret.rs": "const API_KEY: &str = \"secret_key_1\";", - "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" - }, - "tests": { - "test.rs": "mod tests { fn test_it() {} }", - "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" - }, - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs", "**/config.toml"] - }"# - } - }), - ) - .await; - - // Create second worktree with different private_files setting - fs.insert_tree( - path!("/worktree2"), - json!({ - "lib": { - "public.js": "export function greet() { return 'Hello from worktree2'; }", - "private.js": "const SECRET_TOKEN = \"private_token_2\";", - "data.json": "{\"api_key\": \"json_secret_key\"}" - }, - "docs": { - "README.md": "# Public Documentation", - "internal.md": "# Internal Secrets and Configuration" - }, - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let tool = Arc::new(ReadFileTool); - - // Test reading allowed files in worktree1 - let input = json!({ - "path": "worktree1/src/main.rs" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - assert_eq!( - result.content.as_str().unwrap(), - "fn main() { println!(\"Hello from worktree1\"); }" - ); - - // Test reading private file in worktree1 should fail - let input = json!({ - "path": "worktree1/src/secret.rs" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Error should mention worktree private_files setting" - ); - - // Test reading excluded file in worktree1 should fail - let input = json!({ - "path": "worktree1/tests/fixture.sql" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `file_scan_exclusions` setting"), - "Error should mention worktree file_scan_exclusions setting" - ); - - // Test reading allowed files in worktree2 - let input = json!({ - "path": "worktree2/lib/public.js" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await - .unwrap(); - - assert_eq!( - result.content.as_str().unwrap(), - "export function greet() { return 'Hello from worktree2'; }" - ); - - // Test reading private file in worktree2 should fail - let input = json!({ - "path": "worktree2/lib/private.js" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Error should mention worktree private_files setting" - ); - - // Test reading excluded file in worktree2 should fail - let input = json!({ - "path": "worktree2/docs/internal.md" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `file_scan_exclusions` setting"), - "Error should mention worktree file_scan_exclusions setting" - ); - - // Test that files allowed in one worktree but not in another are handled correctly - // (e.g., config.toml is private in worktree1 but doesn't exist in worktree2) - let input = json!({ - "path": "worktree1/src/config.toml" - }); - - let result = cx - .update(|cx| { - tool.clone().run( - input, - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }) - .output - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Config.toml should be blocked by worktree1's private_files setting" - ); - } -} diff --git a/crates/assistant_tools/src/read_file_tool/description.md b/crates/assistant_tools/src/read_file_tool/description.md deleted file mode 100644 index 7bcebc03341541496ab090090ab7ef8beb3f2ebe..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/read_file_tool/description.md +++ /dev/null @@ -1,3 +0,0 @@ -Reads the content of the given file in the project. - -- Never attempt to read a path that hasn't been previously mentioned. diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs deleted file mode 100644 index dab7384efd8ba23669db645c87dcf79e95538d3a..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/schema.rs +++ /dev/null @@ -1,60 +0,0 @@ -use anyhow::Result; -use language_model::LanguageModelToolSchemaFormat; -use schemars::{ - JsonSchema, Schema, - generate::SchemaSettings, - transform::{Transform, transform_subschemas}, -}; - -pub fn json_schema_for( - format: LanguageModelToolSchemaFormat, -) -> Result { - let schema = root_schema_for::(format); - schema_to_json(&schema, format) -} - -fn schema_to_json( - schema: &Schema, - format: LanguageModelToolSchemaFormat, -) -> Result { - let mut value = serde_json::to_value(schema)?; - assistant_tool::adapt_schema_to_format(&mut value, format)?; - Ok(value) -} - -fn root_schema_for(format: LanguageModelToolSchemaFormat) -> Schema { - let mut generator = match format { - LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(), - LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::openapi3() - .with(|settings| { - settings.meta_schema = None; - settings.inline_subschemas = true; - }) - .with_transform(ToJsonSchemaSubsetTransform) - .into_generator(), - }; - generator.root_schema_for::() -} - -#[derive(Debug, Clone)] -struct ToJsonSchemaSubsetTransform; - -impl Transform for ToJsonSchemaSubsetTransform { - fn transform(&mut self, schema: &mut Schema) { - // Ensure that the type field is not an array, this happens when we use - // Option, the type will be [T, "null"]. - if let Some(type_field) = schema.get_mut("type") - && let Some(types) = type_field.as_array() - && let Some(first_type) = types.first() - { - *type_field = first_type.clone(); - } - - // oneOf is not supported, use anyOf instead - if let Some(one_of) = schema.remove("oneOf") { - schema.insert("anyOf".to_string(), one_of); - } - - transform_subschemas(self, schema); - } -} diff --git a/crates/assistant_tools/src/templates.rs b/crates/assistant_tools/src/templates.rs deleted file mode 100644 index c83601199cca11e7a92f07e4159ac6241378d725..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/templates.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; -use handlebars::Handlebars; -use rust_embed::RustEmbed; -use serde::Serialize; -use std::sync::Arc; - -#[derive(RustEmbed)] -#[folder = "src/templates"] -#[include = "*.hbs"] -struct Assets; - -pub struct Templates(Handlebars<'static>); - -impl Templates { - pub fn new() -> Arc { - let mut handlebars = Handlebars::new(); - handlebars.register_embed_templates::().unwrap(); - handlebars.register_escape_fn(|text| text.into()); - Arc::new(Self(handlebars)) - } -} - -pub trait Template: Sized { - const TEMPLATE_NAME: &'static str; - - fn render(&self, templates: &Templates) -> Result - where - Self: Serialize + Sized, - { - Ok(templates.0.render(Self::TEMPLATE_NAME, self)?) - } -} diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs deleted file mode 100644 index cab1498c0bfda186e3d52c7bce02b8f457d4fd85..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/terminal_tool.rs +++ /dev/null @@ -1,883 +0,0 @@ -use crate::{ - schema::json_schema_for, - ui::{COLLAPSED_LINES, ToolOutputPreview}, -}; -use action_log::ActionLog; -use agent_settings; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus}; -use futures::FutureExt as _; -use gpui::{ - AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement, - WeakEntity, Window, -}; -use language::LineEnding; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use portable_pty::{CommandBuilder, PtySize, native_pty_system}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation}; -use std::{ - env, - path::{Path, PathBuf}, - process::ExitStatus, - sync::Arc, - time::{Duration, Instant}, -}; -use task::{Shell, ShellBuilder}; -use terminal::terminal_settings::TerminalSettings; -use terminal_view::TerminalView; -use theme::ThemeSettings; -use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; -use util::{ - ResultExt, get_default_system_shell_preferring_bash, markdown::MarkdownInlineCode, - size::format_file_size, time::duration_alt_display, -}; -use workspace::Workspace; - -const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct TerminalToolInput { - /// The one-liner command to execute. - command: String, - /// Working directory for the command. This must be one of the root directories of the project. - cd: String, -} - -pub struct TerminalTool; - -impl TerminalTool { - pub const NAME: &str = "terminal"; -} - -impl Tool for TerminalTool { - fn name(&self) -> String { - Self::NAME.to_string() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - true - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./terminal_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ToolTerminal - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, input: &serde_json::Value) -> String { - match serde_json::from_value::(input.clone()) { - Ok(input) => { - let mut lines = input.command.lines(); - let first_line = lines.next().unwrap_or_default(); - let remaining_line_count = lines.count(); - match remaining_line_count { - 0 => MarkdownInlineCode(first_line).to_string(), - 1 => MarkdownInlineCode(&format!( - "{} - {} more line", - first_line, remaining_line_count - )) - .to_string(), - n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n)) - .to_string(), - } - } - Err(_) => "Run terminal command".to_string(), - } - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - project: Entity, - _action_log: Entity, - _model: Arc, - window: Option, - cx: &mut App, - ) -> ToolResult { - let input: TerminalToolInput = match serde_json::from_value(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - - let working_dir = match working_dir(&input, &project, cx) { - Ok(dir) => dir, - Err(err) => return Task::ready(Err(err)).into(), - }; - - let cwd = working_dir.clone(); - let env = match &cwd { - Some(dir) => project.update(cx, |project, cx| { - let worktree = project.find_worktree(dir.as_path(), cx); - let shell = TerminalSettings::get( - worktree.as_ref().map(|(worktree, path)| SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: &path, - }), - cx, - ) - .shell - .clone(); - project.directory_environment(&shell, dir.as_path().into(), cx) - }), - None => Task::ready(None).shared(), - }; - let is_windows = project.read(cx).path_style(cx).is_windows(); - let shell = project - .update(cx, |project, cx| { - project - .remote_client() - .and_then(|r| r.read(cx).default_system_shell()) - }) - .unwrap_or_else(|| get_default_system_shell_preferring_bash()); - - let env = cx.spawn(async move |_| { - let mut env = env.await.unwrap_or_default(); - if cfg!(unix) { - env.insert("PAGER".into(), "cat".into()); - } - env - }); - - let build_cmd = { - let input_command = input.command.clone(); - move || { - ShellBuilder::new(&Shell::Program(shell), is_windows) - .redirect_stdin_to_dev_null() - .build(Some(input_command), &[]) - } - }; - - let Some(window) = window else { - // Headless setup, a test or eval. Our terminal subsystem requires a workspace, - // so bypass it and provide a convincing imitation using a pty. - let task = cx.background_spawn(async move { - let env = env.await; - let pty_system = native_pty_system(); - let (command, args) = build_cmd(); - let mut cmd = CommandBuilder::new(command); - cmd.args(args); - for (k, v) in env { - cmd.env(k, v); - } - if let Some(cwd) = cwd { - cmd.cwd(cwd); - } - let pair = pty_system.openpty(PtySize { - rows: 24, - cols: 80, - ..Default::default() - })?; - let mut child = pair.slave.spawn_command(cmd)?; - let mut reader = pair.master.try_clone_reader()?; - drop(pair); - let mut content = String::new(); - reader.read_to_string(&mut content)?; - // Massage the pty output a bit to try to match what the terminal codepath gives us - LineEnding::normalize(&mut content); - content = content - .chars() - .filter(|c| c.is_ascii_whitespace() || !c.is_ascii_control()) - .collect(); - let content = content.trim_start().trim_start_matches("^D"); - let exit_status = child.wait()?; - let (processed_content, _) = - process_content(content, &input.command, Some(exit_status)); - Ok(processed_content.into()) - }); - return ToolResult { - output: task, - card: None, - }; - }; - - let terminal = cx.spawn({ - let project = project.downgrade(); - async move |cx| { - let (command, args) = build_cmd(); - let env = env.await; - project - .update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(command), - args, - cwd, - env, - ..Default::default() - }, - cx, - ) - })? - .await - } - }); - - let command_markdown = cx.new(|cx| { - Markdown::new( - format!("```bash\n{}\n```", input.command).into(), - None, - None, - cx, - ) - }); - - let card = - cx.new(|cx| TerminalToolCard::new(command_markdown, working_dir, cx.entity_id(), cx)); - - let output = cx.spawn({ - let card = card.clone(); - async move |cx| { - let terminal = terminal.await?; - let workspace = window - .downcast::() - .and_then(|handle| handle.entity(cx).ok()) - .context("no workspace entity in root of window")?; - - let terminal_view = window.update(cx, |_, window, cx| { - cx.new(|cx| { - let mut view = TerminalView::new( - terminal.clone(), - workspace.downgrade(), - None, - project.downgrade(), - window, - cx, - ); - view.set_embedded_mode(None, cx); - view - }) - })?; - - card.update(cx, |card, _| { - card.terminal = Some(terminal_view.clone()); - card.start_instant = Instant::now(); - }) - .log_err(); - - let exit_status = terminal - .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? - .await; - let (content, content_line_count) = terminal.read_with(cx, |terminal, _| { - (terminal.get_content(), terminal.total_lines()) - })?; - - let previous_len = content.len(); - let (processed_content, finished_with_empty_output) = process_content( - &content, - &input.command, - exit_status.map(portable_pty::ExitStatus::from), - ); - - card.update(cx, |card, _| { - card.command_finished = true; - card.exit_status = exit_status; - card.was_content_truncated = processed_content.len() < previous_len; - card.original_content_len = previous_len; - card.content_line_count = content_line_count; - card.finished_with_empty_output = finished_with_empty_output; - card.elapsed_time = Some(card.start_instant.elapsed()); - }) - .log_err(); - - Ok(processed_content.into()) - } - }); - - ToolResult { - output, - card: Some(card.into()), - } - } -} - -fn process_content( - content: &str, - command: &str, - exit_status: Option, -) -> (String, bool) { - let should_truncate = content.len() > COMMAND_OUTPUT_LIMIT; - - let content = if should_truncate { - let mut end_ix = COMMAND_OUTPUT_LIMIT.min(content.len()); - while !content.is_char_boundary(end_ix) { - end_ix -= 1; - } - // Don't truncate mid-line, clear the remainder of the last line - end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix); - &content[..end_ix] - } else { - content - }; - let content = content.trim(); - let is_empty = content.is_empty(); - let content = format!("```\n{content}\n```"); - let content = if should_truncate { - format!( - "Command output too long. The first {} bytes:\n\n{content}", - content.len(), - ) - } else { - content - }; - - let content = match exit_status { - Some(exit_status) if exit_status.success() => { - if is_empty { - "Command executed successfully.".to_string() - } else { - content - } - } - Some(exit_status) => { - if is_empty { - format!( - "Command \"{command}\" failed with exit code {}.", - exit_status.exit_code() - ) - } else { - format!( - "Command \"{command}\" failed with exit code {}.\n\n{content}", - exit_status.exit_code() - ) - } - } - None => { - format!( - "Command failed or was interrupted.\nPartial output captured:\n\n{}", - content, - ) - } - }; - (content, is_empty) -} - -fn working_dir( - input: &TerminalToolInput, - project: &Entity, - cx: &mut App, -) -> Result> { - let project = project.read(cx); - let cd = &input.cd; - - if cd == "." || cd.is_empty() { - // Accept "." or "" as meaning "the one worktree" if we only have one worktree. - let mut worktrees = project.worktrees(cx); - - match worktrees.next() { - Some(worktree) => { - anyhow::ensure!( - worktrees.next().is_none(), - "'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.", - ); - Ok(Some(worktree.read(cx).abs_path().to_path_buf())) - } - None => Ok(None), - } - } else { - let input_path = Path::new(cd); - - if input_path.is_absolute() { - // Absolute paths are allowed, but only if they're in one of the project's worktrees. - if project - .worktrees(cx) - .any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path())) - { - return Ok(Some(input_path.into())); - } - } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) { - return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); - } - - anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); - } -} - -struct TerminalToolCard { - input_command: Entity, - working_dir: Option, - entity_id: EntityId, - exit_status: Option, - terminal: Option>, - command_finished: bool, - was_content_truncated: bool, - finished_with_empty_output: bool, - content_line_count: usize, - original_content_len: usize, - preview_expanded: bool, - start_instant: Instant, - elapsed_time: Option, -} - -impl TerminalToolCard { - pub fn new( - input_command: Entity, - working_dir: Option, - entity_id: EntityId, - cx: &mut Context, - ) -> Self { - let expand_terminal_card = - agent_settings::AgentSettings::get_global(cx).expand_terminal_card; - Self { - input_command, - working_dir, - entity_id, - exit_status: None, - terminal: None, - command_finished: false, - was_content_truncated: false, - finished_with_empty_output: false, - original_content_len: 0, - content_line_count: 0, - preview_expanded: expand_terminal_card, - start_instant: Instant::now(), - elapsed_time: None, - } - } -} - -impl ToolCard for TerminalToolCard { - fn render( - &mut self, - status: &ToolUseStatus, - window: &mut Window, - _workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement { - let Some(terminal) = self.terminal.as_ref() else { - return Empty.into_any(); - }; - - let tool_failed = matches!(status, ToolUseStatus::Error(_)); - - let command_failed = - self.command_finished && self.exit_status.is_none_or(|code| !code.success()); - - if (tool_failed || command_failed) && self.elapsed_time.is_none() { - self.elapsed_time = Some(self.start_instant.elapsed()); - } - let time_elapsed = self - .elapsed_time - .unwrap_or_else(|| self.start_instant.elapsed()); - - let header_bg = cx - .theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)); - - let border_color = cx.theme().colors().border.opacity(0.6); - - let path = self - .working_dir - .as_ref() - .cloned() - .or_else(|| env::current_dir().ok()) - .map(|path| path.display().to_string()) - .unwrap_or_else(|| "current directory".to_string()); - - let header = h_flex() - .flex_none() - .gap_1() - .justify_between() - .rounded_t_md() - .child( - div() - .id(("command-target-path", self.entity_id)) - .w_full() - .max_w_full() - .overflow_x_scroll() - .child( - Label::new(path) - .buffer_font(cx) - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .when(!self.command_finished, |header| { - header.child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Info) - .with_rotate_animation(2), - ) - }) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", self.entity_id)) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when(command_failed && self.exit_status.is_some(), |this| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - self.exit_status - .and_then(|status| status.code()) - .unwrap_or(-1), - ))) - }) - .when( - !command_failed && tool_failed && status.error().is_some(), - |this| { - this.tooltip(Tooltip::text(format!( - "Error: {}", - status.error().unwrap(), - ))) - }, - ), - ) - }) - .when(self.was_content_truncated, |header| { - let tooltip = if self.content_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { - "Output exceeded terminal max lines and was \ - truncated, the model received the first 16 KB." - .to_string() - } else { - format!( - "Output is {} long, to avoid unexpected token usage, \ - only 16 KB was sent back to the model.", - format_file_size(self.original_content_len as u64, true), - ) - }; - header.child( - h_flex() - .id(("terminal-tool-truncated-label", self.entity_id)) - .tooltip(Tooltip::text(tooltip)) - .gap_1() - .child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Ignored), - ) - .child( - Label::new("Truncated") - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - }) - .when(time_elapsed > Duration::from_secs(10), |header| { - header.child( - Label::new(format!("({})", duration_alt_display(time_elapsed))) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }) - .when(!self.finished_with_empty_output, |header| { - header.child( - Disclosure::new( - ("terminal-tool-disclosure", self.entity_id), - self.preview_expanded, - ) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .on_click(cx.listener( - move |this, _event, _window, _cx| { - this.preview_expanded = !this.preview_expanded; - }, - )), - ) - }); - - v_flex() - .mb_2() - .border_1() - .when(tool_failed || command_failed, |card| card.border_dashed()) - .border_color(border_color) - .rounded_lg() - .overflow_hidden() - .child( - v_flex() - .p_2() - .gap_0p5() - .bg(header_bg) - .text_xs() - .child(header) - .child( - MarkdownElement::new( - self.input_command.clone(), - markdown_style(window, cx), - ) - .code_block_renderer( - markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: true, - border: false, - }, - ), - ), - ) - .when( - self.preview_expanded && !self.finished_with_empty_output, - |this| { - this.child( - div() - .pt_2() - .border_t_1() - .when(tool_failed || command_failed, |card| card.border_dashed()) - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .rounded_b_md() - .text_ui_sm(cx) - .child({ - let content_mode = terminal.read(cx).content_mode(window, cx); - - if content_mode.is_scrollable() { - div().h_72().child(terminal.clone()).into_any_element() - } else { - ToolOutputPreview::new( - terminal.clone().into_any_element(), - terminal.entity_id(), - ) - .with_total_lines(self.content_line_count) - .toggle_state(!content_mode.is_limited()) - .on_toggle({ - let terminal = terminal.clone(); - move |is_expanded, _, cx| { - terminal.update(cx, |terminal, cx| { - terminal.set_embedded_mode( - if is_expanded { - None - } else { - Some(COLLAPSED_LINES) - }, - cx, - ); - }); - } - }) - .into_any_element() - } - }), - ) - }, - ) - .into_any() - } -} - -fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); - let buffer_font_size = TextSize::Default.rems(cx); - let mut text_style = window.text_style(); - - text_style.refine(&TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - color: Some(cx.theme().colors().text), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style.clone(), - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -#[cfg(test)] -mod tests { - use editor::EditorSettings; - use fs::RealFs; - use gpui::{BackgroundExecutor, TestAppContext}; - use language_model::fake_provider::FakeLanguageModel; - use pretty_assertions::assert_eq; - use serde_json::json; - use settings::{Settings, SettingsStore}; - use terminal::terminal_settings::TerminalSettings; - use util::{ResultExt as _, test::TempTree}; - - use super::*; - - fn init_test(executor: &BackgroundExecutor, cx: &mut TestAppContext) { - zlog::init_test(); - - executor.allow_parking(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - workspace::init_settings(cx); - theme::init(theme::LoadThemes::JustBase, cx); - TerminalSettings::register(cx); - EditorSettings::register(cx); - }); - } - - #[gpui::test] - async fn test_interactive_command(executor: BackgroundExecutor, cx: &mut TestAppContext) { - if cfg!(windows) { - return; - } - init_test(&executor, cx); - - let fs = Arc::new(RealFs::new(None, executor)); - let tree = TempTree::new(json!({ - "project": {}, - })); - let project: Entity = - Project::test(fs, [tree.path().join("project").as_path()], cx).await; - let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone()))); - let model = Arc::new(FakeLanguageModel::default()); - - let input = TerminalToolInput { - command: "cat".to_owned(), - cd: tree - .path() - .join("project") - .as_path() - .to_string_lossy() - .to_string(), - }; - let result = cx.update(|cx| { - TerminalTool::run( - Arc::new(TerminalTool), - serde_json::to_value(input).unwrap(), - Arc::default(), - project.clone(), - action_log.clone(), - model, - None, - cx, - ) - }); - - let output = result.output.await.log_err().unwrap().content; - assert_eq!(output.as_str().unwrap(), "Command executed successfully."); - } - - #[gpui::test] - async fn test_working_directory(executor: BackgroundExecutor, cx: &mut TestAppContext) { - if cfg!(windows) { - return; - } - init_test(&executor, cx); - - let fs = Arc::new(RealFs::new(None, executor)); - let tree = TempTree::new(json!({ - "project": {}, - "other-project": {}, - })); - let project: Entity = - Project::test(fs, [tree.path().join("project").as_path()], cx).await; - let action_log = cx.update(|cx| cx.new(|_| ActionLog::new(project.clone()))); - let model = Arc::new(FakeLanguageModel::default()); - - let check = |input, expected, cx: &mut App| { - let headless_result = TerminalTool::run( - Arc::new(TerminalTool), - serde_json::to_value(input).unwrap(), - Arc::default(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ); - cx.spawn(async move |_| { - let output = headless_result.output.await.map(|output| output.content); - assert_eq!( - output - .ok() - .and_then(|content| content.as_str().map(ToString::to_string)), - expected - ); - }) - }; - - cx.update(|cx| { - check( - TerminalToolInput { - command: "pwd".into(), - cd: ".".into(), - }, - Some(format!( - "```\n{}\n```", - tree.path().join("project").display() - )), - cx, - ) - }) - .await; - - cx.update(|cx| { - check( - TerminalToolInput { - command: "pwd".into(), - cd: "other-project".into(), - }, - None, // other-project is a dir, but *not* a worktree (yet) - cx, - ) - }) - .await; - - // Absolute path above the worktree root - cx.update(|cx| { - check( - TerminalToolInput { - command: "pwd".into(), - cd: tree.path().to_string_lossy().into(), - }, - None, - cx, - ) - }) - .await; - - project - .update(cx, |project, cx| { - project.create_worktree(tree.path().join("other-project"), true, cx) - }) - .await - .unwrap(); - - cx.update(|cx| { - check( - TerminalToolInput { - command: "pwd".into(), - cd: "other-project".into(), - }, - Some(format!( - "```\n{}\n```", - tree.path().join("other-project").display() - )), - cx, - ) - }) - .await; - - cx.update(|cx| { - check( - TerminalToolInput { - command: "pwd".into(), - cd: ".".into(), - }, - None, - cx, - ) - }) - .await; - } -} diff --git a/crates/assistant_tools/src/terminal_tool/description.md b/crates/assistant_tools/src/terminal_tool/description.md deleted file mode 100644 index 3cb5d87d163b3919abafa899ed2fbdba67500773..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/terminal_tool/description.md +++ /dev/null @@ -1,11 +0,0 @@ -Executes a shell one-liner and returns the combined output. - -This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result. - -The output results will be shown to the user already, only list it again if necessary, avoid being redundant. - -Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error. - -Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own. - -Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations. diff --git a/crates/assistant_tools/src/thinking_tool.rs b/crates/assistant_tools/src/thinking_tool.rs deleted file mode 100644 index 17ce4afc2eeeff8c6f37834cd9e8c4ff71e7cd70..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/thinking_tool.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::sync::Arc; - -use crate::schema::json_schema_for; -use action_log::ActionLog; -use anyhow::{Result, anyhow}; -use assistant_tool::{Tool, ToolResult}; -use gpui::{AnyWindowHandle, App, Entity, Task}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::IconName; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ThinkingToolInput { - /// Content to think about. This should be a description of what to think about or - /// a problem to solve. - content: String, -} - -pub struct ThinkingTool; - -impl Tool for ThinkingTool { - fn name(&self) -> String { - "thinking".to_string() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - include_str!("./thinking_tool/description.md").to_string() - } - - fn icon(&self) -> IconName { - IconName::ToolThink - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - "Thinking".to_string() - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - _cx: &mut App, - ) -> ToolResult { - // This tool just "thinks out loud" and doesn't perform any actions. - Task::ready(match serde_json::from_value::(input) { - Ok(_input) => Ok("Finished thinking.".to_string().into()), - Err(err) => Err(anyhow!(err)), - }) - .into() - } -} diff --git a/crates/assistant_tools/src/thinking_tool/description.md b/crates/assistant_tools/src/thinking_tool/description.md deleted file mode 100644 index b625d22f321fa427945fdb9c42aaaed9ab86f6be..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/thinking_tool/description.md +++ /dev/null @@ -1 +0,0 @@ -A tool for thinking through problems, brainstorming ideas, or planning without executing any actions. Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. diff --git a/crates/assistant_tools/src/ui.rs b/crates/assistant_tools/src/ui.rs deleted file mode 100644 index 793427385456939eb1a7070fff5bba928a6c2643..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/ui.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod tool_call_card_header; -mod tool_output_preview; - -pub use tool_call_card_header::*; -pub use tool_output_preview::*; diff --git a/crates/assistant_tools/src/ui/tool_call_card_header.rs b/crates/assistant_tools/src/ui/tool_call_card_header.rs deleted file mode 100644 index b41f19432f99685cf745f684228169b53939fffb..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/ui/tool_call_card_header.rs +++ /dev/null @@ -1,131 +0,0 @@ -use gpui::{Animation, AnimationExt, AnyElement, App, IntoElement, pulsating_between}; -use std::time::Duration; -use ui::{Tooltip, prelude::*}; - -/// A reusable header component for tool call cards. -#[derive(IntoElement)] -pub struct ToolCallCardHeader { - icon: IconName, - primary_text: SharedString, - secondary_text: Option, - code_path: Option, - disclosure_slot: Option, - is_loading: bool, - error: Option, -} - -impl ToolCallCardHeader { - pub fn new(icon: IconName, primary_text: impl Into) -> Self { - Self { - icon, - primary_text: primary_text.into(), - secondary_text: None, - code_path: None, - disclosure_slot: None, - is_loading: false, - error: None, - } - } - - pub fn with_secondary_text(mut self, text: impl Into) -> Self { - self.secondary_text = Some(text.into()); - self - } - - pub fn with_code_path(mut self, text: impl Into) -> Self { - self.code_path = Some(text.into()); - self - } - - pub fn disclosure_slot(mut self, element: impl IntoElement) -> Self { - self.disclosure_slot = Some(element.into_any_element()); - self - } - - pub fn loading(mut self) -> Self { - self.is_loading = true; - self - } - - pub fn with_error(mut self, error: impl Into) -> Self { - self.error = Some(error.into()); - self - } -} - -impl RenderOnce for ToolCallCardHeader { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let font_size = rems(0.8125); - let line_height = window.line_height(); - - let secondary_text = self.secondary_text; - let code_path = self.code_path; - - let bullet_divider = || { - div() - .size(px(3.)) - .rounded_full() - .bg(cx.theme().colors().text) - }; - - h_flex() - .id("tool-label-container") - .gap_2() - .max_w_full() - .overflow_x_scroll() - .opacity(0.8) - .child( - h_flex() - .h(line_height) - .gap_1p5() - .text_size(font_size) - .child( - h_flex().h(line_height).justify_center().child( - Icon::new(self.icon) - .size(IconSize::Small) - .color(Color::Muted), - ), - ) - .map(|this| { - if let Some(error) = &self.error { - this.child(format!("{} failed", self.primary_text)).child( - IconButton::new("error_info", IconName::Warning) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Warning) - .tooltip(Tooltip::text(error.clone())), - ) - } else { - this.child(self.primary_text.clone()) - } - }) - .when_some(secondary_text, |this, secondary_text| { - this.child(bullet_divider()) - .child(div().text_size(font_size).child(secondary_text)) - }) - .when_some(code_path, |this, code_path| { - this.child(bullet_divider()) - .child(Label::new(code_path).size(LabelSize::Small).inline_code(cx)) - }) - .with_animation( - "loading-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.6, 1.)), - move |this, delta| { - if self.is_loading { - this.opacity(delta) - } else { - this - } - }, - ), - ) - .when_some(self.disclosure_slot, |container, disclosure_slot| { - container - .group("disclosure") - .justify_between() - .child(div().visible_on_hover("disclosure").child(disclosure_slot)) - }) - } -} diff --git a/crates/assistant_tools/src/ui/tool_output_preview.rs b/crates/assistant_tools/src/ui/tool_output_preview.rs deleted file mode 100644 index a672bb8b99daa1fd776f59c4e8be789b8e25240c..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/ui/tool_output_preview.rs +++ /dev/null @@ -1,115 +0,0 @@ -use gpui::{AnyElement, EntityId, prelude::*}; -use ui::{Tooltip, prelude::*}; - -#[derive(IntoElement)] -pub struct ToolOutputPreview -where - F: Fn(bool, &mut Window, &mut App) + 'static, -{ - content: AnyElement, - entity_id: EntityId, - full_height: bool, - total_lines: usize, - collapsed_fade: bool, - on_toggle: Option, -} - -pub const COLLAPSED_LINES: usize = 10; - -impl ToolOutputPreview -where - F: Fn(bool, &mut Window, &mut App) + 'static, -{ - pub fn new(content: AnyElement, entity_id: EntityId) -> Self { - Self { - content, - entity_id, - full_height: true, - total_lines: 0, - collapsed_fade: false, - on_toggle: None, - } - } - - pub fn with_total_lines(mut self, total_lines: usize) -> Self { - self.total_lines = total_lines; - self - } - - pub fn toggle_state(mut self, full_height: bool) -> Self { - self.full_height = full_height; - self - } - - pub fn with_collapsed_fade(mut self) -> Self { - self.collapsed_fade = true; - self - } - - pub fn on_toggle(mut self, listener: F) -> Self { - self.on_toggle = Some(listener); - self - } -} - -impl RenderOnce for ToolOutputPreview -where - F: Fn(bool, &mut Window, &mut App) + 'static, -{ - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - if self.total_lines <= COLLAPSED_LINES { - return self.content; - } - let border_color = cx.theme().colors().border.opacity(0.6); - - let (icon, tooltip_label) = if self.full_height { - (IconName::ChevronUp, "Collapse") - } else { - (IconName::ChevronDown, "Expand") - }; - - let gradient_overlay = - if self.collapsed_fade && !self.full_height { - Some(div().absolute().bottom_5().left_0().w_full().h_2_5().bg( - gpui::linear_gradient( - 0., - gpui::linear_color_stop(cx.theme().colors().editor_background, 0.), - gpui::linear_color_stop( - cx.theme().colors().editor_background.opacity(0.), - 1., - ), - ), - )) - } else { - None - }; - - v_flex() - .relative() - .child(self.content) - .children(gradient_overlay) - .child( - h_flex() - .id(("expand-button", self.entity_id)) - .flex_none() - .cursor_pointer() - .h_5() - .justify_center() - .border_t_1() - .rounded_b_md() - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1))) - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - .tooltip(Tooltip::text(tooltip_label)) - .when_some(self.on_toggle, |this, on_toggle| { - this.on_click({ - move |_, window, cx| { - on_toggle(!self.full_height, window, cx); - } - }) - }), - ) - .into_any() - } -} diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs deleted file mode 100644 index dbcca0a1f6f2d5f679fd240a5bfe64c6c9705256..0000000000000000000000000000000000000000 --- a/crates/assistant_tools/src/web_search_tool.rs +++ /dev/null @@ -1,327 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use crate::schema::json_schema_for; -use crate::ui::ToolCallCardHeader; -use action_log::ActionLog; -use anyhow::{Context as _, Result, anyhow}; -use assistant_tool::{ - Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus, -}; -use cloud_llm_client::{WebSearchResponse, WebSearchResult}; -use futures::{Future, FutureExt, TryFutureExt}; -use gpui::{ - AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window, -}; -use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::{IconName, Tooltip, prelude::*}; -use web_search::WebSearchRegistry; -use workspace::Workspace; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct WebSearchToolInput { - /// The search term or question to query on the web. - query: String, -} - -pub struct WebSearchTool; - -impl Tool for WebSearchTool { - fn name(&self) -> String { - "web_search".into() - } - - fn needs_confirmation(&self, _: &serde_json::Value, _: &Entity, _: &App) -> bool { - false - } - - fn may_perform_edits(&self) -> bool { - false - } - - fn description(&self) -> String { - "Search the web for information using your query. Use this when you need real-time information, facts, or data that might not be in your training. Results will include snippets and links from relevant web pages.".into() - } - - fn icon(&self) -> IconName { - IconName::ToolWeb - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - json_schema_for::(format) - } - - fn ui_text(&self, _input: &serde_json::Value) -> String { - "Searching the Web".to_string() - } - - fn run( - self: Arc, - input: serde_json::Value, - _request: Arc, - _project: Entity, - _action_log: Entity, - _model: Arc, - _window: Option, - cx: &mut App, - ) -> ToolResult { - let input = match serde_json::from_value::(input) { - Ok(input) => input, - Err(err) => return Task::ready(Err(anyhow!(err))).into(), - }; - let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else { - return Task::ready(Err(anyhow!("Web search is not available."))).into(); - }; - - let search_task = provider.search(input.query, cx).map_err(Arc::new).shared(); - let output = cx.background_spawn({ - let search_task = search_task.clone(); - async move { - let response = search_task.await.map_err(|err| anyhow!(err))?; - Ok(ToolResultOutput { - content: ToolResultContent::Text( - serde_json::to_string(&response) - .context("Failed to serialize search results")?, - ), - output: Some(serde_json::to_value(response)?), - }) - } - }); - - ToolResult { - output, - card: Some(cx.new(|cx| WebSearchToolCard::new(search_task, cx)).into()), - } - } - - fn deserialize_card( - self: Arc, - output: serde_json::Value, - _project: Entity, - _window: &mut Window, - cx: &mut App, - ) -> Option { - let output = serde_json::from_value::(output).ok()?; - let card = cx.new(|cx| WebSearchToolCard::new(Task::ready(Ok(output)), cx)); - Some(card.into()) - } -} - -#[derive(RegisterComponent)] -struct WebSearchToolCard { - response: Option>, - _task: Task<()>, -} - -impl WebSearchToolCard { - fn new( - search_task: impl 'static + Future>>, - cx: &mut Context, - ) -> Self { - let _task = cx.spawn(async move |this, cx| { - let response = search_task.await.map_err(|err| anyhow!(err)); - this.update(cx, |this, cx| { - this.response = Some(response); - cx.notify(); - }) - .ok(); - }); - - Self { - response: None, - _task, - } - } -} - -impl ToolCard for WebSearchToolCard { - fn render( - &mut self, - _status: &ToolUseStatus, - _window: &mut Window, - _workspace: WeakEntity, - cx: &mut Context, - ) -> impl IntoElement { - let icon = IconName::ToolWeb; - - let header = match self.response.as_ref() { - Some(Ok(response)) => { - let text: SharedString = if response.results.len() == 1 { - "1 result".into() - } else { - format!("{} results", response.results.len()).into() - }; - ToolCallCardHeader::new(icon, "Searched the Web").with_secondary_text(text) - } - Some(Err(error)) => { - ToolCallCardHeader::new(icon, "Web Search").with_error(error.to_string()) - } - None => ToolCallCardHeader::new(icon, "Searching the Web").loading(), - }; - - let content = self.response.as_ref().and_then(|response| match response { - Ok(response) => Some( - v_flex() - .overflow_hidden() - .ml_1p5() - .pl(px(5.)) - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .gap_1() - .children(response.results.iter().enumerate().map(|(index, result)| { - let title = result.title.clone(); - let url = SharedString::from(result.url.clone()); - - Button::new(("result", index), title) - .label_size(LabelSize::Small) - .color(Color::Muted) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End) - .truncate(true) - .tooltip({ - let url = url.clone(); - move |window, cx| { - Tooltip::with_meta( - "Web Search Result", - None, - url.clone(), - window, - cx, - ) - } - }) - .on_click(move |_, _, cx| cx.open_url(&url)) - })) - .into_any(), - ), - Err(_) => None, - }); - - v_flex().mb_3().gap_1().child(header).children(content) - } -} - -impl Component for WebSearchToolCard { - fn scope() -> ComponentScope { - ComponentScope::Agent - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let in_progress_search = cx.new(|cx| WebSearchToolCard { - response: None, - _task: cx.spawn(async move |_this, cx| { - loop { - cx.background_executor() - .timer(Duration::from_secs(60)) - .await - } - }), - }); - - let successful_search = cx.new(|_cx| WebSearchToolCard { - response: Some(Ok(example_search_response())), - _task: Task::ready(()), - }); - - let error_search = cx.new(|_cx| WebSearchToolCard { - response: Some(Err(anyhow!("Failed to resolve https://google.com"))), - _task: Task::ready(()), - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group(vec![ - single_example( - "In Progress", - div() - .size_full() - .child(in_progress_search.update(cx, |tool, cx| { - tool.render( - &ToolUseStatus::Pending, - window, - WeakEntity::new_invalid(), - cx, - ) - .into_any_element() - })) - .into_any_element(), - ), - single_example( - "Successful", - div() - .size_full() - .child(successful_search.update(cx, |tool, cx| { - tool.render( - &ToolUseStatus::Finished("".into()), - window, - WeakEntity::new_invalid(), - cx, - ) - .into_any_element() - })) - .into_any_element(), - ), - single_example( - "Error", - div() - .size_full() - .child(error_search.update(cx, |tool, cx| { - tool.render( - &ToolUseStatus::Error("".into()), - window, - WeakEntity::new_invalid(), - cx, - ) - .into_any_element() - })) - .into_any_element(), - ), - ])]) - .into_any_element(), - ) - } -} - -fn example_search_response() -> WebSearchResponse { - WebSearchResponse { - results: vec![ - WebSearchResult { - title: "Alo".to_string(), - url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(), - text: "Alo is a popular restaurant in Toronto.".to_string(), - }, - WebSearchResult { - title: "Alo".to_string(), - url: "https://www.google.com/maps/search/Alo%2C+Toronto%2C+Canada".to_string(), - text: "Information about Alo restaurant in Toronto.".to_string(), - }, - WebSearchResult { - title: "Edulis".to_string(), - url: "https://www.google.com/maps/search/Edulis%2C+Toronto%2C+Canada".to_string(), - text: "Details about Edulis restaurant in Toronto.".to_string(), - }, - WebSearchResult { - title: "Sushi Masaki Saito".to_string(), - url: "https://www.google.com/maps/search/Sushi+Masaki+Saito%2C+Toronto%2C+Canada" - .to_string(), - text: "Information about Sushi Masaki Saito in Toronto.".to_string(), - }, - WebSearchResult { - title: "Shoushin".to_string(), - url: "https://www.google.com/maps/search/Shoushin%2C+Toronto%2C+Canada".to_string(), - text: "Details about Shoushin restaurant in Toronto.".to_string(), - }, - WebSearchResult { - title: "Restaurant 20 Victoria".to_string(), - url: - "https://www.google.com/maps/search/Restaurant+20+Victoria%2C+Toronto%2C+Canada" - .to_string(), - text: "Information about Restaurant 20 Victoria in Toronto.".to_string(), - }, - ], - } -} diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index a0214c76a1c7230e071cbc65c1eadbc44c7d6ca8..42dd07b8c610746850923cb9eb96fc900e5206db 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -18,12 +18,12 @@ name = "explorer" path = "src/explorer.rs" [dependencies] +acp_thread.workspace = true agent.workspace = true +agent-client-protocol.workspace = true agent_settings.workspace = true agent_ui.workspace = true anyhow.workspace = true -assistant_tool.workspace = true -assistant_tools.workspace = true async-trait.workspace = true buffer_diff.workspace = true chrono.workspace = true diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 40d8c14f4f7ddc441f31581951ee4d6c26376a04..3afcc32a930ab32746352e81577d55a25c807cb4 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -429,7 +429,6 @@ pub fn init(cx: &mut App) -> Arc { true, cx, ); - assistant_tools::init(client.http_client(), cx); SettingsStore::update_global(cx, |store, cx| { store.set_user_settings(include_str!("../runner_settings.json"), cx) diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index c0f0900a6cfa5dd942bd27eed852ee4a52896c2c..22a8f9484c9f2c1d4ad01a107841b57e8b96f67b 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -9,7 +9,9 @@ use crate::{ ToolMetrics, assertions::{AssertionsReport, RanAssertion, RanAssertionResult}, }; -use agent::{ContextLoadResult, Thread, ThreadEvent}; +use acp_thread::UserMessageId; +use agent::{Thread, ThreadEvent, UserMessageContent}; +use agent_client_protocol as acp; use agent_settings::AgentProfileId; use anyhow::{Result, anyhow}; use async_trait::async_trait; diff --git a/crates/eval/src/examples/comment_translation.rs b/crates/eval/src/examples/comment_translation.rs index b6c9f7376f05fdc38e9f8128c78eb1761bc59c37..893166f3f13207e3444cb03bb17b2dea650170e7 100644 --- a/crates/eval/src/examples/comment_translation.rs +++ b/crates/eval/src/examples/comment_translation.rs @@ -1,7 +1,7 @@ use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion}; +use agent::{EditFileMode, EditFileToolInput}; use agent_settings::AgentProfileId; use anyhow::Result; -use assistant_tools::{EditFileMode, EditFileToolInput}; use async_trait::async_trait; pub struct CommentTranslation; diff --git a/crates/eval/src/examples/file_search.rs b/crates/eval/src/examples/file_search.rs index f1a482a41a952e889b6053e90e9e243ed546d2db..c893aef14299a6086e8c50072d69b0cbed7e9fde 100644 --- a/crates/eval/src/examples/file_search.rs +++ b/crates/eval/src/examples/file_search.rs @@ -1,6 +1,6 @@ +use agent::FindPathToolInput; use agent_settings::AgentProfileId; use anyhow::Result; -use assistant_tools::FindPathToolInput; use async_trait::async_trait; use regex::Regex; diff --git a/crates/eval/src/examples/grep_params_escapement.rs b/crates/eval/src/examples/grep_params_escapement.rs index 0532698ba28b45bd8111767eb51ea1336e18fa13..face6451572725ed402f23aac7bdc2c70a670b67 100644 --- a/crates/eval/src/examples/grep_params_escapement.rs +++ b/crates/eval/src/examples/grep_params_escapement.rs @@ -1,6 +1,5 @@ use agent_settings::AgentProfileId; use anyhow::Result; -use assistant_tools::GrepToolInput; use async_trait::async_trait; use crate::example::{Example, ExampleContext, ExampleMetadata}; diff --git a/crates/eval/src/examples/overwrite_file.rs b/crates/eval/src/examples/overwrite_file.rs index df0b75294c31bf7ff365e96aea18c371b817e710..d4b73aaec4d7d9a18be411ba7d453db9ffcb18a1 100644 --- a/crates/eval/src/examples/overwrite_file.rs +++ b/crates/eval/src/examples/overwrite_file.rs @@ -1,6 +1,5 @@ use agent_settings::AgentProfileId; use anyhow::Result; -use assistant_tools::{EditFileMode, EditFileToolInput}; use async_trait::async_trait; use crate::example::{Example, ExampleContext, ExampleMetadata}; diff --git a/crates/eval/src/examples/planets.rs b/crates/eval/src/examples/planets.rs index f3a69332d2c544479ca4f367699dc3def4d83370..caa15c728400a82b4223fb9ea8522b0815b36b5a 100644 --- a/crates/eval/src/examples/planets.rs +++ b/crates/eval/src/examples/planets.rs @@ -1,7 +1,6 @@ +use agent::{AgentTool, OpenTool, TerminalTool}; use agent_settings::AgentProfileId; use anyhow::Result; -use assistant_tool::Tool; -use assistant_tools::{OpenTool, TerminalTool}; use async_trait::async_trait; use crate::example::{Example, ExampleContext, ExampleMetadata, JudgeAssertion}; @@ -38,9 +37,9 @@ impl Example for Planets { let mut terminal_tool_uses = 0; for tool_use in response.tool_uses() { - if tool_use.name == OpenTool.name() { + if tool_use.name == OpenTool::name() { open_tool_uses += 1; - } else if tool_use.name == TerminalTool::NAME { + } else if tool_use.name == TerminalTool::name() { terminal_tool_uses += 1; } } diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index 208147e2f04b26a7337c071d36f4f687ca0fe184..e95264c3c3b726244abe4edb61dee474d3bff51a 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -1,6 +1,5 @@ -use agent::{Message, MessageSegment, SerializedThread, ThreadStore}; +use agent::Message; use anyhow::{Context as _, Result, anyhow, bail}; -use assistant_tool::ToolWorkingSet; use client::proto::LspWorkProgress; use futures::channel::mpsc; use futures::{FutureExt as _, StreamExt as _, future}; diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index a85283cf121bc10a82e1022071d6a136dd5716f5..2f0fe67034875ebe8c240093b87d66f44e247c2b 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -32,7 +32,6 @@ image.workspace = true log.workspace = true parking_lot.workspace = true proto.workspace = true -schemars.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 38f2b0959072599900cb8a13c16f4e2f8e9c55db..24f9b84afcfa7b9a40b4a1b7684e9a9b036a5a85 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -19,8 +19,7 @@ use http_client::{StatusCode, http}; use icons::IconName; use open_router::OpenRouterError; use parking_lot::Mutex; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde::{Deserialize, Serialize}; pub use settings::LanguageModelCacheConfiguration; use std::ops::{Add, Sub}; use std::str::FromStr; @@ -669,11 +668,6 @@ pub trait LanguageModelExt: LanguageModel { } impl LanguageModelExt for dyn LanguageModel {} -pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema { - fn name() -> String; - fn description() -> String; -} - /// An error that occurred when trying to authenticate the language model provider. #[derive(Debug, Error)] pub enum AuthenticateError { diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 37c77299ef4657ab62324fdef93f71e95ef026d1..3d28f6ba565330a5fc3c0ea0249aaf760c880439 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -75,8 +75,7 @@ minidumper.workspace = true [dev-dependencies] action_log.workspace = true -assistant_tool.workspace = true -assistant_tools.workspace = true +agent.workspace = true client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } collections.workspace = true diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index f6cddc65688a35b6ed67bfaa13bccb1ff5bde2c2..4010d033c09473cb475ae40b977af70fca390b82 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -2,12 +2,11 @@ /// The tests in this file assume that server_cx is running on Windows too. /// We neead to find a way to test Windows-Non-Windows interactions. use crate::headless_project::HeadlessProject; -use assistant_tool::{Tool as _, ToolResultContent}; -use assistant_tools::{ReadFileTool, ReadFileToolInput}; +use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream}; use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; -use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModel}; +use language_model::LanguageModelToolResultContent; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs}; @@ -1721,47 +1720,26 @@ async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mu .unwrap(); let action_log = cx.new(|_| action_log::ActionLog::new(project.clone())); - let model = Arc::new(FakeLanguageModel::default()); - let request = Arc::new(LanguageModelRequest::default()); let input = ReadFileToolInput { path: "project/b.txt".into(), start_line: None, end_line: None, }; - let exists_result = cx.update(|cx| { - ReadFileTool::run( - Arc::new(ReadFileTool), - serde_json::to_value(input).unwrap(), - request.clone(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }); - let output = exists_result.output.await.unwrap().content; - assert_eq!(output, ToolResultContent::Text("B".to_string())); + let read_tool = Arc::new(ReadFileTool::new(project, action_log)); + let (event_stream, _) = ToolCallEventStream::test(); + + let exists_result = cx.update(|cx| read_tool.clone().run(input, event_stream.clone(), cx)); + let output = exists_result.await.unwrap(); + assert_eq!(output, LanguageModelToolResultContent::Text("B".into())); let input = ReadFileToolInput { path: "project/c.txt".into(), start_line: None, end_line: None, }; - let does_not_exist_result = cx.update(|cx| { - ReadFileTool::run( - Arc::new(ReadFileTool), - serde_json::to_value(input).unwrap(), - request.clone(), - project.clone(), - action_log.clone(), - model.clone(), - None, - cx, - ) - }); - does_not_exist_result.output.await.unwrap_err(); + let does_not_exist_result = cx.update(|cx| read_tool.run(input, event_stream, cx)); + does_not_exist_result.await.unwrap_err(); } #[gpui::test] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fd9ffdead941a506c251dbae306988642e1926c7..44ab6c2285cf2a9d75393edbb053d21d35ea1840 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,13 +21,11 @@ path = "src/main.rs" [dependencies] acp_tools.workspace = true activity_indicator.workspace = true -agent.workspace = true agent_settings.workspace = true agent_ui.workspace = true anyhow.workspace = true askpass.workspace = true assets.workspace = true -assistant_tools.workspace = true audio.workspace = true auto_update.workspace = true auto_update_ui.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cc05cdfd822bd41135034dbaa3c174fd0af667cb..92897bc3344c710f4d694667a21040100a23a3cc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -582,7 +582,6 @@ pub fn main() { false, cx, ); - assistant_tools::init(app_state.client.http_client(), cx); repl::init(app_state.fs.clone(), cx); recent_projects::init(cx); diff --git a/script/danger/dangerfile.ts b/script/danger/dangerfile.ts index 6ed4a27fedb0bea7882ad4bcdd1016929bdd40e3..88dc5c5e71c640a83315ac5f1b14c216763023fd 100644 --- a/script/danger/dangerfile.ts +++ b/script/danger/dangerfile.ts @@ -61,12 +61,11 @@ if (includesIssueUrl) { const PROMPT_PATHS = [ "assets/prompts/content_prompt.hbs", "assets/prompts/terminal_assistant_prompt.hbs", - "crates/agent/src/prompts/stale_files_prompt_header.txt", - "crates/agent/src/prompts/summarize_thread_detailed_prompt.txt", - "crates/agent/src/prompts/summarize_thread_prompt.txt", - "crates/assistant_tools/src/templates/create_file_prompt.hbs", - "crates/assistant_tools/src/templates/edit_file_prompt_xml.hbs", - "crates/assistant_tools/src/templates/edit_file_prompt_diff_fenced.hbs", + "crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt", + "crates/agent_settings/src/prompts/summarize_thread_prompt.txt", + "crates/agent/src/templates/create_file_prompt.hbs", + "crates/agent/src/templates/edit_file_prompt_xml.hbs", + "crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs", "crates/git_ui/src/commit_message_prompt.txt", ]; From 62858f6a5cc3efdc7a967de24b0469ea6d90166d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 17 Oct 2025 13:28:19 -0400 Subject: [PATCH 003/202] Restore Oxford comma in README (#40518) We use Oxford commas in this household. Release Notes: - N/A --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 386c9b4664b0836a1c0e041d9d17a7952fb19e14..adc152b7af163b3c90c73a23e0f45bab1120bddc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of ### Installation -On macOS, Linux and Windows you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager). +On macOS, Linux, and Windows you can [download Zed directly](https://zed.dev/download) or [install Zed via your local package manager](https://zed.dev/docs/linux#installing-via-a-package-manager). Other platforms are not yet available: From 1fbe1e351246dd54577324e9632c6f9a48bc6ece Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 17 Oct 2025 11:47:05 -0600 Subject: [PATCH 004/202] VSCode settings import refactor (#40513) A small follow-up to the settings refactor of a few weeks ago to move all the VSCode settings imports to one place. This should make it easier to spot missing imports, and easier to test the importer. Release Notes: - N/A --- crates/agent_settings/src/agent_settings.rs | 12 +- crates/client/src/client.rs | 25 - crates/editor/src/editor_settings.rs | 208 +---- crates/git_ui/src/git_panel_settings.rs | 14 +- crates/language/src/language_settings.rs | 127 +-- .../src/outline_panel_settings.rs | 16 - crates/project/src/project.rs | 6 - crates/project/src/project_settings.rs | 59 -- .../src/project_panel_settings.rs | 40 +- crates/settings/src/base_keymap_setting.rs | 11 +- crates/settings/src/settings_store.rs | 71 +- crates/settings/src/vscode_import.rs | 812 ++++++++++++++++-- crates/terminal/src/terminal_settings.rs | 80 +- crates/theme/src/settings.rs | 11 - .../vim_mode_setting/src/vim_mode_setting.rs | 6 - crates/workspace/src/item.rs | 59 -- crates/workspace/src/workspace_settings.rs | 110 --- crates/worktree/src/worktree_settings.rs | 27 +- crates/zlog_settings/src/zlog_settings.rs | 2 - 19 files changed, 773 insertions(+), 923 deletions(-) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index adab899fadf3b36d199dd13ee19dc8421da9da8f..988340318c9f6a68d7b36010eecf0957df145236 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -10,7 +10,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, - NotifyWhenAgentWaiting, Settings, SettingsContent, + NotifyWhenAgentWaiting, Settings, }; pub use crate::agent_profile::*; @@ -185,14 +185,4 @@ impl Settings for AgentSettings { message_editor_min_lines: agent.message_editor_min_lines.unwrap(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - if let Some(b) = vscode - .read_value("chat.agent.enabled") - .and_then(|b| b.as_bool()) - { - current.agent.get_or_insert_default().enabled = Some(b); - current.agent.get_or_insert_default().button = Some(b); - } - } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 911cada78f14ee587a1b4570c9a35181a2e6fdec..5aff87155f3a0328aa017060604b5fc79604731e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -138,10 +138,6 @@ impl Settings for ProxySettings { proxy: content.proxy.clone(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - vscode.string_setting("http.proxy", &mut current.proxy); - } } pub fn init_settings(cx: &mut App) { @@ -525,27 +521,6 @@ impl settings::Settings for TelemetrySettings { metrics: content.telemetry.as_ref().unwrap().metrics.unwrap(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - let mut telemetry = settings::TelemetrySettingsContent::default(); - vscode.enum_setting("telemetry.telemetryLevel", &mut telemetry.metrics, |s| { - Some(s == "all") - }); - vscode.enum_setting( - "telemetry.telemetryLevel", - &mut telemetry.diagnostics, - |s| Some(matches!(s, "all" | "error" | "crash")), - ); - // we could translate telemetry.telemetryLevel, but just because users didn't want - // to send microsoft telemetry doesn't mean they don't want to send it to zed. their - // all/error/crash/off correspond to combinations of our "diagnostics" and "metrics". - if let Some(diagnostics) = telemetry.diagnostics { - current.telemetry.get_or_insert_default().diagnostics = Some(diagnostics) - } - if let Some(metrics) = telemetry.metrics { - current.telemetry.get_or_insert_default().metrics = Some(metrics) - } - } } impl Client { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 066d827bb90b96481823a92ea747d8123b95b47d..9ecbbff97612d391e56271f19331160ef08ba534 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -1,16 +1,14 @@ use core::num; -use std::num::NonZeroU32; use gpui::App; use language::CursorShape; use project::project_settings::DiagnosticSeverity; +use settings::Settings; pub use settings::{ CurrentLineHighlight, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder, - VsCodeSettings, }; -use settings::{Settings, SettingsContent}; use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; /// Imports from the VSCode settings at @@ -270,208 +268,4 @@ impl Settings for EditorSettings { minimum_contrast_for_highlights: editor.minimum_contrast_for_highlights.unwrap().0, } } - - fn import_from_vscode(vscode: &VsCodeSettings, current: &mut SettingsContent) { - vscode.enum_setting( - "editor.cursorBlinking", - &mut current.editor.cursor_blink, - |s| match s { - "blink" | "phase" | "expand" | "smooth" => Some(true), - "solid" => Some(false), - _ => None, - }, - ); - vscode.enum_setting( - "editor.cursorStyle", - &mut current.editor.cursor_shape, - |s| match s { - "block" => Some(settings::CursorShape::Block), - "block-outline" => Some(settings::CursorShape::Hollow), - "line" | "line-thin" => Some(settings::CursorShape::Bar), - "underline" | "underline-thin" => Some(settings::CursorShape::Underline), - _ => None, - }, - ); - - vscode.enum_setting( - "editor.renderLineHighlight", - &mut current.editor.current_line_highlight, - |s| match s { - "gutter" => Some(CurrentLineHighlight::Gutter), - "line" => Some(CurrentLineHighlight::Line), - "all" => Some(CurrentLineHighlight::All), - _ => None, - }, - ); - - vscode.bool_setting( - "editor.selectionHighlight", - &mut current.editor.selection_highlight, - ); - vscode.bool_setting( - "editor.roundedSelection", - &mut current.editor.rounded_selection, - ); - vscode.bool_setting( - "editor.hover.enabled", - &mut current.editor.hover_popover_enabled, - ); - vscode.u64_setting( - "editor.hover.delay", - &mut current.editor.hover_popover_delay, - ); - - let mut gutter = settings::GutterContent::default(); - vscode.enum_setting( - "editor.showFoldingControls", - &mut gutter.folds, - |s| match s { - "always" | "mouseover" => Some(true), - "never" => Some(false), - _ => None, - }, - ); - vscode.enum_setting( - "editor.lineNumbers", - &mut gutter.line_numbers, - |s| match s { - "on" | "relative" => Some(true), - "off" => Some(false), - _ => None, - }, - ); - if let Some(old_gutter) = current.editor.gutter.as_mut() { - if gutter.folds.is_some() { - old_gutter.folds = gutter.folds - } - if gutter.line_numbers.is_some() { - old_gutter.line_numbers = gutter.line_numbers - } - } else if gutter != settings::GutterContent::default() { - current.editor.gutter = Some(gutter) - } - if let Some(b) = vscode.read_bool("editor.scrollBeyondLastLine") { - current.editor.scroll_beyond_last_line = Some(if b { - ScrollBeyondLastLine::OnePage - } else { - ScrollBeyondLastLine::Off - }) - } - - let mut scrollbar_axes = settings::ScrollbarAxesContent::default(); - vscode.enum_setting( - "editor.scrollbar.horizontal", - &mut scrollbar_axes.horizontal, - |s| match s { - "auto" | "visible" => Some(true), - "hidden" => Some(false), - _ => None, - }, - ); - vscode.enum_setting( - "editor.scrollbar.vertical", - &mut scrollbar_axes.horizontal, - |s| match s { - "auto" | "visible" => Some(true), - "hidden" => Some(false), - _ => None, - }, - ); - - if scrollbar_axes != settings::ScrollbarAxesContent::default() { - let scrollbar_settings = current.editor.scrollbar.get_or_insert_default(); - let axes_settings = scrollbar_settings.axes.get_or_insert_default(); - - if let Some(vertical) = scrollbar_axes.vertical { - axes_settings.vertical = Some(vertical); - } - if let Some(horizontal) = scrollbar_axes.horizontal { - axes_settings.horizontal = Some(horizontal); - } - } - - // TODO: check if this does the int->float conversion? - vscode.f32_setting( - "editor.cursorSurroundingLines", - &mut current.editor.vertical_scroll_margin, - ); - vscode.f32_setting( - "editor.mouseWheelScrollSensitivity", - &mut current.editor.scroll_sensitivity, - ); - vscode.f32_setting( - "editor.fastScrollSensitivity", - &mut current.editor.fast_scroll_sensitivity, - ); - if Some("relative") == vscode.read_string("editor.lineNumbers") { - current.editor.relative_line_numbers = Some(true); - } - - vscode.enum_setting( - "editor.find.seedSearchStringFromSelection", - &mut current.editor.seed_search_query_from_cursor, - |s| match s { - "always" => Some(SeedQuerySetting::Always), - "selection" => Some(SeedQuerySetting::Selection), - "never" => Some(SeedQuerySetting::Never), - _ => None, - }, - ); - vscode.bool_setting("search.smartCase", &mut current.editor.use_smartcase_search); - vscode.enum_setting( - "editor.multiCursorModifier", - &mut current.editor.multi_cursor_modifier, - |s| match s { - "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), - "alt" => Some(MultiCursorModifier::Alt), - _ => None, - }, - ); - - vscode.bool_setting( - "editor.parameterHints.enabled", - &mut current.editor.auto_signature_help, - ); - vscode.bool_setting( - "editor.parameterHints.enabled", - &mut current.editor.show_signature_help_after_edits, - ); - - if let Some(use_ignored) = vscode.read_bool("search.useIgnoreFiles") { - let search = current.editor.search.get_or_insert_default(); - search.include_ignored = Some(use_ignored); - } - - let mut minimap = settings::MinimapContent::default(); - let minimap_enabled = vscode.read_bool("editor.minimap.enabled").unwrap_or(true); - let autohide = vscode.read_bool("editor.minimap.autohide"); - let mut max_width_columns: Option = None; - vscode.u32_setting("editor.minimap.maxColumn", &mut max_width_columns); - if minimap_enabled { - if let Some(false) = autohide { - minimap.show = Some(ShowMinimap::Always); - } else { - minimap.show = Some(ShowMinimap::Auto); - } - } else { - minimap.show = Some(ShowMinimap::Never); - } - if let Some(max_width_columns) = max_width_columns { - minimap.max_width_columns = NonZeroU32::new(max_width_columns); - } - - vscode.enum_setting( - "editor.minimap.showSlider", - &mut minimap.thumb, - |s| match s { - "always" => Some(MinimapThumb::Always), - "mouseover" => Some(MinimapThumb::Hover), - _ => None, - }, - ); - - if minimap != settings::MinimapContent::default() { - current.editor.minimap = Some(minimap) - } - } } diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index f98493d1d9ef4bcf9b53393671091c8b72dcd998..83259b228b59c5bb063473cc4a04710a0520808c 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -2,7 +2,7 @@ use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsContent, StatusStyle}; +use settings::{Settings, StatusStyle}; use ui::{ px, scrollbars::{ScrollbarVisibility, ShowScrollbar}, @@ -58,16 +58,4 @@ impl Settings for GitPanelSettings { collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - if let Some(git_enabled) = vscode.read_bool("git.enabled") { - current.git_panel.get_or_insert_default().button = Some(git_enabled); - } - if let Some(default_branch) = vscode.read_string("git.defaultBranchName") { - current - .git_panel - .get_or_insert_default() - .fallback_branch_name = Some(default_branch.to_string()); - } - } } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 2f96e850f49dd5cda6c5d34b2b424812b23a524d..b6c65ede0596fe96ba1a750bcbcbcb971a3be617 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -15,7 +15,7 @@ pub use settings::{ Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; -use settings::{ExtendingVec, Settings, SettingsContent, SettingsLocation, SettingsStore}; +use settings::{Settings, SettingsLocation, SettingsStore}; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; @@ -679,131 +679,6 @@ impl settings::Settings for AllLanguageSettings { file_types, } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - let d = &mut current.project.all_languages.defaults; - if let Some(size) = vscode - .read_value("editor.tabSize") - .and_then(|v| v.as_u64()) - .and_then(|n| NonZeroU32::new(n as u32)) - { - d.tab_size = Some(size); - } - if let Some(v) = vscode.read_bool("editor.insertSpaces") { - d.hard_tabs = Some(!v); - } - - vscode.enum_setting("editor.wordWrap", &mut d.soft_wrap, |s| match s { - "on" => Some(SoftWrap::EditorWidth), - "wordWrapColumn" => Some(SoftWrap::PreferLine), - "bounded" => Some(SoftWrap::Bounded), - "off" => Some(SoftWrap::None), - _ => None, - }); - vscode.u32_setting("editor.wordWrapColumn", &mut d.preferred_line_length); - - if let Some(arr) = vscode - .read_value("editor.rulers") - .and_then(|v| v.as_array()) - .map(|v| v.iter().map(|n| n.as_u64().map(|n| n as usize)).collect()) - { - d.wrap_guides = arr; - } - if let Some(b) = vscode.read_bool("editor.guides.indentation") { - d.indent_guides.get_or_insert_default().enabled = Some(b); - } - - if let Some(b) = vscode.read_bool("editor.guides.formatOnSave") { - d.format_on_save = Some(if b { - FormatOnSave::On - } else { - FormatOnSave::Off - }); - } - vscode.bool_setting( - "editor.trimAutoWhitespace", - &mut d.remove_trailing_whitespace_on_save, - ); - vscode.bool_setting( - "files.insertFinalNewline", - &mut d.ensure_final_newline_on_save, - ); - vscode.bool_setting("editor.inlineSuggest.enabled", &mut d.show_edit_predictions); - vscode.enum_setting("editor.renderWhitespace", &mut d.show_whitespaces, |s| { - Some(match s { - "boundary" => ShowWhitespaceSetting::Boundary, - "trailing" => ShowWhitespaceSetting::Trailing, - "selection" => ShowWhitespaceSetting::Selection, - "all" => ShowWhitespaceSetting::All, - _ => ShowWhitespaceSetting::None, - }) - }); - vscode.enum_setting( - "editor.autoSurround", - &mut d.use_auto_surround, - |s| match s { - "languageDefined" | "quotes" | "brackets" => Some(true), - "never" => Some(false), - _ => None, - }, - ); - vscode.bool_setting("editor.formatOnType", &mut d.use_on_type_format); - vscode.bool_setting("editor.linkedEditing", &mut d.linked_edits); - vscode.bool_setting("editor.formatOnPaste", &mut d.auto_indent_on_paste); - vscode.bool_setting( - "editor.suggestOnTriggerCharacters", - &mut d.show_completions_on_input, - ); - if let Some(b) = vscode.read_bool("editor.suggest.showWords") { - let mode = if b { - WordsCompletionMode::Enabled - } else { - WordsCompletionMode::Disabled - }; - d.completions.get_or_insert_default().words = Some(mode); - } - // TODO: pull ^ out into helper and reuse for per-language settings - - // vscodes file association map is inverted from ours, so we flip the mapping before merging - let mut associations: HashMap, ExtendingVec> = HashMap::default(); - if let Some(map) = vscode - .read_value("files.associations") - .and_then(|v| v.as_object()) - { - for (k, v) in map { - let Some(v) = v.as_str() else { continue }; - associations.entry(v.into()).or_default().0.push(k.clone()); - } - } - - // TODO: do we want to merge imported globs per filetype? for now we'll just replace - current - .project - .all_languages - .file_types - .get_or_insert_default() - .extend(associations); - - // cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs - if let Some(disabled_globs) = vscode - .read_value("cursor.general.globalCursorIgnoreList") - .and_then(|v| v.as_array()) - { - current - .project - .all_languages - .edit_predictions - .get_or_insert_default() - .disabled_globs - .get_or_insert_default() - .extend( - disabled_globs - .iter() - .filter_map(|glob| glob.as_str()) - .map(|s| s.to_string()), - ); - } - } } #[derive(Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 58598bdb4f9089e2c6284976869b82be600825ae..77fb15ddeb273b6fbe928e5f364f4a135321e7be 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -62,20 +62,4 @@ impl Settings for OutlinePanelSettings { expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(), } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - if let Some(b) = vscode.read_bool("outline.icons") { - let outline_panel = current.outline_panel.get_or_insert_default(); - outline_panel.file_icons = Some(b); - outline_panel.folder_icons = Some(b); - } - - if let Some(b) = vscode.read_bool("git.decorations.enabled") { - let outline_panel = current.outline_panel.get_or_insert_default(); - outline_panel.git_status = Some(b); - } - } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4fed6be0ef3eb8eeb587015df323f00864bd95ea..56a2811f07a4c3f37c610df48bb7c6db1904f9f2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -985,12 +985,6 @@ impl settings::Settings for DisableAiSettings { disable_ai: content.disable_ai.unwrap().0, } } - - fn import_from_vscode( - _vscode: &settings::VsCodeSettings, - _current: &mut settings::SettingsContent, - ) { - } } impl Project { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 88fe7bb6d215540e68d7770c799bc3028cadc674..788cd0212a094154e6c4e3b3eb0d379ecadaf11c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -522,65 +522,6 @@ impl Settings for ProjectSettings { }, } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - // this just sets the binary name instead of a full path so it relies on path lookup - // resolving to the one you want - let npm_path = vscode.read_enum("npm.packageManager", |s| match s { - v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), - _ => None, - }); - if npm_path.is_some() { - current.node.get_or_insert_default().npm_path = npm_path; - } - - if let Some(b) = vscode.read_bool("git.blame.editorDecoration.enabled") { - current - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .enabled = Some(b); - } - - #[derive(Deserialize)] - struct VsCodeContextServerCommand { - command: PathBuf, - args: Option>, - env: Option>, - // note: we don't support envFile and type - } - if let Some(mcp) = vscode.read_value("mcp").and_then(|v| v.as_object()) { - current - .project - .context_servers - .extend(mcp.iter().filter_map(|(k, v)| { - Some(( - k.clone().into(), - settings::ContextServerSettingsContent::Custom { - enabled: true, - command: serde_json::from_value::( - v.clone(), - ) - .ok() - .map(|cmd| { - settings::ContextServerCommand { - path: cmd.command, - args: cmd.args.unwrap_or_default(), - env: cmd.env, - timeout: None, - } - })?, - }, - )) - })); - } - - // TODO: translate lsp settings for rust-analyzer and other popular ones to old.lsp - } } pub enum SettingsObserverMode { diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 3eca8a6e8685b787069bc14482bdffce551d87ac..632537fc0213f3702755144c045e58fcb737ed30 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -2,10 +2,7 @@ use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{ - DockSide, ProjectPanelEntrySpacing, Settings, SettingsContent, ShowDiagnostics, - ShowIndentGuides, -}; +use settings::{DockSide, ProjectPanelEntrySpacing, Settings, ShowDiagnostics, ShowIndentGuides}; use ui::{ px, scrollbars::{ScrollbarVisibility, ShowScrollbar}, @@ -86,39 +83,4 @@ impl Settings for ProjectPanelSettings { open_file_on_paste: project_panel.open_file_on_paste.unwrap(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - if let Some(hide_gitignore) = vscode.read_bool("explorer.excludeGitIgnore") { - current.project_panel.get_or_insert_default().hide_gitignore = Some(hide_gitignore); - } - if let Some(auto_reveal) = vscode.read_bool("explorer.autoReveal") { - current - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(auto_reveal); - } - if let Some(compact_folders) = vscode.read_bool("explorer.compactFolders") { - current.project_panel.get_or_insert_default().auto_fold_dirs = Some(compact_folders); - } - - if Some(false) == vscode.read_bool("git.decorations.enabled") { - current.project_panel.get_or_insert_default().git_status = Some(false); - } - if Some(false) == vscode.read_bool("problems.decorations.enabled") { - current - .project_panel - .get_or_insert_default() - .show_diagnostics = Some(ShowDiagnostics::Off); - } - if let (Some(false), Some(false)) = ( - vscode.read_bool("explorer.decorations.badges"), - vscode.read_bool("explorer.decorations.colors"), - ) { - current.project_panel.get_or_insert_default().git_status = Some(false); - current - .project_panel - .get_or_insert_default() - .show_diagnostics = Some(ShowDiagnostics::Off); - } - } } diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs index e955a539ac73d2c8c8ab6c6ca443962bf2959e75..4915bdd85319e4abaf3ea575d387a39cc14f302d 100644 --- a/crates/settings/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,12 +1,9 @@ use std::fmt::{Display, Formatter}; -use crate::{ - self as settings, - settings_content::{BaseKeymapContent, SettingsContent}, -}; +use crate::{self as settings, settings_content::BaseKeymapContent}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, VsCodeSettings}; +use settings::Settings; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// @@ -133,8 +130,4 @@ impl Settings for BaseKeymap { fn from_settings(s: &crate::settings_content::SettingsContent) -> Self { s.base_keymap.unwrap().into() } - - fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut SettingsContent) { - current.base_keymap = Some(BaseKeymapContent::VSCode); - } } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 559ed777c7aa86047f6acb33c8b358e0bd7b6e58..709b4982706250f91c7aaefc365ddf7613cdf5f4 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -70,10 +70,6 @@ pub trait Settings: 'static + Send + Sync + Sized { /// and you should add a default to default.json for documentation. fn from_settings(content: &SettingsContent) -> Self; - /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known - /// equivalent settings from a vscode config to our config - fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut SettingsContent) {} - #[track_caller] fn register(cx: &mut App) where @@ -208,11 +204,6 @@ trait AnySettingValue: 'static + Send + Sync { fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); - fn import_from_vscode( - &self, - vscode_settings: &VsCodeSettings, - settings_content: &mut SettingsContent, - ); } impl SettingsStore { @@ -614,10 +605,8 @@ impl SettingsStore { } pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String { - self.new_text_for_update(old_text, |settings_content| { - for v in self.setting_values.values() { - v.import_from_vscode(vscode, settings_content) - } + self.new_text_for_update(old_text, |content| { + content.merge_from(&vscode.settings_content()) }) } @@ -1129,14 +1118,6 @@ impl AnySettingValue for SettingValue { Err(ix) => self.local_values.insert(ix, (root_id, path, value)), } } - - fn import_from_vscode( - &self, - vscode_settings: &VsCodeSettings, - settings_content: &mut SettingsContent, - ) { - T::import_from_vscode(vscode_settings, settings_content); - } } #[cfg(test)] @@ -1179,19 +1160,6 @@ mod tests { git_status: content.git_status.unwrap(), } } - - fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { - let mut show = None; - - vscode.bool_setting("workbench.editor.decorations.colors", &mut show); - if let Some(show) = show { - content - .tabs - .get_or_insert_default() - .git_status - .replace(show); - } - } } #[derive(Debug, PartialEq)] @@ -1208,18 +1176,6 @@ mod tests { preferred_line_length: content.preferred_line_length.unwrap(), } } - - fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { - let content = &mut content.project.all_languages.defaults; - - if let Some(size) = vscode - .read_value("editor.tabSize") - .and_then(|v| v.as_u64()) - .and_then(|n| NonZeroU32::new(n as u32)) - { - content.tab_size = Some(size); - } - } } #[derive(Debug, PartialEq)] @@ -1236,16 +1192,6 @@ mod tests { buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(), } } - - fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { - let content = &mut content.theme; - - vscode.font_family_setting( - "editor.fontFamily", - &mut content.buffer_font_family, - &mut content.buffer_font_fallbacks, - ); - } } #[gpui::test] @@ -1581,6 +1527,7 @@ mod tests { .unindent(), r#" { "editor.tabSize": 37 } "#.to_owned(), r#"{ + "base_keymap": "VSCode", "tab_size": 37 } "# @@ -1598,6 +1545,7 @@ mod tests { .unindent(), r#"{ "editor.tabSize": 42 }"#.to_owned(), r#"{ + "base_keymap": "VSCode", "tab_size": 42, "preferred_line_length": 99, } @@ -1617,6 +1565,7 @@ mod tests { .unindent(), r#"{}"#.to_owned(), r#"{ + "base_keymap": "VSCode", "preferred_line_length": 99, "tab_size": 42 } @@ -1632,8 +1581,15 @@ mod tests { } "# .unindent(), - r#"{ "workbench.editor.decorations.colors": true }"#.to_owned(), + r#"{ "git.decorations.enabled": true }"#.to_owned(), r#"{ + "project_panel": { + "git_status": true + }, + "outline_panel": { + "git_status": true + }, + "base_keymap": "VSCode", "tabs": { "git_status": true } @@ -1652,6 +1608,7 @@ mod tests { .unindent(), r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(), r#"{ + "base_keymap": "VSCode", "buffer_font_fallbacks": [ "Consolas", "Courier New" diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index c0c1085684b448dbd3d4ef83faabf21ca1cfbf7f..cc55613c63ef7d21b5f4830b0f5c6496ac1930f2 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -1,10 +1,15 @@ +use crate::*; use anyhow::{Context as _, Result, anyhow}; +use collections::HashMap; use fs::Fs; use paths::{cursor_settings_file_paths, vscode_settings_file_paths}; +use serde::Deserialize; use serde_json::{Map, Value}; -use std::{path::Path, sync::Arc}; - -use crate::FontFamilyName; +use std::{ + num::{NonZeroU32, NonZeroUsize}, + path::{Path, PathBuf}, + sync::Arc, +}; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum VsCodeSettingsSource { @@ -79,83 +84,53 @@ impl VsCodeSettings { }) } - pub fn read_value(&self, setting: &str) -> Option<&Value> { + fn read_value(&self, setting: &str) -> Option<&Value> { self.content.get(setting) } - pub fn read_string(&self, setting: &str) -> Option<&str> { + fn read_str(&self, setting: &str) -> Option<&str> { self.read_value(setting).and_then(|v| v.as_str()) } - pub fn read_bool(&self, setting: &str) -> Option { - self.read_value(setting).and_then(|v| v.as_bool()) - } - - pub fn string_setting(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_str) { - *setting = Some(s.to_owned()) - } - } - - pub fn bool_setting(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_bool) { - *setting = Some(s) - } + fn read_string(&self, setting: &str) -> Option { + self.read_value(setting) + .and_then(|v| v.as_str()) + .map(|s| s.to_owned()) } - pub fn u32_setting(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_u64) { - *setting = Some(s as u32) - } + fn read_bool(&self, setting: &str) -> Option { + self.read_value(setting).and_then(|v| v.as_bool()) } - pub fn u64_setting(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_u64) { - *setting = Some(s) - } + fn read_f32(&self, setting: &str) -> Option { + self.read_value(setting) + .and_then(|v| v.as_f64()) + .map(|v| v as f32) } - pub fn usize_setting(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_u64) { - *setting = Some(s.try_into().unwrap()) - } + fn read_u64(&self, setting: &str) -> Option { + self.read_value(setting).and_then(|v| v.as_u64()) } - pub fn f32_setting(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_f64) { - *setting = Some(s as f32) - } + fn read_usize(&self, setting: &str) -> Option { + self.read_value(setting) + .and_then(|v| v.as_u64()) + .and_then(|v| v.try_into().ok()) } - pub fn from_f32_setting>(&self, key: &str, setting: &mut Option) { - if let Some(s) = self.content.get(key).and_then(Value::as_f64) { - *setting = Some(T::from(s as f32)) - } + fn read_u32(&self, setting: &str) -> Option { + self.read_value(setting) + .and_then(|v| v.as_u64()) + .and_then(|v| v.try_into().ok()) } - pub fn enum_setting( - &self, - key: &str, - setting: &mut Option, - f: impl FnOnce(&str) -> Option, - ) { - if let Some(s) = self.content.get(key).and_then(Value::as_str).and_then(f) { - *setting = Some(s) - } - } - - pub fn read_enum(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { + fn read_enum(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { self.content.get(key).and_then(Value::as_str).and_then(f) } - pub fn font_family_setting( - &self, - key: &str, - font_family: &mut Option, - font_fallbacks: &mut Option>, - ) { + fn read_fonts(&self, key: &str) -> (Option, Option>) { let Some(css_name) = self.content.get(key).and_then(Value::as_str) else { - return; + return (None, None); }; let mut name_buffer = String::new(); @@ -188,12 +163,723 @@ impl VsCodeSettings { } add_font(&mut name_buffer); + if fonts.is_empty() { + return (None, None); + } + (Some(fonts.remove(0)), skip_default(fonts)) + } + + pub fn settings_content(&self) -> SettingsContent { + SettingsContent { + agent: self.agent_settings_content(), + agent_servers: None, + audio: None, + auto_update: None, + base_keymap: Some(BaseKeymapContent::VSCode), + calls: None, + collaboration_panel: None, + debugger: None, + diagnostics: None, + disable_ai: None, + editor: self.editor_settings_content(), + extension: ExtensionSettingsContent::default(), + file_finder: None, + git: self.git_settings_content(), + git_panel: self.git_panel_settings_content(), + global_lsp_settings: None, + helix_mode: None, + image_viewer: None, + journal: None, + language_models: None, + line_indicator_format: None, + log: None, + message_editor: None, + node: self.node_binary_settings(), + notification_panel: None, + outline_panel: self.outline_panel_settings_content(), + preview_tabs: self.preview_tabs_settings_content(), + project: self.project_settings_content(), + project_panel: self.project_panel_settings_content(), + proxy: self.read_string("http.proxy"), + remote: RemoteSettingsContent::default(), + repl: None, + server_url: None, + session: None, + status_bar: self.status_bar_settings_content(), + tab_bar: self.tab_bar_settings_content(), + tabs: self.item_settings_content(), + telemetry: self.telemetry_settings_content(), + terminal: self.terminal_settings_content(), + theme: Box::new(self.theme_settings_content()), + title_bar: None, + vim: None, + vim_mode: None, + workspace: self.workspace_settings_content(), + } + } + + fn agent_settings_content(&self) -> Option { + let enabled = self.read_bool("chat.agent.enabled"); + skip_default(AgentSettingsContent { + enabled: enabled, + button: enabled, + ..Default::default() + }) + } + + fn editor_settings_content(&self) -> EditorSettingsContent { + EditorSettingsContent { + auto_signature_help: self.read_bool("editor.parameterHints.enabled"), + autoscroll_on_clicks: None, + cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s { + "blink" | "phase" | "expand" | "smooth" => Some(true), + "solid" => Some(false), + _ => None, + }), + cursor_shape: self.read_enum("editor.cursorStyle", |s| match s { + "block" => Some(CursorShape::Block), + "block-outline" => Some(CursorShape::Hollow), + "line" | "line-thin" => Some(CursorShape::Bar), + "underline" | "underline-thin" => Some(CursorShape::Underline), + _ => None, + }), + current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s { + "gutter" => Some(CurrentLineHighlight::Gutter), + "line" => Some(CurrentLineHighlight::Line), + "all" => Some(CurrentLineHighlight::All), + _ => None, + }), + diagnostics_max_severity: None, + double_click_in_multibuffer: None, + drag_and_drop_selection: None, + excerpt_context_lines: None, + expand_excerpt_lines: None, + fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"), + go_to_definition_fallback: None, + gutter: self.gutter_content(), + hide_mouse: None, + horizontal_scroll_margin: None, + hover_popover_delay: self.read_u64("editor.hover.delay"), + hover_popover_enabled: self.read_bool("editor.hover.enabled"), + inline_code_actions: None, + jupyter: None, + lsp_document_colors: None, + lsp_highlight_debounce: None, + middle_click_paste: None, + minimap: self.minimap_content(), + minimum_contrast_for_highlights: None, + multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s { + "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), + "alt" => Some(MultiCursorModifier::Alt), + _ => None, + }), + redact_private_values: None, + relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s { + "relative" => Some(true), + _ => None, + }), + rounded_selection: self.read_bool("editor.roundedSelection"), + scroll_beyond_last_line: None, + scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"), + scrollbar: self.scrollbar_content(), + search: self.search_content(), + search_wrap: None, + seed_search_query_from_cursor: self.read_enum( + "editor.find.seedSearchStringFromSelection", + |s| match s { + "always" => Some(SeedQuerySetting::Always), + "selection" => Some(SeedQuerySetting::Selection), + "never" => Some(SeedQuerySetting::Never), + _ => None, + }, + ), + selection_highlight: self.read_bool("editor.selectionHighlight"), + show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"), + snippet_sort_order: None, + toolbar: None, + use_smartcase_search: self.read_bool("search.smartCase"), + vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"), + } + } + + fn gutter_content(&self) -> Option { + skip_default(GutterContent { + line_numbers: self.read_enum("editor.lineNumbers", |s| match s { + "on" | "relative" => Some(true), + "off" => Some(false), + _ => None, + }), + min_line_number_digits: None, + runnables: None, + breakpoints: None, + folds: self.read_enum("editor.showFoldingControls", |s| match s { + "always" | "mouseover" => Some(true), + "never" => Some(false), + _ => None, + }), + }) + } + + fn scrollbar_content(&self) -> Option { + let scrollbar_axes = skip_default(ScrollbarAxesContent { + horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s { + "auto" | "visible" => Some(true), + "hidden" => Some(false), + _ => None, + }), + vertical: self.read_enum("editor.scrollbar.vertical", |s| match s { + "auto" | "visible" => Some(true), + "hidden" => Some(false), + _ => None, + }), + })?; + + Some(ScrollbarContent { + axes: Some(scrollbar_axes), + ..Default::default() + }) + } + + fn search_content(&self) -> Option { + skip_default(SearchSettingsContent { + include_ignored: self.read_bool("search.useIgnoreFiles"), + ..Default::default() + }) + } + + fn minimap_content(&self) -> Option { + let minimap_enabled = self.read_bool("editor.minimap.enabled"); + let autohide = self.read_bool("editor.minimap.autohide"); + let show = match (minimap_enabled, autohide) { + (Some(true), Some(false)) => Some(ShowMinimap::Always), + (Some(true), _) => Some(ShowMinimap::Auto), + (Some(false), _) => Some(ShowMinimap::Never), + _ => None, + }; + + skip_default(MinimapContent { + show, + thumb: self.read_enum("editor.minimap.showSlider", |s| match s { + "always" => Some(MinimapThumb::Always), + "mouseover" => Some(MinimapThumb::Hover), + _ => None, + }), + max_width_columns: self + .read_u32("editor.minimap.maxColumn") + .and_then(|v| NonZeroU32::new(v)), + ..Default::default() + }) + } - let mut iter = fonts.into_iter(); - *font_family = iter.next(); - let fallbacks: Vec<_> = iter.collect(); - if !fallbacks.is_empty() { - *font_fallbacks = Some(fallbacks); + fn git_panel_settings_content(&self) -> Option { + skip_default(GitPanelSettingsContent { + button: self.read_bool("git.enabled"), + fallback_branch_name: self.read_string("git.defaultBranchName"), + ..Default::default() + }) + } + + fn project_settings_content(&self) -> ProjectSettingsContent { + ProjectSettingsContent { + all_languages: AllLanguageSettingsContent { + features: None, + edit_predictions: self.edit_predictions_settings_content(), + defaults: self.default_language_settings_content(), + languages: Default::default(), + file_types: self.file_types(), + }, + worktree: self.worktree_settings_content(), + lsp: Default::default(), + terminal: None, + dap: Default::default(), + context_servers: self.context_servers(), + load_direnv: None, + slash_commands: None, + git_hosting_providers: None, + } + } + + fn default_language_settings_content(&self) -> LanguageSettingsContent { + LanguageSettingsContent { + allow_rewrap: None, + always_treat_brackets_as_autoclosed: None, + auto_indent: None, + auto_indent_on_paste: self.read_bool("editor.formatOnPaste"), + code_actions_on_format: None, + completions: skip_default(CompletionSettingsContent { + words: self.read_bool("editor.suggest.showWords").map(|b| { + if b { + WordsCompletionMode::Enabled + } else { + WordsCompletionMode::Disabled + } + }), + ..Default::default() + }), + debuggers: None, + edit_predictions_disabled_in: None, + enable_language_server: None, + ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"), + extend_comment_on_newline: None, + format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| { + if b { + FormatOnSave::On + } else { + FormatOnSave::Off + } + }), + formatter: None, + hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v), + indent_guides: skip_default(IndentGuideSettingsContent { + enabled: self.read_bool("editor.guides.indentation"), + ..Default::default() + }), + inlay_hints: None, + jsx_tag_auto_close: None, + language_servers: None, + linked_edits: self.read_bool("editor.linkedEditing"), + preferred_line_length: self.read_u32("editor.wordWrapColumn"), + prettier: None, + remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"), + show_completion_documentation: None, + show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"), + show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"), + show_whitespaces: self.read_enum("editor.renderWhitespace", |s| { + Some(match s { + "boundary" => ShowWhitespaceSetting::Boundary, + "trailing" => ShowWhitespaceSetting::Trailing, + "selection" => ShowWhitespaceSetting::Selection, + "all" => ShowWhitespaceSetting::All, + _ => ShowWhitespaceSetting::None, + }) + }), + show_wrap_guides: None, + soft_wrap: self.read_enum("editor.wordWrap", |s| match s { + "on" => Some(SoftWrap::EditorWidth), + "wordWrapColumn" => Some(SoftWrap::PreferLine), + "bounded" => Some(SoftWrap::Bounded), + "off" => Some(SoftWrap::None), + _ => None, + }), + tab_size: self + .read_u32("editor.tabSize") + .and_then(|n| NonZeroU32::new(n)), + tasks: None, + use_auto_surround: self.read_enum("editor.autoSurround", |s| match s { + "languageDefined" | "quotes" | "brackets" => Some(true), + "never" => Some(false), + _ => None, + }), + use_autoclose: None, + use_on_type_format: self.read_bool("editor.formatOnType"), + whitespace_map: None, + wrap_guides: self + .read_value("editor.rulers") + .and_then(|v| v.as_array()) + .map(|v| { + v.iter() + .flat_map(|n| n.as_u64().map(|n| n as usize)) + .collect() + }), } } + + fn file_types(&self) -> Option, ExtendingVec>> { + // vscodes file association map is inverted from ours, so we flip the mapping before merging + let mut associations: HashMap, ExtendingVec> = HashMap::default(); + let map = self.read_value("files.associations")?.as_object()?; + for (k, v) in map { + let Some(v) = v.as_str() else { continue }; + associations.entry(v.into()).or_default().0.push(k.clone()); + } + skip_default(associations) + } + + fn edit_predictions_settings_content(&self) -> Option { + let disabled_globs = self + .read_value("cursor.general.globalCursorIgnoreList")? + .as_array()?; + + skip_default(EditPredictionSettingsContent { + disabled_globs: skip_default( + disabled_globs + .iter() + .filter_map(|glob| glob.as_str()) + .map(|s| s.to_string()) + .collect(), + ), + ..Default::default() + }) + } + + fn outline_panel_settings_content(&self) -> Option { + skip_default(OutlinePanelSettingsContent { + file_icons: self.read_bool("outline.icons"), + folder_icons: self.read_bool("outline.icons"), + git_status: self.read_bool("git.decorations.enabled"), + ..Default::default() + }) + } + + fn node_binary_settings(&self) -> Option { + // this just sets the binary name instead of a full path so it relies on path lookup + // resolving to the one you want + skip_default(NodeBinarySettings { + npm_path: self.read_enum("npm.packageManager", |s| match s { + v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), + _ => None, + }), + ..Default::default() + }) + } + + fn git_settings_content(&self) -> Option { + let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?; + skip_default(GitSettings { + inline_blame: Some(InlineBlameSettings { + enabled: Some(inline_blame), + ..Default::default() + }), + ..Default::default() + }) + } + + fn context_servers(&self) -> HashMap, ContextServerSettingsContent> { + #[derive(Deserialize)] + struct VsCodeContextServerCommand { + command: PathBuf, + args: Option>, + env: Option>, + // note: we don't support envFile and type + } + let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else { + return Default::default(); + }; + mcp.iter() + .filter_map(|(k, v)| { + Some(( + k.clone().into(), + ContextServerSettingsContent::Custom { + enabled: true, + command: serde_json::from_value::(v.clone()) + .ok() + .map(|cmd| ContextServerCommand { + path: cmd.command, + args: cmd.args.unwrap_or_default(), + env: cmd.env, + timeout: None, + })?, + }, + )) + }) + .collect() + } + + fn item_settings_content(&self) -> Option { + skip_default(ItemSettingsContent { + git_status: self.read_bool("git.decorations.enabled"), + close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s { + "right" => Some(ClosePosition::Right), + "left" => Some(ClosePosition::Left), + _ => None, + }), + file_icons: self.read_bool("workbench.editor.showIcons"), + activate_on_close: self + .read_bool("workbench.editor.focusRecentEditorAfterClose") + .map(|b| { + if b { + ActivateOnClose::History + } else { + ActivateOnClose::LeftNeighbour + } + }), + show_diagnostics: None, + show_close_button: self + .read_bool("workbench.editor.tabActionCloseVisibility") + .map(|b| { + if b { + ShowCloseButton::Always + } else { + ShowCloseButton::Hidden + } + }), + }) + } + + fn preview_tabs_settings_content(&self) -> Option { + skip_default(PreviewTabsSettingsContent { + enabled: self.read_bool("workbench.editor.enablePreview"), + enable_preview_from_file_finder: self + .read_bool("workbench.editor.enablePreviewFromQuickOpen"), + enable_preview_from_code_navigation: self + .read_bool("workbench.editor.enablePreviewFromCodeNavigation"), + }) + } + + fn tab_bar_settings_content(&self) -> Option { + skip_default(TabBarSettingsContent { + show: self.read_enum("workbench.editor.showTabs", |s| match s { + "multiple" => Some(true), + "single" | "none" => Some(false), + _ => None, + }), + show_nav_history_buttons: None, + show_tab_bar_buttons: self + .read_str("workbench.editor.editorActionsLocation") + .and_then(|str| if str == "hidden" { Some(false) } else { None }), + }) + } + + fn status_bar_settings_content(&self) -> Option { + skip_default(StatusBarSettingsContent { + show: self.read_bool("workbench.statusBar.visible"), + active_language_button: None, + cursor_position_button: None, + }) + } + + fn project_panel_settings_content(&self) -> Option { + let mut project_panel_settings = ProjectPanelSettingsContent { + auto_fold_dirs: self.read_bool("explorer.compactFolders"), + auto_reveal_entries: self.read_bool("explorer.autoReveal"), + button: None, + default_width: None, + dock: None, + drag_and_drop: None, + entry_spacing: None, + file_icons: None, + folder_icons: None, + git_status: self.read_bool("git.decorations.enabled"), + hide_gitignore: self.read_bool("explorer.excludeGitIgnore"), + hide_hidden: None, + hide_root: None, + indent_guides: None, + indent_size: None, + open_file_on_paste: None, + scrollbar: None, + show_diagnostics: self + .read_bool("problems.decorations.enabled") + .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }), + starts_open: None, + sticky_scroll: None, + }; + + if let (Some(false), Some(false)) = ( + self.read_bool("explorer.decorations.badges"), + self.read_bool("explorer.decorations.colors"), + ) { + project_panel_settings.git_status = Some(false); + project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off); + } + + skip_default(project_panel_settings) + } + + fn telemetry_settings_content(&self) -> Option { + self.read_enum("telemetry.telemetryLevel", |level| { + let (metrics, diagnostics) = match level { + "all" => (true, true), + "error" | "crash" => (false, true), + "off" => (false, false), + _ => return None, + }; + Some(TelemetrySettingsContent { + metrics: Some(metrics), + diagnostics: Some(diagnostics), + }) + }) + } + + fn terminal_settings_content(&self) -> Option { + let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily"); + skip_default(TerminalSettingsContent { + alternate_scroll: None, + blinking: self + .read_bool("terminal.integrated.cursorBlinking") + .map(|b| { + if b { + TerminalBlink::On + } else { + TerminalBlink::Off + } + }), + button: None, + copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"), + cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s { + "block" => Some(CursorShapeContent::Block), + "line" => Some(CursorShapeContent::Bar), + "underline" => Some(CursorShapeContent::Underline), + _ => None, + }), + default_height: None, + default_width: None, + dock: None, + font_fallbacks, + font_family, + font_features: None, + font_size: self.read_f32("terminal.integrated.fontSize"), + font_weight: None, + keep_selection_on_copy: None, + line_height: self + .read_f32("terminal.integrated.lineHeight") + .map(|lh| TerminalLineHeight::Custom(lh)), + max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"), + minimum_contrast: None, + option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"), + project: self.project_terminal_settings_content(), + scrollbar: None, + toolbar: None, + }) + } + + fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent { + #[cfg(target_os = "windows")] + let platform = "windows"; + #[cfg(target_os = "linux")] + let platform = "linux"; + #[cfg(target_os = "macos")] + let platform = "osx"; + #[cfg(target_os = "freebsd")] + let platform = "freebsd"; + let env = self + .read_value(&format!("terminal.integrated.env.{platform}")) + .and_then(|v| v.as_object()) + .map(|v| v.iter().map(|(k, v)| (k.clone(), v.to_string())).collect()); + + ProjectTerminalSettingsContent { + // TODO: handle arguments + shell: self + .read_string(&format!("terminal.integrated.{platform}Exec")) + .map(|s| Shell::Program(s)), + working_directory: None, + env, + detect_venv: None, + } + } + + fn theme_settings_content(&self) -> ThemeSettingsContent { + let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily"); + ThemeSettingsContent { + ui_font_size: None, + ui_font_family: None, + ui_font_fallbacks: None, + ui_font_features: None, + ui_font_weight: None, + buffer_font_family, + buffer_font_fallbacks, + buffer_font_size: self.read_f32("editor.fontSize"), + buffer_font_weight: self.read_f32("editor.fontWeight").map(|w| w.into()), + buffer_line_height: None, + buffer_font_features: None, + agent_ui_font_size: None, + agent_buffer_font_size: None, + theme: None, + icon_theme: None, + ui_density: None, + unnecessary_code_fade: None, + experimental_theme_overrides: None, + theme_overrides: Default::default(), + } + } + + fn workspace_settings_content(&self) -> WorkspaceSettingsContent { + WorkspaceSettingsContent { + active_pane_modifiers: self.active_pane_modifiers(), + autosave: self.read_enum("files.autoSave", |s| match s { + "off" => Some(AutosaveSetting::Off), + "afterDelay" => Some(AutosaveSetting::AfterDelay { + milliseconds: self + .read_value("files.autoSaveDelay") + .and_then(|v| v.as_u64()) + .unwrap_or(1000), + }), + "onFocusChange" => Some(AutosaveSetting::OnFocusChange), + "onWindowChange" => Some(AutosaveSetting::OnWindowChange), + _ => None, + }), + bottom_dock_layout: None, + centered_layout: None, + close_on_file_delete: None, + command_aliases: Default::default(), + confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s { + "always" | "keyboardOnly" => Some(true), + "never" => Some(false), + _ => None, + }), + drop_target_size: None, + // workbench.editor.limit contains "enabled", "value", and "perEditorGroup" + // our semantics match if those are set to true, some N, and true respectively. + // we'll ignore "perEditorGroup" for now since we only support a global max + max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) { + self.read_usize("workbench.editor.limit.value") + .and_then(|n| NonZeroUsize::new(n)) + } else { + None + }, + on_last_window_closed: None, + pane_split_direction_horizontal: None, + pane_split_direction_vertical: None, + resize_all_panels_in_dock: None, + restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"), + restore_on_startup: None, + show_call_status_icon: None, + use_system_path_prompts: self.read_bool("files.simpleDialog.enable"), + use_system_prompts: None, + use_system_window_tabs: self.read_bool("window.nativeTabs"), + when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| { + if b { + CloseWindowWhenNoItems::CloseWindow + } else { + CloseWindowWhenNoItems::KeepWindowOpen + } + }), + zoomed_padding: None, + } + } + + fn active_pane_modifiers(&self) -> Option { + if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true) + && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity") + { + Some(ActivePaneModifiers { + border_size: None, + inactive_opacity: Some(opacity), + }) + } else { + None + } + } + + fn worktree_settings_content(&self) -> WorktreeSettingsContent { + WorktreeSettingsContent { + project_name: None, + file_scan_exclusions: self + .read_value("files.watcherExclude") + .and_then(|v| v.as_array()) + .map(|v| { + v.iter() + .filter_map(|n| n.as_str().map(str::to_owned)) + .collect::>() + }) + .filter(|r| !r.is_empty()), + file_scan_inclusions: self + .read_value("files.watcherInclude") + .and_then(|v| v.as_array()) + .map(|v| { + v.iter() + .filter_map(|n| n.as_str().map(str::to_owned)) + .collect::>() + }) + .filter(|r| !r.is_empty()), + private_files: None, + } + } +} + +fn skip_default(value: T) -> Option { + if value == T::default() { + None + } else { + Some(value) + } } diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 27cccea126fecd7d015b21cec6d18809b756bdf8..9bb5ffb517b15225eed711a6d4e31e2977626d0a 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -8,9 +8,8 @@ use serde::{Deserialize, Serialize}; pub use settings::AlternateScroll; use settings::{ - CursorShapeContent, SettingsContent, ShowScrollbar, TerminalBlink, TerminalDockPosition, - TerminalLineHeight, TerminalSettingsContent, VenvSettings, WorkingDirectory, - merge_from::MergeFrom, + ShowScrollbar, TerminalBlink, TerminalDockPosition, TerminalLineHeight, VenvSettings, + WorkingDirectory, merge_from::MergeFrom, }; use task::Shell; use theme::FontFamilyName; @@ -116,81 +115,6 @@ impl settings::Settings for TerminalSettings { minimum_contrast: user_content.minimum_contrast.unwrap(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, content: &mut SettingsContent) { - let mut default = TerminalSettingsContent::default(); - let current = content.terminal.as_mut().unwrap_or(&mut default); - let name = |s| format!("terminal.integrated.{s}"); - - vscode.f32_setting(&name("fontSize"), &mut current.font_size); - vscode.font_family_setting( - &name("fontFamily"), - &mut current.font_family, - &mut current.font_fallbacks, - ); - vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select); - vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta); - vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines); - match vscode.read_bool(&name("cursorBlinking")) { - Some(true) => current.blinking = Some(TerminalBlink::On), - Some(false) => current.blinking = Some(TerminalBlink::Off), - None => {} - } - vscode.enum_setting( - &name("cursorStyle"), - &mut current.cursor_shape, - |s| match s { - "block" => Some(CursorShapeContent::Block), - "line" => Some(CursorShapeContent::Bar), - "underline" => Some(CursorShapeContent::Underline), - _ => None, - }, - ); - // they also have "none" and "outline" as options but just for the "Inactive" variant - if let Some(height) = vscode - .read_value(&name("lineHeight")) - .and_then(|v| v.as_f64()) - { - current.line_height = Some(TerminalLineHeight::Custom(height as f32)) - } - - #[cfg(target_os = "windows")] - let platform = "windows"; - #[cfg(target_os = "linux")] - let platform = "linux"; - #[cfg(target_os = "macos")] - let platform = "osx"; - #[cfg(target_os = "freebsd")] - let platform = "freebsd"; - - // TODO: handle arguments - let shell_name = format!("{platform}Exec"); - if let Some(s) = vscode.read_string(&name(&shell_name)) { - current.project.shell = Some(settings::Shell::Program(s.to_owned())) - } - - if let Some(env) = vscode - .read_value(&name(&format!("env.{platform}"))) - .and_then(|v| v.as_object()) - { - for (k, v) in env { - if v.is_null() - && let Some(zed_env) = current.project.env.as_mut() - { - zed_env.remove(k); - } - let Some(v) = v.as_str() else { continue }; - if let Some(zed_env) = current.project.env.as_mut() { - zed_env.insert(k.clone(), v.to_owned()); - } else { - current.project.env = Some([(k.clone(), v.to_owned())].into_iter().collect()) - } - } - } - if content.terminal.is_none() && default != TerminalSettingsContent::default() { - content.terminal = Some(default) - } - } } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 9f753d5a034466631d2324e52fbad7bd858e8c5c..3ac0f410efbdb4418236959e06d1b6772f7e3684 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -727,15 +727,4 @@ impl settings::Settings for ThemeSettings { unnecessary_code_fade: content.unnecessary_code_fade.unwrap().0.clamp(0.0, 0.9), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - vscode.from_f32_setting("editor.fontWeight", &mut current.theme.buffer_font_weight); - vscode.from_f32_setting("editor.fontSize", &mut current.theme.buffer_font_size); - vscode.font_family_setting( - "editor.fontFamily", - &mut current.theme.buffer_font_family, - &mut current.theme.buffer_font_fallbacks, - ) - // TODO: possibly map editor.fontLigatures to buffer_font_features? - } } diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs index d9495c556646f9b9f12dc0b52b9530796a5ad5e3..4caa95b2b412755bd4663a024197c074cb0f1b51 100644 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ b/crates/vim_mode_setting/src/vim_mode_setting.rs @@ -19,10 +19,6 @@ impl Settings for VimModeSetting { fn from_settings(content: &SettingsContent) -> Self { Self(content.vim_mode.unwrap()) } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _content: &mut SettingsContent) { - // TODO: could possibly check if any of the `vim.` keys are set? - } } pub struct HelixModeSetting(pub bool); @@ -31,6 +27,4 @@ impl Settings for HelixModeSetting { fn from_settings(content: &SettingsContent) -> Self { Self(content.helix_mode.unwrap()) } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut SettingsContent) {} } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index f868547dbf1da85bce8cf90c4bca266f941f78d9..1a6a09c38d0aea0c2df59947455628ec4a7ccd43 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -76,40 +76,6 @@ impl Settings for ItemSettings { show_close_button: tabs.show_close_button.unwrap(), } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - if let Some(b) = vscode.read_bool("workbench.editor.tabActionCloseVisibility") { - current.tabs.get_or_insert_default().show_close_button = Some(if b { - ShowCloseButton::Always - } else { - ShowCloseButton::Hidden - }) - } - if let Some(s) = vscode.read_enum("workbench.editor.tabActionLocation", |s| match s { - "right" => Some(ClosePosition::Right), - "left" => Some(ClosePosition::Left), - _ => None, - }) { - current.tabs.get_or_insert_default().close_position = Some(s) - } - if let Some(b) = vscode.read_bool("workbench.editor.focusRecentEditorAfterClose") { - current.tabs.get_or_insert_default().activate_on_close = Some(if b { - ActivateOnClose::History - } else { - ActivateOnClose::LeftNeighbour - }) - } - - if let Some(b) = vscode.read_bool("workbench.editor.showIcons") { - current.tabs.get_or_insert_default().file_icons = Some(b); - }; - if let Some(b) = vscode.read_bool("git.decorations.enabled") { - current.tabs.get_or_insert_default().git_status = Some(b); - } - } } impl Settings for PreviewTabsSettings { @@ -123,31 +89,6 @@ impl Settings for PreviewTabsSettings { .unwrap(), } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - if let Some(enabled) = vscode.read_bool("workbench.editor.enablePreview") { - current.preview_tabs.get_or_insert_default().enabled = Some(enabled); - } - if let Some(enable_preview_from_code_navigation) = - vscode.read_bool("workbench.editor.enablePreviewFromCodeNavigation") - { - current - .preview_tabs - .get_or_insert_default() - .enable_preview_from_code_navigation = Some(enable_preview_from_code_navigation) - } - if let Some(enable_preview_from_file_finder) = - vscode.read_bool("workbench.editor.enablePreviewFromQuickOpen") - { - current - .preview_tabs - .get_or_insert_default() - .enable_preview_from_file_finder = Some(enable_preview_from_file_finder) - } - } } #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 541194b0044dd897723c89763abc7d3a2abc20f3..ffa6767427d150d59b7f9f66575e3e385cca9564 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -108,91 +108,6 @@ impl Settings for WorkspaceSettings { zoomed_padding: workspace.zoomed_padding.unwrap(), } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - if vscode - .read_bool("accessibility.dimUnfocused.enabled") - .unwrap_or_default() - && let Some(opacity) = vscode - .read_value("accessibility.dimUnfocused.opacity") - .and_then(|v| v.as_f64()) - { - current - .workspace - .active_pane_modifiers - .get_or_insert_default() - .inactive_opacity = Some(opacity as f32); - } - - vscode.enum_setting( - "window.confirmBeforeClose", - &mut current.workspace.confirm_quit, - |s| match s { - "always" | "keyboardOnly" => Some(true), - "never" => Some(false), - _ => None, - }, - ); - - vscode.bool_setting( - "workbench.editor.restoreViewState", - &mut current.workspace.restore_on_file_reopen, - ); - - if let Some(b) = vscode.read_bool("window.closeWhenEmpty") { - current.workspace.when_closing_with_no_tabs = Some(if b { - settings::CloseWindowWhenNoItems::CloseWindow - } else { - settings::CloseWindowWhenNoItems::KeepWindowOpen - }); - } - - if let Some(b) = vscode.read_bool("files.simpleDialog.enable") { - current.workspace.use_system_path_prompts = Some(!b); - } - - if let Some(v) = vscode.read_enum("files.autoSave", |s| match s { - "off" => Some(AutosaveSetting::Off), - "afterDelay" => Some(AutosaveSetting::AfterDelay { - milliseconds: vscode - .read_value("files.autoSaveDelay") - .and_then(|v| v.as_u64()) - .unwrap_or(1000), - }), - "onFocusChange" => Some(AutosaveSetting::OnFocusChange), - "onWindowChange" => Some(AutosaveSetting::OnWindowChange), - _ => None, - }) { - current.workspace.autosave = Some(v); - } - - // workbench.editor.limit contains "enabled", "value", and "perEditorGroup" - // our semantics match if those are set to true, some N, and true respectively. - // we'll ignore "perEditorGroup" for now since we only support a global max - if let Some(n) = vscode - .read_value("workbench.editor.limit.value") - .and_then(|v| v.as_u64()) - .and_then(|n| NonZeroUsize::new(n as usize)) - && vscode - .read_bool("workbench.editor.limit.enabled") - .unwrap_or_default() - { - current.workspace.max_tabs = Some(n) - } - - if let Some(b) = vscode.read_bool("window.nativeTabs") { - current.workspace.use_system_window_tabs = Some(b); - } - - // some combination of "window.restoreWindows" and "workbench.startupEditor" might - // map to our "restore_on_startup" - - // there doesn't seem to be a way to read whether the bottom dock's "justified" - // setting is enabled in vscode. that'd be our equivalent to "bottom_dock_layout" - } } impl Settings for TabBarSettings { @@ -204,22 +119,6 @@ impl Settings for TabBarSettings { show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(), } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - if let Some(b) = vscode.read_enum("workbench.editor.showTabs", |s| match s { - "multiple" => Some(true), - "single" | "none" => Some(false), - _ => None, - }) { - current.tab_bar.get_or_insert_default().show = Some(b); - } - if Some("hidden") == vscode.read_string("workbench.editor.editorActionsLocation") { - current.tab_bar.get_or_insert_default().show_tab_bar_buttons = Some(false) - } - } } #[derive(Deserialize)] @@ -238,13 +137,4 @@ impl Settings for StatusBarSettings { cursor_position_button: status_bar.cursor_position_button.unwrap(), } } - - fn import_from_vscode( - vscode: &settings::VsCodeSettings, - current: &mut settings::SettingsContent, - ) { - if let Some(show) = vscode.read_bool("workbench.statusBar.visible") { - current.status_bar.get_or_insert_default().show = Some(show); - } - } } diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index a9fcbf0909617986dd2d1d816ed513dd281f2940..8e432f8affbbfa9e7eb53ec474970c43ec0e8a94 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -1,7 +1,7 @@ use std::path::Path; use anyhow::Context as _; -use settings::{Settings, SettingsContent}; +use settings::Settings; use util::{ ResultExt, paths::{PathMatcher, PathStyle}, @@ -64,31 +64,6 @@ impl Settings for WorktreeSettings { .unwrap_or_default(), } } - - fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut SettingsContent) { - if let Some(inclusions) = vscode - .read_value("files.watcherInclude") - .and_then(|v| v.as_array()) - .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect()) - { - if let Some(old) = current.project.worktree.file_scan_inclusions.as_mut() { - old.extend(inclusions) - } else { - current.project.worktree.file_scan_inclusions = Some(inclusions) - } - } - if let Some(exclusions) = vscode - .read_value("files.watcherExclude") - .and_then(|v| v.as_array()) - .and_then(|v| v.iter().map(|n| n.as_str().map(str::to_owned)).collect()) - { - if let Some(old) = current.project.worktree.file_scan_exclusions.as_mut() { - old.extend(exclusions) - } else { - current.project.worktree.file_scan_exclusions = Some(exclusions) - } - } - } } fn path_matchers(mut values: Vec, context: &'static str) -> anyhow::Result { diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs index 1f695aa8ff5f8eb09d4cc0c2ae04282c469fb29c..abbce9a98c3106de0093a8586313fbda9750b12b 100644 --- a/crates/zlog_settings/src/zlog_settings.rs +++ b/crates/zlog_settings/src/zlog_settings.rs @@ -29,6 +29,4 @@ impl Settings for ZlogSettings { scopes: content.log.clone().unwrap(), } } - - fn import_from_vscode(_: &settings::VsCodeSettings, _: &mut settings::SettingsContent) {} } From 27dcdb58416c24b29952487ae68225ea76ac58a8 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 17 Oct 2025 20:17:34 +0200 Subject: [PATCH 005/202] multi_buffer: Reduce `RefCell::borrow_mut` calls to the bare minimum (#40522) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/buffer_diff/src/buffer_diff.rs | 12 +- crates/clock/src/clock.rs | 10 +- crates/editor/src/editor.rs | 3 +- crates/editor/src/element.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 511 ++++++++---------- crates/multi_buffer/src/multi_buffer_tests.rs | 16 +- crates/project/src/git_store/conflict_set.rs | 6 +- crates/search/src/project_search.rs | 4 +- crates/text/src/anchor.rs | 12 +- 9 files changed, 282 insertions(+), 294 deletions(-) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 1787f616ad365175de352e3eeeede3e1749dede4..13479f6428b02d52f45415a989b694cc04ab5c25 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -118,11 +118,11 @@ impl sum_tree::Summary for DiffHunkSummary { } fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) { - self.buffer_range.start = self + self.buffer_range.start = *self .buffer_range .start .min(&other.buffer_range.start, buffer); - self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer); + self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer); } } @@ -1068,8 +1068,8 @@ impl BufferDiff { self.range_to_hunk_range(secondary_changed_range, buffer, cx) { if let Some(range) = &mut changed_range { - range.start = secondary_hunk_range.start.min(&range.start, buffer); - range.end = secondary_hunk_range.end.max(&range.end, buffer); + range.start = *secondary_hunk_range.start.min(&range.start, buffer); + range.end = *secondary_hunk_range.end.max(&range.end, buffer); } else { changed_range = Some(secondary_hunk_range); } @@ -1083,8 +1083,8 @@ impl BufferDiff { if let Some((first, last)) = state.pending_hunks.first().zip(state.pending_hunks.last()) { if let Some(range) = &mut changed_range { - range.start = range.start.min(&first.buffer_range.start, buffer); - range.end = range.end.max(&last.buffer_range.end, buffer); + range.start = *range.start.min(&first.buffer_range.start, buffer); + range.end = *range.end.max(&last.buffer_range.end, buffer); } else { changed_range = Some(first.buffer_range.start..last.buffer_range.end); } diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index b4f57116d273733d6c43a0a09c8a2a33ccb89b38..64645c9b46f68416c6792b17258baf8e49ca9585 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -206,7 +206,13 @@ impl Lamport { impl fmt::Debug for Lamport { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value) + if *self == Self::MAX { + write!(f, "Lamport {{MAX}}") + } else if *self == Self::MIN { + write!(f, "Lamport {{MIN}}") + } else { + write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value) + } } } @@ -219,6 +225,8 @@ impl fmt::Debug for Global { } if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID { write!(f, ": {}", timestamp.value)?; + } else if timestamp.replica_id == AGENT_REPLICA_ID { + write!(f, ": {}", timestamp.value)?; } else { write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?; } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 15af61f5d28336f77976ee3fadc783016cc283bd..5d881f221b238c35224f804c1b95094a2db957e8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6887,7 +6887,8 @@ impl Editor { continue; } - let range = Anchor::range_in_buffer(excerpt_id, buffer_id, start..end); + let range = + Anchor::range_in_buffer(excerpt_id, buffer_id, *start..*end); if highlight.kind == lsp::DocumentHighlightKind::WRITE { write_ranges.push(range); } else { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 290c8892d9c93ee805ee8106bab183921f616099..f71110ed95d13eba4577e53cf148e8d7efbc20c1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7465,7 +7465,7 @@ impl EditorElement { let clipped_start = range.start.max(&buffer_range.start, buffer); let clipped_end = range.end.min(&buffer_range.end, buffer); let range = buffer_snapshot - .anchor_range_in_excerpt(excerpt_id, clipped_start..clipped_end)?; + .anchor_range_in_excerpt(excerpt_id, *clipped_start..*clipped_end)?; let start = range.start.to_display_point(display_snapshot); let end = range.end.to_display_point(display_snapshot); let selection_layout = SelectionLayout { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index ac769dfdafcf79af51c1f3119453e89d39ca333a..18a619212dba49bf1c384679ec90120af80e0e2a 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -64,17 +64,22 @@ pub struct MultiBuffer { /// Use [`MultiBuffer::snapshot`] to get a up-to-date snapshot. snapshot: RefCell, /// Contains the state of the buffers being edited - buffers: RefCell>, - // only used by consumers using `set_excerpts_for_buffer` + buffers: HashMap, + /// Mapping from path keys to their excerpts. excerpts_by_path: BTreeMap>, + /// Mapping from excerpt IDs to their path key. paths_by_excerpt: HashMap, + /// Mapping from buffer IDs to their diff states diffs: HashMap, - // all_diff_hunks_expanded: bool, subscriptions: Topic, /// If true, the multi-buffer only contains a single [`Buffer`] and a single [`Excerpt`] singleton: bool, + /// The history of the multi-buffer. history: History, + /// The explicit title of the multi-buffer. + /// If `None`, it will be derived from the underlying path or content. title: Option, + /// The writing capability of the multi-buffer. capability: Capability, buffer_changed_since_sync: Rc>, } @@ -249,8 +254,8 @@ pub trait ToPointUtf16: 'static + fmt::Debug { struct BufferState { buffer: Entity, - last_version: clock::Global, - last_non_text_state_update_count: usize, + last_version: RefCell, + last_non_text_state_update_count: Cell, excerpts: Vec, _subscriptions: [gpui::Subscription; 2], } @@ -282,15 +287,20 @@ impl DiffState { #[derive(Clone, Default)] pub struct MultiBufferSnapshot { singleton: bool, + /* mut */ excerpts: SumTree, + /* mut */ excerpt_ids: SumTree, diffs: TreeMap, diff_transforms: SumTree, + /* mut */ replaced_excerpts: TreeMap, + /* mut */ trailing_excerpt_update_count: usize, all_diff_hunks_expanded: bool, non_text_state_update_count: usize, edit_count: usize, + /* mut */ is_dirty: bool, has_deleted_file: bool, has_conflict: bool, @@ -612,12 +622,41 @@ impl IndentGuide { impl MultiBuffer { pub fn new(capability: Capability) -> Self { - Self { - snapshot: RefCell::new(MultiBufferSnapshot { + Self::new_( + capability, + MultiBufferSnapshot { show_headers: true, ..MultiBufferSnapshot::default() - }), - buffers: RefCell::default(), + }, + ) + } + + pub fn without_headers(capability: Capability) -> Self { + Self::new_(capability, Default::default()) + } + + pub fn singleton(buffer: Entity, cx: &mut Context) -> Self { + let mut this = Self::new_( + buffer.read(cx).capability(), + MultiBufferSnapshot { + singleton: true, + ..MultiBufferSnapshot::default() + }, + ); + this.singleton = true; + this.push_excerpts( + buffer, + [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], + cx, + ); + this + } + + #[inline] + pub fn new_(capability: Capability, snapshot: MultiBufferSnapshot) -> Self { + Self { + snapshot: RefCell::new(snapshot), + buffers: Default::default(), diffs: HashMap::default(), subscriptions: Topic::default(), singleton: false, @@ -636,32 +675,10 @@ impl MultiBuffer { } } - pub fn without_headers(capability: Capability) -> Self { - Self { - snapshot: Default::default(), - buffers: Default::default(), - excerpts_by_path: Default::default(), - paths_by_excerpt: Default::default(), - diffs: HashMap::default(), - subscriptions: Default::default(), - singleton: false, - capability, - buffer_changed_since_sync: Default::default(), - history: History { - next_transaction_id: Default::default(), - undo_stack: Default::default(), - redo_stack: Default::default(), - transaction_depth: 0, - group_interval: Duration::from_millis(300), - }, - title: Default::default(), - } - } - pub fn clone(&self, new_cx: &mut Context) -> Self { let mut buffers = HashMap::default(); let buffer_changed_since_sync = Rc::new(Cell::new(false)); - for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + for (buffer_id, buffer_state) in self.buffers.iter() { buffer_state.buffer.update(new_cx, |buffer, _| { buffer.record_changes(Rc::downgrade(&buffer_changed_since_sync)); }); @@ -670,7 +687,9 @@ impl MultiBuffer { BufferState { buffer: buffer_state.buffer.clone(), last_version: buffer_state.last_version.clone(), - last_non_text_state_update_count: buffer_state.last_non_text_state_update_count, + last_non_text_state_update_count: buffer_state + .last_non_text_state_update_count + .clone(), excerpts: buffer_state.excerpts.clone(), _subscriptions: [ new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), @@ -685,7 +704,7 @@ impl MultiBuffer { } Self { snapshot: RefCell::new(self.snapshot.borrow().clone()), - buffers: RefCell::new(buffers), + buffers: buffers, excerpts_by_path: Default::default(), paths_by_excerpt: Default::default(), diffs: diff_bases, @@ -707,18 +726,6 @@ impl MultiBuffer { self.capability == Capability::ReadOnly } - pub fn singleton(buffer: Entity, cx: &mut Context) -> Self { - let mut this = Self::new(buffer.read(cx).capability()); - this.singleton = true; - this.push_excerpts( - buffer, - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - this.snapshot.borrow_mut().singleton = true; - this - } - /// Returns an up-to-date snapshot of the MultiBuffer. pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot { self.sync(cx); @@ -732,15 +739,7 @@ impl MultiBuffer { pub fn as_singleton(&self) -> Option> { if self.singleton { - Some( - self.buffers - .borrow() - .values() - .next() - .unwrap() - .buffer - .clone(), - ) + Some(self.buffers.values().next().unwrap().buffer.clone()) } else { None } @@ -773,7 +772,7 @@ impl MultiBuffer { } pub fn is_empty(&self) -> bool { - self.buffers.borrow().is_empty() + self.buffers.is_empty() } pub fn symbols_containing( @@ -817,7 +816,7 @@ impl MultiBuffer { mut autoindent_mode: Option, cx: &mut Context, ) { - if this.read_only() || this.buffers.borrow().is_empty() { + if this.read_only() || this.buffers.is_empty() { return; } @@ -836,78 +835,74 @@ impl MultiBuffer { for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); - this.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = Arc::default(); + this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { + let mut edits = edits.into_iter().peekable(); + let mut insertions = Vec::new(); + let mut original_indent_columns = Vec::new(); + let mut deletions = Vec::new(); + let empty_str: Arc = Arc::default(); + while let Some(BufferEdit { + mut range, + mut new_text, + mut is_insertion, + original_indent_column, + excerpt_id, + }) = edits.next() + { while let Some(BufferEdit { - mut range, - mut new_text, - mut is_insertion, - original_indent_column, - excerpt_id, - }) = edits.next() + range: next_range, + is_insertion: next_is_insertion, + new_text: next_new_text, + excerpt_id: next_excerpt_id, + .. + }) = edits.peek() { - while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - new_text: next_new_text, - excerpt_id: next_excerpt_id, - .. - }) = edits.peek() - { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - if excerpt_id == *next_excerpt_id { - new_text = format!("{new_text}{next_new_text}").into(); - } - edits.next(); - } else { - break; + if range.end >= next_range.start { + range.end = cmp::max(next_range.end, range.end); + is_insertion |= *next_is_insertion; + if excerpt_id == *next_excerpt_id { + new_text = format!("{new_text}{next_new_text}").into(); } + edits.next(); + } else { + break; } + } - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start) - ..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start) - ..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } + if is_insertion { + original_indent_columns.push(original_indent_column); + insertions.push(( + buffer.anchor_before(range.start)..buffer.anchor_before(range.end), + new_text.clone(), + )); + } else if !range.is_empty() { + deletions.push(( + buffer.anchor_before(range.start)..buffer.anchor_before(range.end), + empty_str.clone(), + )); } + } - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - autoindent_mode.clone() - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - autoindent_mode.clone() - }; + let deletion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns: Default::default(), + }) + } else { + autoindent_mode.clone() + }; + let insertion_autoindent_mode = + if let Some(AutoindentMode::Block { .. }) = autoindent_mode { + Some(AutoindentMode::Block { + original_indent_columns, + }) + } else { + autoindent_mode.clone() + }; - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); - }) + buffer.edit(deletions, deletion_autoindent_mode, cx); + buffer.edit(insertions, insertion_autoindent_mode, cx); + }) } cx.emit(Event::ExcerptsEdited { @@ -1064,7 +1059,7 @@ impl MultiBuffer { edits: Vec<(Range, Arc)>, cx: &mut Context, ) { - if this.read_only() || this.buffers.borrow().is_empty() { + if this.read_only() || this.buffers.is_empty() { return; } @@ -1088,11 +1083,9 @@ impl MultiBuffer { ranges.push(edit.range); } - this.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - buffer.autoindent_ranges(ranges, cx); - }) + this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { + buffer.autoindent_ranges(ranges, cx); + }) } cx.emit(Event::ExcerptsEdited { @@ -1135,7 +1128,7 @@ impl MultiBuffer { return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); } - for BufferState { buffer, .. } in self.buffers.borrow().values() { + for BufferState { buffer, .. } in self.buffers.values() { buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); } self.history.start_transaction(now) @@ -1167,7 +1160,7 @@ impl MultiBuffer { } let mut buffer_transactions = HashMap::default(); - for BufferState { buffer, .. } in self.buffers.borrow().values() { + for BufferState { buffer, .. } in self.buffers.values() { if let Some(transaction_id) = buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) { @@ -1197,11 +1190,10 @@ impl MultiBuffer { let mut ranges = Vec::new(); let snapshot = self.read(cx); - let buffers = self.buffers.borrow(); let mut cursor = snapshot.excerpts.cursor::(()); for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { - let Some(buffer_state) = buffers.get(buffer_id) else { + let Some(buffer_state) = self.buffers.get(buffer_id) else { continue; }; @@ -1254,7 +1246,7 @@ impl MultiBuffer { if let Some(destination_buffer_transaction_id) = destination.buffer_transactions.get(&buffer_id) { - if let Some(state) = self.buffers.borrow().get(&buffer_id) { + if let Some(state) = self.buffers.get(&buffer_id) { state.buffer.update(cx, |buffer, _| { buffer.merge_transactions( buffer_transaction_id, @@ -1273,7 +1265,7 @@ impl MultiBuffer { pub fn finalize_last_transaction(&mut self, cx: &mut Context) { self.history.finalize_last_transaction(); - for BufferState { buffer, .. } in self.buffers.borrow().values() { + for BufferState { buffer, .. } in self.buffers.values() { buffer.update(cx, |buffer, _| { buffer.finalize_last_transaction(); }); @@ -1345,7 +1337,7 @@ impl MultiBuffer { } } - for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + for (buffer_id, buffer_state) in self.buffers.iter() { if !selections_by_buffer.contains_key(buffer_id) { buffer_state .buffer @@ -1354,32 +1346,30 @@ impl MultiBuffer { } for (buffer_id, mut selections) in selections_by_buffer { - self.buffers.borrow()[&buffer_id] - .buffer - .update(cx, |buffer, cx| { - selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); - let mut selections = selections.into_iter().peekable(); - let merged_selections = Arc::from_iter(iter::from_fn(|| { - let mut selection = selections.next()?; - while let Some(next_selection) = selections.peek() { - if selection.end.cmp(&next_selection.start, buffer).is_ge() { - let next_selection = selections.next().unwrap(); - if next_selection.end.cmp(&selection.end, buffer).is_ge() { - selection.end = next_selection.end; - } - } else { - break; + self.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { + selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); + let mut selections = selections.into_iter().peekable(); + let merged_selections = Arc::from_iter(iter::from_fn(|| { + let mut selection = selections.next()?; + while let Some(next_selection) = selections.peek() { + if selection.end.cmp(&next_selection.start, buffer).is_ge() { + let next_selection = selections.next().unwrap(); + if next_selection.end.cmp(&selection.end, buffer).is_ge() { + selection.end = next_selection.end; } + } else { + break; } - Some(selection) - })); - buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); - }); + } + Some(selection) + })); + buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); + }); } } pub fn remove_active_selections(&self, cx: &mut Context) { - for buffer in self.buffers.borrow().values() { + for buffer in self.buffers.values() { buffer .buffer .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); @@ -1394,7 +1384,7 @@ impl MultiBuffer { while let Some(transaction) = self.history.pop_undo() { let mut undone = false; for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { undone |= buffer.update(cx, |buffer, cx| { let undo_to = *buffer_transaction_id; if let Some(entry) = buffer.peek_undo_stack() { @@ -1427,7 +1417,7 @@ impl MultiBuffer { while let Some(transaction) = self.history.pop_redo() { let mut redone = false; for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { redone |= buffer.update(cx, |buffer, cx| { let redo_to = *buffer_transaction_id; if let Some(entry) = buffer.peek_redo_stack() { @@ -1451,7 +1441,7 @@ impl MultiBuffer { buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { for (buffer_id, transaction_id) in &transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { buffer.update(cx, |buffer, cx| { buffer.undo_transaction(*transaction_id, cx) }); @@ -1467,7 +1457,7 @@ impl MultiBuffer { }); } else if let Some(transaction) = self.history.forget(transaction_id) { for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(state) = self.buffers.borrow_mut().get_mut(&buffer_id) { + if let Some(state) = self.buffers.get_mut(&buffer_id) { state.buffer.update(cx, |buffer, _| { buffer.forget_transaction(buffer_transaction_id); }); @@ -1571,12 +1561,7 @@ impl MultiBuffer { continue; }; - let Some(buffer) = self - .buffers - .borrow() - .get(buffer_id) - .map(|b| b.buffer.clone()) - else { + let Some(buffer) = self.buffers.get(buffer_id).map(|b| b.buffer.clone()) else { continue; }; @@ -1631,13 +1616,13 @@ impl MultiBuffer { pub fn set_anchored_excerpts_for_path( &self, + path_key: PathKey, buffer: Entity, ranges: Vec>, context_line_count: u32, cx: &mut Context, ) -> Task>> { let buffer_snapshot = buffer.read(cx).snapshot(); - let path_key = PathKey::for_buffer(&buffer, cx); cx.spawn(async move |multi_buffer, cx| { let snapshot = buffer_snapshot.clone(); let (excerpt_ranges, new, counts) = cx @@ -1802,7 +1787,7 @@ impl MultiBuffer { last.context.end = last.context.end.max(existing_range.end); to_remove.push(*existing_id); self.snapshot - .borrow_mut() + .get_mut() .replaced_excerpts .insert(*existing_id, *last_id); existing_iter.next(); @@ -1852,7 +1837,7 @@ impl MultiBuffer { let existing_id = existing_iter.next().unwrap(); let new_id = next_excerpt_id(); self.snapshot - .borrow_mut() + .get_mut() .replaced_excerpts .insert(existing_id, new_id); to_remove.push(existing_id); @@ -1941,15 +1926,16 @@ impl MultiBuffer { let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_id = buffer_snapshot.remote_id(); - let mut buffers = self.buffers.borrow_mut(); - let buffer_state = buffers.entry(buffer_id).or_insert_with(|| { + let buffer_state = self.buffers.entry(buffer_id).or_insert_with(|| { self.buffer_changed_since_sync.replace(true); buffer.update(cx, |buffer, _| { buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync)); }); BufferState { - last_version: buffer_snapshot.version().clone(), - last_non_text_state_update_count: buffer_snapshot.non_text_state_update_count(), + last_version: RefCell::new(buffer_snapshot.version().clone()), + last_non_text_state_update_count: Cell::new( + buffer_snapshot.non_text_state_update_count(), + ), excerpts: Default::default(), _subscriptions: [ cx.observe(&buffer, |_, _, cx| cx.notify()), @@ -1959,7 +1945,7 @@ impl MultiBuffer { } }); - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); @@ -2023,7 +2009,7 @@ impl MultiBuffer { snapshot.trailing_excerpt_update_count += 1; } - self.sync_diff_transforms( + let edits = Self::sync_diff_transforms( &mut snapshot, vec![Edit { old: edit_start..edit_start, @@ -2031,6 +2017,10 @@ impl MultiBuffer { }], DiffChangeKind::BufferEdited, ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } + cx.emit(Event::Edited { edited_buffer: None, }); @@ -2045,15 +2035,10 @@ impl MultiBuffer { pub fn clear(&mut self, cx: &mut Context) { self.sync(cx); let ids = self.excerpt_ids(); - let removed_buffer_ids = self - .buffers - .borrow_mut() - .drain() - .map(|(id, _)| id) - .collect(); + let removed_buffer_ids = self.buffers.drain().map(|(id, _)| id).collect(); self.excerpts_by_path.clear(); self.paths_by_excerpt.clear(); - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let start = ExcerptOffset::new(0); let prev_len = ExcerptOffset::new(snapshot.excerpts.summary().text.len); snapshot.excerpts = Default::default(); @@ -2063,7 +2048,7 @@ impl MultiBuffer { snapshot.has_conflict = false; snapshot.replaced_excerpts.clear(); - self.sync_diff_transforms( + let edits = Self::sync_diff_transforms( &mut snapshot, vec![Edit { old: start..prev_len, @@ -2071,6 +2056,9 @@ impl MultiBuffer { }], DiffChangeKind::BufferEdited, ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2088,9 +2076,8 @@ impl MultiBuffer { ) -> Vec<(ExcerptId, ExcerptRange)> { let mut excerpts = Vec::new(); let snapshot = self.read(cx); - let buffers = self.buffers.borrow(); let mut cursor = snapshot.excerpts.cursor::>(()); - if let Some(locators) = buffers.get(&buffer_id).map(|state| &state.excerpts) { + if let Some(locators) = self.buffers.get(&buffer_id).map(|state| &state.excerpts) { for locator in locators { cursor.seek_forward(&Some(locator), Bias::Left); if let Some(excerpt) = cursor.item() @@ -2106,7 +2093,6 @@ impl MultiBuffer { pub fn excerpt_ranges_for_buffer(&self, buffer_id: BufferId, cx: &App) -> Vec> { let snapshot = self.read(cx); - let buffers = self.buffers.borrow(); let mut excerpts = snapshot .excerpts .cursor::, ExcerptDimension>>(()); @@ -2114,7 +2100,8 @@ impl MultiBuffer { .diff_transforms .cursor::, OutputDimension>>(()); diff_transforms.next(); - let locators = buffers + let locators = self + .buffers .get(&buffer_id) .into_iter() .flat_map(|state| &state.excerpts); @@ -2175,12 +2162,7 @@ impl MultiBuffer { .map(|excerpt| { ( excerpt.id, - self.buffers - .borrow() - .get(&excerpt.buffer_id) - .unwrap() - .buffer - .clone(), + self.buffers.get(&excerpt.buffer_id).unwrap().buffer.clone(), excerpt.range.context.clone(), ) }) @@ -2204,11 +2186,7 @@ impl MultiBuffer { let snapshot = self.read(cx); let (buffer, offset) = snapshot.point_to_buffer_offset(point)?; Some(( - self.buffers - .borrow() - .get(&buffer.remote_id())? - .buffer - .clone(), + self.buffers.get(&buffer.remote_id())?.buffer.clone(), offset, )) } @@ -2223,11 +2201,7 @@ impl MultiBuffer { let (buffer, point, is_main_buffer) = snapshot.point_to_buffer_point(point.to_point(&snapshot))?; Some(( - self.buffers - .borrow() - .get(&buffer.remote_id())? - .buffer - .clone(), + self.buffers.get(&buffer.remote_id())?.buffer.clone(), point, is_main_buffer, )) @@ -2273,8 +2247,7 @@ impl MultiBuffer { return; } - let mut buffers = self.buffers.borrow_mut(); - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let mut new_excerpts = SumTree::default(); let mut cursor = snapshot .excerpts @@ -2297,14 +2270,14 @@ impl MultiBuffer { // Skip over the removed excerpt. 'remove_excerpts: loop { - if let Some(buffer_state) = buffers.get_mut(&excerpt.buffer_id) { + if let Some(buffer_state) = self.buffers.get_mut(&excerpt.buffer_id) { buffer_state.excerpts.retain(|l| l != &excerpt.locator); if buffer_state.excerpts.is_empty() { log::debug!( "removing buffer and diff for buffer {}", excerpt.buffer_id ); - buffers.remove(&excerpt.buffer_id); + self.buffers.remove(&excerpt.buffer_id); removed_buffer_ids.push(excerpt.buffer_id); } } @@ -2355,7 +2328,10 @@ impl MultiBuffer { snapshot.trailing_excerpt_update_count += 1; } - self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } self.buffer_changed_since_sync.replace(true); cx.emit(Event::Edited { edited_buffer: None, @@ -2372,12 +2348,11 @@ impl MultiBuffer { anchors: Anchors, cx: &mut Context, ) -> impl 'static + Future> + use { - let borrow = self.buffers.borrow(); let mut error = None; let mut futures = Vec::new(); for anchor in anchors { if let Some(buffer_id) = anchor.buffer_id { - if let Some(buffer) = borrow.get(&buffer_id) { + if let Some(buffer) = self.buffers.get(&buffer_id) { buffer.buffer.update(cx, |buffer, _| { futures.push(buffer.wait_for_anchors([anchor.text_anchor])) }); @@ -2407,12 +2382,7 @@ impl MultiBuffer { ) -> Option<(Entity, language::Anchor)> { let snapshot = self.read(cx); let anchor = snapshot.anchor_before(position); - let buffer = self - .buffers - .borrow() - .get(&anchor.buffer_id?)? - .buffer - .clone(); + let buffer = self.buffers.get(&anchor.buffer_id?)?.buffer.clone(); Some((buffer, anchor.text_anchor)) } @@ -2444,7 +2414,7 @@ impl MultiBuffer { fn buffer_diff_language_changed(&mut self, diff: Entity, cx: &mut Context) { self.sync(cx); - let mut snapshot = self.snapshot.borrow_mut(); + let snapshot = self.snapshot.get_mut(); let diff = diff.read(cx); let buffer_id = diff.buffer_id; let diff = diff.snapshot(cx); @@ -2462,8 +2432,7 @@ impl MultiBuffer { let diff = diff.read(cx); let buffer_id = diff.buffer_id; - let buffers = self.buffers.borrow(); - let Some(buffer_state) = buffers.get(&buffer_id) else { + let Some(buffer_state) = self.buffers.get(&buffer_id) else { return; }; @@ -2471,7 +2440,7 @@ impl MultiBuffer { let diff_change_range = range.to_offset(buffer); let new_diff = diff.snapshot(cx); - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let base_text_changed = snapshot .diffs .get(&buffer_id) @@ -2515,13 +2484,16 @@ impl MultiBuffer { } } - self.sync_diff_transforms( + let edits = Self::sync_diff_transforms( &mut snapshot, excerpt_edits, DiffChangeKind::DiffUpdated { base_changed: base_text_changed, }, ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2529,19 +2501,17 @@ impl MultiBuffer { pub fn all_buffers(&self) -> HashSet> { self.buffers - .borrow() .values() .map(|state| state.buffer.clone()) .collect() } pub fn all_buffer_ids(&self) -> Vec { - self.buffers.borrow().keys().copied().collect() + self.buffers.keys().copied().collect() } pub fn buffer(&self, buffer_id: BufferId) -> Option> { self.buffers - .borrow() .get(&buffer_id) .map(|state| state.buffer.clone()) } @@ -2583,10 +2553,7 @@ impl MultiBuffer { } pub fn for_each_buffer(&self, mut f: impl FnMut(&Entity)) { - self.buffers - .borrow() - .values() - .for_each(|state| f(&state.buffer)) + self.buffers.values().for_each(|state| f(&state.buffer)) } pub fn title<'a>(&'a self, cx: &'a App) -> Cow<'a, str> { @@ -2655,7 +2622,7 @@ impl MultiBuffer { /// Preserve preview tabs containing this multibuffer until additional edits occur. pub fn refresh_preview(&self, cx: &mut Context) { - for buffer_state in self.buffers.borrow().values() { + for buffer_state in self.buffers.values() { buffer_state .buffer .update(cx, |buffer, _cx| buffer.refresh_preview()); @@ -2665,7 +2632,6 @@ impl MultiBuffer { /// Whether we should preserve the preview status of a tab containing this multi-buffer. pub fn preserve_preview(&self, cx: &App) -> bool { self.buffers - .borrow() .values() .all(|state| state.buffer.read(cx).preserve_preview()) } @@ -2694,7 +2660,7 @@ impl MultiBuffer { } pub fn set_all_diff_hunks_expanded(&mut self, cx: &mut Context) { - self.snapshot.borrow_mut().all_diff_hunks_expanded = true; + self.snapshot.get_mut().all_diff_hunks_expanded = true; self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); } @@ -2703,7 +2669,7 @@ impl MultiBuffer { } pub fn set_all_diff_hunks_collapsed(&mut self, cx: &mut Context) { - self.snapshot.borrow_mut().all_diff_hunks_expanded = false; + self.snapshot.get_mut().all_diff_hunks_expanded = false; self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], false, cx); } @@ -2764,7 +2730,7 @@ impl MultiBuffer { return; } self.sync(cx); - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let mut excerpt_edits = Vec::new(); let mut last_hunk_row = None; for (range, end_excerpt_id) in ranges { @@ -2795,11 +2761,14 @@ impl MultiBuffer { } } - self.sync_diff_transforms( + let edits = Self::sync_diff_transforms( &mut snapshot, excerpt_edits, DiffChangeKind::ExpandOrCollapseHunks { expand }, ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } cx.emit(Event::DiffHunksToggled); cx.emit(Event::Edited { edited_buffer: None, @@ -2833,7 +2802,7 @@ impl MultiBuffer { ) { self.sync(cx); - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let locator = snapshot.excerpt_locator_for_id(id); let mut new_excerpts = SumTree::default(); let mut cursor = snapshot @@ -2883,7 +2852,10 @@ impl MultiBuffer { drop(cursor); snapshot.excerpts = new_excerpts; - self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } cx.emit(Event::Edited { edited_buffer: None, }); @@ -2906,7 +2878,7 @@ impl MultiBuffer { self.expand_excerpts_with_paths(ids, line_count, direction, cx); return; } - let mut snapshot = self.snapshot.borrow_mut(); + let mut snapshot = self.snapshot.get_mut(); let ids = ids.into_iter().collect::>(); let locators = snapshot.excerpt_locators_for_ids(ids.iter().copied()); @@ -2987,7 +2959,10 @@ impl MultiBuffer { drop(cursor); snapshot.excerpts = new_excerpts; - self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } cx.emit(Event::Edited { edited_buffer: None, }); @@ -3008,18 +2983,19 @@ impl MultiBuffer { let mut has_deleted_file = false; let mut has_conflict = false; let mut edited = false; - let mut buffers = self.buffers.borrow_mut(); - for buffer_state in buffers.values_mut() { + for buffer_state in self.buffers.values() { let buffer = buffer_state.buffer.read(cx); let version = buffer.version(); let non_text_state_update_count = buffer.non_text_state_update_count(); - let buffer_edited = version.changed_since(&buffer_state.last_version); + let buffer_edited = version.changed_since(&buffer_state.last_version.borrow()); let buffer_non_text_state_updated = - non_text_state_update_count > buffer_state.last_non_text_state_update_count; + non_text_state_update_count > buffer_state.last_non_text_state_update_count.get(); if buffer_edited || buffer_non_text_state_updated { - buffer_state.last_version = version; - buffer_state.last_non_text_state_update_count = non_text_state_update_count; + *buffer_state.last_version.borrow_mut() = version; + buffer_state + .last_non_text_state_update_count + .set(non_text_state_update_count); excerpts_to_edit.extend( buffer_state .excerpts @@ -3110,17 +3086,19 @@ impl MultiBuffer { drop(cursor); snapshot.excerpts = new_excerpts; - self.sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } } fn sync_diff_transforms( - &self, snapshot: &mut MultiBufferSnapshot, excerpt_edits: Vec>, change_kind: DiffChangeKind, - ) { + ) -> Vec> { if excerpt_edits.is_empty() { - return; + return vec![]; } let mut excerpts = snapshot.excerpts.cursor::(()); @@ -3145,12 +3123,12 @@ impl MultiBuffer { if at_transform_boundary { at_transform_boundary = false; let transforms_before_edit = old_diff_transforms.slice(&edit.old.start, Bias::Left); - self.append_diff_transforms(&mut new_diff_transforms, transforms_before_edit); + Self::append_diff_transforms(&mut new_diff_transforms, transforms_before_edit); if let Some(transform) = old_diff_transforms.item() && old_diff_transforms.end().0 == edit.old.start && old_diff_transforms.start().0 < edit.old.start { - self.push_diff_transform(&mut new_diff_transforms, transform.clone()); + Self::push_diff_transform(&mut new_diff_transforms, transform.clone()); old_diff_transforms.next(); } } @@ -3160,7 +3138,7 @@ impl MultiBuffer { let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; let edit_new_start = (edit_old_start as isize + output_delta) as usize; - let changed_diff_hunks = self.recompute_diff_transforms_for_edit( + let changed_diff_hunks = Self::recompute_diff_transforms_for_edit( &edit, &mut excerpts, &mut old_diff_transforms, @@ -3213,7 +3191,7 @@ impl MultiBuffer { } old_expanded_hunks.clear(); - self.push_buffer_content_transform( + Self::push_buffer_content_transform( snapshot, &mut new_diff_transforms, excerpt_offset, @@ -3224,7 +3202,7 @@ impl MultiBuffer { } // Keep any transforms that are after the last edit. - self.append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix()); + Self::append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix()); // Ensure there's always at least one buffer content transform. if new_diff_transforms.is_empty() { @@ -3237,7 +3215,6 @@ impl MultiBuffer { ); } - self.subscriptions.publish(output_edits); drop(old_diff_transforms); drop(excerpts); snapshot.diff_transforms = new_diff_transforms; @@ -3245,10 +3222,10 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] snapshot.check_invariants(); + output_edits } fn recompute_diff_transforms_for_edit( - &self, edit: &Edit>, excerpts: &mut Cursor>, old_diff_transforms: &mut Cursor, usize>>, @@ -3333,7 +3310,7 @@ impl MultiBuffer { + ExcerptOffset::new(hunk_buffer_range.end - excerpt_buffer_start), ); - self.push_buffer_content_transform( + Self::push_buffer_content_transform( snapshot, new_diff_transforms, hunk_excerpt_start, @@ -3414,7 +3391,6 @@ impl MultiBuffer { } fn append_diff_transforms( - &self, new_transforms: &mut SumTree, subtree: SumTree, ) { @@ -3422,7 +3398,7 @@ impl MultiBuffer { inserted_hunk_info, summary, }) = subtree.first() - && self.extend_last_buffer_content_transform( + && Self::extend_last_buffer_content_transform( new_transforms, *inserted_hunk_info, *summary, @@ -3437,16 +3413,12 @@ impl MultiBuffer { new_transforms.append(subtree, ()); } - fn push_diff_transform( - &self, - new_transforms: &mut SumTree, - transform: DiffTransform, - ) { + fn push_diff_transform(new_transforms: &mut SumTree, transform: DiffTransform) { if let DiffTransform::BufferContent { inserted_hunk_info: inserted_hunk_anchor, summary, } = transform - && self.extend_last_buffer_content_transform( + && Self::extend_last_buffer_content_transform( new_transforms, inserted_hunk_anchor, summary, @@ -3458,7 +3430,6 @@ impl MultiBuffer { } fn push_buffer_content_transform( - &self, old_snapshot: &MultiBufferSnapshot, new_transforms: &mut SumTree, end_offset: ExcerptOffset, @@ -3478,7 +3449,7 @@ impl MultiBuffer { let summary_to_add = old_snapshot .text_summary_for_excerpt_offset_range::(start_offset..end_offset); - if !self.extend_last_buffer_content_transform( + if !Self::extend_last_buffer_content_transform( new_transforms, inserted_hunk_info, summary_to_add, @@ -3495,7 +3466,6 @@ impl MultiBuffer { } fn extend_last_buffer_content_transform( - &self, new_transforms: &mut SumTree, new_inserted_hunk_info: Option, summary_to_add: TextSummary, @@ -3655,7 +3625,7 @@ impl MultiBuffer { let excerpt_ids = self.excerpt_ids(); if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) { - let buffer_handle = if rng.random() || self.buffers.borrow().is_empty() { + let buffer_handle = if rng.random() || self.buffers.is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); buffers.push(cx.new(|cx| Buffer::local(text, cx))); let buffer = buffers.last().unwrap().read(cx); @@ -3666,13 +3636,7 @@ impl MultiBuffer { ); buffers.last().unwrap().clone() } else { - self.buffers - .borrow() - .values() - .choose(rng) - .unwrap() - .buffer - .clone() + self.buffers.values().choose(rng).unwrap().buffer.clone() }; let buffer = buffer_handle.read(cx); @@ -3723,7 +3687,6 @@ impl MultiBuffer { if rng.random_bool(0.7) || self.singleton { let buffer = self .buffers - .borrow() .values() .choose(rng) .map(|state| state.buffer.clone()); diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 2f4bfdfda8d023cf08e31ec6ffc9300446f8a400..1532e5b68ec29e6befbbd17fa97b966449874822 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -797,7 +797,13 @@ async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) { let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let anchor_ranges_1 = multibuffer .update(cx, |multibuffer, cx| { - multibuffer.set_anchored_excerpts_for_path(buffer_1.clone(), ranges_1, 2, cx) + multibuffer.set_anchored_excerpts_for_path( + PathKey::for_buffer(&buffer_1, cx), + buffer_1.clone(), + ranges_1, + 2, + cx, + ) }) .await; let snapshot_1 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); @@ -814,7 +820,13 @@ async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) { ); let anchor_ranges_2 = multibuffer .update(cx, |multibuffer, cx| { - multibuffer.set_anchored_excerpts_for_path(buffer_2.clone(), ranges_2, 2, cx) + multibuffer.set_anchored_excerpts_for_path( + PathKey::for_buffer(&buffer_2, cx), + buffer_2.clone(), + ranges_2, + 2, + cx, + ) }) .await; let snapshot_2 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 13a082b35024b11870fb14fb3419c76841566193..879280c885a0bfda20e5faa70e1e07f7d9fe038c 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -72,13 +72,15 @@ impl ConflictSetSnapshot { (None, None) => None, (None, Some(conflict)) => Some(conflict.range.start), (Some(conflict), None) => Some(conflict.range.start), - (Some(first), Some(second)) => Some(first.range.start.min(&second.range.start, buffer)), + (Some(first), Some(second)) => { + Some(*first.range.start.min(&second.range.start, buffer)) + } }; let end = match (old_conflicts.last(), new_conflicts.last()) { (None, None) => None, (None, Some(conflict)) => Some(conflict.range.end), (Some(first), None) => Some(first.range.end), - (Some(first), Some(second)) => Some(first.range.end.max(&second.range.end, buffer)), + (Some(first), Some(second)) => Some(*first.range.end.max(&second.range.end, buffer)), }; ConflictSetUpdate { buffer_range: start.zip(end).map(|(start, end)| start..end), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4197e57e4d4db0f3ca6d3827d5b32ba19fab6295..1373c7d8454c9e31681d8365f167026f666cb429 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -8,7 +8,8 @@ use crate::{ use anyhow::Context as _; use collections::HashMap; use editor::{ - Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects, + Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey, + SelectionEffects, actions::{Backtab, SelectAll, Tab}, items::active_match_index, multibuffer_context_lines, @@ -340,6 +341,7 @@ impl ProjectSearch { .into_iter() .map(|(buffer, ranges)| { excerpts.set_anchored_excerpts_for_path( + PathKey::for_buffer(&buffer, cx), buffer, ranges, multibuffer_context_lines(cx), diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index a05da1243faa05f33708fe6858fc9dada3c0a1e0..56172c21afcf9fa70a9039218feea59f055a27c5 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -45,19 +45,19 @@ impl Anchor { .then_with(|| self.bias.cmp(&other.bias)) } - pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + pub fn min<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self { if self.cmp(other, buffer).is_le() { - *self + self } else { - *other + other } } - pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + pub fn max<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self { if self.cmp(other, buffer).is_ge() { - *self + self } else { - *other + other } } From 375a404132142c3fed7374c8387fcae9eb4f164a Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 17 Oct 2025 13:56:20 -0500 Subject: [PATCH 006/202] settings_ui: Fix missing list state reset causing panic (#40497) Closes #40467 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/settings_ui.rs | 72 ++++++++++----------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 592e71bbf12f7a64dbb14dd6aa3d152b890b0121..2cd18d0a3da34f5fb6f42b84f14972d1eb9e0503 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -541,7 +541,6 @@ pub struct SettingsWindow { content_focus_handle: Entity, files_focus_handle: FocusHandle, search_index: Option>, - visible_items: Vec, list_state: ListState, } @@ -1074,7 +1073,6 @@ impl SettingsWindow { .tab_index(HEADER_CONTAINER_TAB_INDEX) .tab_stop(false), search_index: None, - visible_items: Vec::default(), list_state, }; @@ -1116,16 +1114,8 @@ impl SettingsWindow { let expanded = &mut self.navbar_entries[nav_entry_index].expanded; *expanded = !*expanded; - let expanded = *expanded; - - let toggle_page_index = self.page_index_from_navbar_index(nav_entry_index); - let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry); - // if currently selected page is a child of the parent page we are folding, - // set the current page to the parent page - if !expanded && selected_page_index == toggle_page_index { - self.navbar_entry = nav_entry_index; - // note: not opening page. Toggling does not change content just selected page - } + self.navbar_entry = nav_entry_index; + self.reset_list_state(); } fn build_navbar(&mut self, cx: &App) { @@ -1480,14 +1470,14 @@ impl SettingsWindow { fn reset_list_state(&mut self) { // plus one for the title - self.visible_items = self.visible_page_items().map(|(index, _)| index).collect(); + let mut visible_items_count = self.visible_page_items().count(); - if self.visible_items.is_empty() { - self.list_state.reset(0); - } else { + if visible_items_count > 0 { // show page title if page is non empty - self.list_state.reset(self.visible_items.len() + 1); + visible_items_count += 1; } + + self.list_state.reset(visible_items_count); } fn build_ui(&mut self, window: &mut Window, cx: &mut Context) { @@ -1961,9 +1951,6 @@ impl SettingsWindow { .on_toggle(cx.listener( move |this, _, window, cx| { this.toggle_navbar_entry(ix); - // Update selection state immediately before cx.notify - // to prevent double selection flash - this.navbar_entry = ix; window.focus( &this.navbar_entries[ix].focus_handle, ); @@ -2134,7 +2121,7 @@ impl SettingsWindow { let mut page_content = v_flex().id("settings-ui-page").size_full(); let has_active_search = !self.search_bar.read(cx).is_empty(cx); - let has_no_results = self.visible_items.len() == 0 && has_active_search; + let has_no_results = self.visible_page_items().next().is_none() && has_active_search; if has_no_results { let search_query = self.search_bar.read(cx).text(cx); @@ -2153,16 +2140,12 @@ impl SettingsWindow { ), ) } else { - let items = &self.current_page().items; - let last_non_header_index = self - .visible_items - .iter() - .map(|index| &items[*index]) - .enumerate() - .rev() - .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_))) - .map(|(index, _)| index); + .visible_page_items() + .filter_map(|(index, item)| { + (!matches!(item, SettingsPageItem::SectionHeader(_))).then_some(index) + }) + .last(); let root_nav_label = self .navbar_entries @@ -2184,20 +2167,16 @@ impl SettingsWindow { }) .into_any_element(); } + let mut visible_items = this.visible_page_items(); + let Some((actual_item_index, item)) = visible_items.nth(index - 1) else { + return gpui::Empty.into_any_element(); + }; - let index = index - 1; - let actual_item_index = this.visible_items[index]; - let item: &SettingsPageItem = &this.current_page().items[actual_item_index]; - - let no_bottom_border = this - .visible_items - .get(index + 1) - .map(|item_index| { - let item = &this.current_page().items[*item_index]; - matches!(item, SettingsPageItem::SectionHeader(_)) - }) + let no_bottom_border = visible_items + .next() + .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_))) .unwrap_or(false); - let is_last = Some(index) == last_non_header_index; + let is_last = Some(actual_item_index) == last_non_header_index; v_flex() .id(("settings-page-item", actual_item_index)) @@ -3073,7 +3052,6 @@ mod test { ), files_focus_handle: cx.focus_handle(), search_index: None, - visible_items: Vec::default(), list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)), }; @@ -3231,11 +3209,11 @@ mod test { ", toggle_page: "General Page", after: r" - > General Page + > General Page* v Project - Worktree Settings Content v AI - - General* + - General > Appearance & Behavior " ); @@ -3254,13 +3232,13 @@ mod test { ", toggle_page: "General Page", after: r" - v General Page + v General Page* - General - Privacy v Project - Worktree Settings Content v AI - - General* + - General > Appearance & Behavior " ); From ef5b8c6fed25da85658ad2d376323f26daa88296 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Fri, 17 Oct 2025 11:58:14 -0700 Subject: [PATCH 007/202] Remove workspace-hack (#40216) We've been considering removing workspace-hack for a couple reasons: - Lukas ran into a situation where its build script seemed to be causing spurious rebuilds. This seems more likely to be a cargo bug than an issue with workspace-hack itself (given that it has an empty build script), but we don't necessarily want to take the time to hunt that down right now. - Marshall mentioned hakari interacts poorly with automated crate updates (in our case provided by rennovate) because you'd need to have `cargo hakari generate && cargo hakari manage-deps` after their changes and we prefer to not have actions that make commits. Currently removing workspace-hack causes our workspace to grow from ~1700 to ~2000 crates being built (depending on platform), which is mainly a problem when you're building the whole workspace or running tests across the the normal and remote binaries (which is where feature-unification nets us the most sharing). It doesn't impact incremental times noticeably when you're just iterating on `-p zed`, and we'll hopefully get these savings back in the future when rust-lang/cargo#14774 (which re-implements the functionality of hakari) is finished. Release Notes: - N/A --- .config/hakari.toml | 42 - .github/workflows/ci.yml | 35 - Cargo.lock | 4143 ++++++++--------- Cargo.toml | 22 +- crates/acp_thread/Cargo.toml | 1 - crates/acp_tools/Cargo.toml | 1 - crates/action_log/Cargo.toml | 1 - crates/activity_indicator/Cargo.toml | 1 - crates/agent/Cargo.toml | 1 - crates/agent_servers/Cargo.toml | 1 - crates/agent_settings/Cargo.toml | 1 - crates/agent_ui/Cargo.toml | 1 - crates/ai_onboarding/Cargo.toml | 1 - crates/anthropic/Cargo.toml | 1 - crates/askpass/Cargo.toml | 1 - crates/assets/Cargo.toml | 1 - crates/assistant_context/Cargo.toml | 1 - crates/assistant_slash_command/Cargo.toml | 1 - crates/assistant_slash_commands/Cargo.toml | 1 - crates/audio/Cargo.toml | 3 +- crates/auto_update/Cargo.toml | 1 - crates/auto_update_helper/Cargo.toml | 1 - crates/auto_update_ui/Cargo.toml | 1 - crates/aws_http_client/Cargo.toml | 1 - crates/bedrock/Cargo.toml | 1 - crates/breadcrumbs/Cargo.toml | 1 - crates/buffer_diff/Cargo.toml | 1 - crates/call/Cargo.toml | 1 - crates/channel/Cargo.toml | 1 - crates/cli/Cargo.toml | 1 - crates/client/Cargo.toml | 1 - crates/clock/Cargo.toml | 1 - crates/cloud_api_client/Cargo.toml | 1 - crates/cloud_api_types/Cargo.toml | 1 - crates/cloud_llm_client/Cargo.toml | 1 - crates/cloud_zeta2_prompt/Cargo.toml | 1 - crates/codestral/Cargo.toml | 1 - crates/collab/Cargo.toml | 7 +- crates/collab/src/db/queries/extensions.rs | 2 +- crates/collab/src/db/queries/notifications.rs | 4 +- crates/collab/src/db/tests.rs | 2 +- crates/collab_ui/Cargo.toml | 1 - crates/collections/Cargo.toml | 1 - crates/command_palette/Cargo.toml | 1 - crates/command_palette_hooks/Cargo.toml | 1 - crates/component/Cargo.toml | 1 - crates/context_server/Cargo.toml | 1 - crates/copilot/Cargo.toml | 1 - crates/crashes/Cargo.toml | 1 - crates/credentials_provider/Cargo.toml | 1 - crates/dap/Cargo.toml | 1 - crates/dap/src/adapters.rs | 2 +- crates/dap_adapters/Cargo.toml | 1 - crates/db/Cargo.toml | 1 - crates/debug_adapter_extension/Cargo.toml | 1 - crates/debugger_tools/Cargo.toml | 1 - crates/debugger_ui/Cargo.toml | 1 - crates/deepseek/Cargo.toml | 1 - crates/denoise/Cargo.toml | 1 - crates/diagnostics/Cargo.toml | 1 - crates/docs_preprocessor/Cargo.toml | 1 - crates/edit_prediction/Cargo.toml | 1 - crates/edit_prediction_button/Cargo.toml | 1 - crates/edit_prediction_context/Cargo.toml | 1 - crates/editor/Cargo.toml | 1 - crates/eval/Cargo.toml | 1 - .../src/examples/threads/overwrite-file.json | 2 +- crates/explorer_command_injector/Cargo.toml | 1 - crates/extension/Cargo.toml | 1 - crates/extension_cli/Cargo.toml | 1 - crates/extension_host/Cargo.toml | 1 - .../src/wasm_host/wit/since_v0_1_0.rs | 2 +- .../src/wasm_host/wit/since_v0_6_0.rs | 2 +- crates/extensions_ui/Cargo.toml | 1 - crates/feature_flags/Cargo.toml | 1 - crates/feedback/Cargo.toml | 1 - crates/file_finder/Cargo.toml | 1 - crates/file_icons/Cargo.toml | 1 - crates/fs/Cargo.toml | 1 - crates/fs_benchmarks/Cargo.toml | 13 - crates/fsevent/Cargo.toml | 1 - crates/fuzzy/Cargo.toml | 1 - crates/git/Cargo.toml | 1 - crates/git_hosting_providers/Cargo.toml | 1 - crates/git_ui/Cargo.toml | 1 - crates/go_to_line/Cargo.toml | 1 - crates/google_ai/Cargo.toml | 1 - crates/gpui/Cargo.toml | 2 +- crates/gpui_macros/Cargo.toml | 1 - crates/gpui_tokio/Cargo.toml | 1 - crates/html_to_markdown/Cargo.toml | 1 - crates/http_client/Cargo.toml | 1 - crates/http_client_tls/Cargo.toml | 1 - crates/icons/Cargo.toml | 1 - crates/image_viewer/Cargo.toml | 1 - crates/inspector_ui/Cargo.toml | 1 - crates/install_cli/Cargo.toml | 1 - crates/journal/Cargo.toml | 1 - crates/json_schema_store/Cargo.toml | 1 - crates/keymap_editor/Cargo.toml | 1 - crates/language/Cargo.toml | 1 - crates/language_extension/Cargo.toml | 1 - crates/language_model/Cargo.toml | 1 - crates/language_models/Cargo.toml | 1 - crates/language_onboarding/Cargo.toml | 1 - crates/language_selector/Cargo.toml | 1 - crates/language_tools/Cargo.toml | 1 - crates/languages/Cargo.toml | 1 - crates/line_ending_selector/Cargo.toml | 1 - crates/livekit_api/Cargo.toml | 1 - crates/livekit_client/Cargo.toml | 3 +- crates/lmstudio/Cargo.toml | 1 - crates/lsp/Cargo.toml | 1 - crates/markdown/Cargo.toml | 1 - crates/markdown_preview/Cargo.toml | 1 - crates/media/Cargo.toml | 1 - crates/menu/Cargo.toml | 1 - crates/migrator/Cargo.toml | 1 - crates/mistral/Cargo.toml | 1 - crates/multi_buffer/Cargo.toml | 1 - crates/nc/Cargo.toml | 1 - crates/net/Cargo.toml | 1 - crates/node_runtime/Cargo.toml | 1 - crates/notifications/Cargo.toml | 1 - crates/ollama/Cargo.toml | 1 - crates/onboarding/Cargo.toml | 1 - crates/open_ai/Cargo.toml | 1 - crates/open_router/Cargo.toml | 1 - crates/outline/Cargo.toml | 1 - crates/outline_panel/Cargo.toml | 1 - crates/panel/Cargo.toml | 1 - crates/paths/Cargo.toml | 1 - crates/picker/Cargo.toml | 1 - crates/prettier/Cargo.toml | 1 - crates/project/Cargo.toml | 1 - crates/project_panel/Cargo.toml | 1 - crates/project_symbols/Cargo.toml | 1 - crates/prompt_store/Cargo.toml | 1 - crates/proto/Cargo.toml | 1 - crates/recent_projects/Cargo.toml | 1 - crates/refineable/Cargo.toml | 1 - .../refineable/derive_refineable/Cargo.toml | 1 - crates/release_channel/Cargo.toml | 1 - crates/remote/Cargo.toml | 1 - crates/remote/src/transport/ssh.rs | 2 +- crates/remote/src/transport/wsl.rs | 2 +- crates/repl/Cargo.toml | 3 +- crates/reqwest_client/Cargo.toml | 1 - crates/rich_text/Cargo.toml | 1 - crates/rope/Cargo.toml | 1 - crates/rpc/Cargo.toml | 1 - crates/rules_library/Cargo.toml | 1 - crates/scheduler/Cargo.toml | 1 - crates/schema_generator/Cargo.toml | 1 - crates/search/Cargo.toml | 1 - crates/semantic_version/Cargo.toml | 1 - crates/session/Cargo.toml | 1 - crates/settings/Cargo.toml | 1 - crates/settings_macros/Cargo.toml | 1 - crates/settings_profile_selector/Cargo.toml | 1 - crates/settings_ui/Cargo.toml | 1 - crates/snippet/Cargo.toml | 1 - crates/snippet_provider/Cargo.toml | 1 - crates/snippets_ui/Cargo.toml | 1 - crates/sqlez/Cargo.toml | 1 - crates/sqlez_macros/Cargo.toml | 1 - crates/story/Cargo.toml | 1 - crates/storybook/Cargo.toml | 1 - crates/streaming_diff/Cargo.toml | 1 - crates/sum_tree/Cargo.toml | 1 - crates/supermaven/Cargo.toml | 1 - crates/supermaven_api/Cargo.toml | 1 - crates/svg_preview/Cargo.toml | 1 - crates/system_specs/Cargo.toml | 1 - crates/tab_switcher/Cargo.toml | 1 - crates/task/Cargo.toml | 1 - crates/tasks_ui/Cargo.toml | 1 - crates/telemetry/Cargo.toml | 1 - crates/telemetry_events/Cargo.toml | 1 - crates/terminal/Cargo.toml | 1 - crates/terminal_view/Cargo.toml | 1 - crates/text/Cargo.toml | 1 - crates/theme/Cargo.toml | 1 - crates/theme_extension/Cargo.toml | 1 - crates/theme_importer/Cargo.toml | 1 - crates/theme_selector/Cargo.toml | 1 - crates/time_format/Cargo.toml | 1 - crates/title_bar/Cargo.toml | 1 - crates/toolchain_selector/Cargo.toml | 1 - crates/ui/Cargo.toml | 1 - crates/ui_input/Cargo.toml | 1 - crates/ui_macros/Cargo.toml | 1 - crates/ui_prompt/Cargo.toml | 1 - crates/util/Cargo.toml | 1 - crates/util_macros/Cargo.toml | 1 - crates/vercel/Cargo.toml | 1 - crates/vim/Cargo.toml | 1 - crates/vim_mode_setting/Cargo.toml | 1 - crates/watch/Cargo.toml | 1 - crates/web_search/Cargo.toml | 1 - crates/web_search_providers/Cargo.toml | 1 - crates/workspace/Cargo.toml | 1 - crates/worktree/Cargo.toml | 1 - crates/worktree_benchmarks/Cargo.toml | 1 - crates/x_ai/Cargo.toml | 1 - crates/zed/Cargo.toml | 1 - crates/zed_actions/Cargo.toml | 1 - crates/zed_env_vars/Cargo.toml | 1 - crates/zeta/Cargo.toml | 1 - crates/zeta2/Cargo.toml | 1 - crates/zeta2_tools/Cargo.toml | 1 - crates/zeta_cli/Cargo.toml | 1 - crates/zlog/Cargo.toml | 1 - crates/zlog_settings/Cargo.toml | 1 - renovate.json | 2 +- script/new-crate | 1 - script/update-workspace-hack | 20 - script/update-workspace-hack.ps1 | 36 - tooling/perf/Cargo.toml | 1 - tooling/workspace-hack/.gitattributes | 4 - tooling/workspace-hack/.ignore | 2 - tooling/workspace-hack/Cargo.toml | 700 --- tooling/workspace-hack/LICENSE-GPL | 1 - tooling/workspace-hack/build.rs | 2 - tooling/workspace-hack/src/lib.rs | 1 - tooling/xtask/Cargo.toml | 1 - tooling/xtask/src/tasks/package_conformity.rs | 5 - 227 files changed, 2020 insertions(+), 3244 deletions(-) delete mode 100644 .config/hakari.toml delete mode 100644 crates/fs_benchmarks/Cargo.toml delete mode 100755 script/update-workspace-hack delete mode 100644 script/update-workspace-hack.ps1 delete mode 100644 tooling/workspace-hack/.gitattributes delete mode 100644 tooling/workspace-hack/.ignore delete mode 100644 tooling/workspace-hack/Cargo.toml delete mode 120000 tooling/workspace-hack/LICENSE-GPL delete mode 100644 tooling/workspace-hack/build.rs delete mode 100644 tooling/workspace-hack/src/lib.rs diff --git a/.config/hakari.toml b/.config/hakari.toml deleted file mode 100644 index 1e8386a14115be2e36b287ace0d47d464df9e620..0000000000000000000000000000000000000000 --- a/.config/hakari.toml +++ /dev/null @@ -1,42 +0,0 @@ -# This file contains settings for `cargo hakari`. -# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options. - -hakari-package = "workspace-hack" - -resolver = "2" -dep-format-version = "4" -workspace-hack-line-style = "workspace-dotted" - -# this should be the same list as "targets" in ../rust-toolchain.toml -platforms = [ - "x86_64-apple-darwin", - "aarch64-apple-darwin", - "x86_64-unknown-linux-gnu", - "aarch64-unknown-linux-gnu", - "x86_64-pc-windows-msvc", - "x86_64-unknown-linux-musl", # remote server -] - -[traversal-excludes] -workspace-members = [ - "remote_server", -] -third-party = [ - { name = "reqwest", version = "0.11.27" }, - # build of remote_server should not include scap / its x11 dependency - { name = "zed-scap", git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", version = "0.0.8-zed" }, - # build of remote_server should not need to include on libalsa through rodio - { name = "rodio", git = "https://github.com/RustAudio/rodio" }, -] - -[final-excludes] -workspace-members = [ - "zed_extension_api", - - # exclude all extensions - "zed_glsl", - "zed_html", - "zed_proto", - "slash_commands_example", - "zed_test_extension", -] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5bf5790e4b7daf02f4713d25b1017b494f88f1a..a8a587895aaf747f89fb4b93ece8e3b51deb076c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,39 +130,6 @@ jobs: input: "crates/proto/proto/" against: "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/" - workspace_hack: - timeout-minutes: 60 - name: Check workspace-hack crate - needs: [job_spec] - if: | - github.repository_owner == 'zed-industries' && - needs.job_spec.outputs.run_tests == 'true' - runs-on: - - namespace-profile-8x16-ubuntu-2204 - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - - name: Install cargo-hakari - uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2 - with: - command: install - args: cargo-hakari@0.9.35 - - - name: Check workspace-hack Cargo.toml is up-to-date - run: | - cargo hakari generate --diff || { - echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1"; - false - } - - name: Check all crates depend on workspace-hack - run: | - cargo hakari manage-deps --dry-run || { - echo "To fix, run script/update-workspace-hack or script/update-workspace-hack.ps1" - false - } - style: timeout-minutes: 60 name: Check formatting and spelling @@ -507,7 +474,6 @@ jobs: - actionlint - migration_checks # run_tests: If adding required tests, add them here and to script below. - - workspace_hack - linux_tests - build_remote_server - macos_tests @@ -533,7 +499,6 @@ jobs: # Only check test jobs if they were supposed to run if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then - [[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; } [[ "${{ needs.macos_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "macOS tests failed"; } [[ "${{ needs.linux_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Linux tests failed"; } [[ "${{ needs.windows_tests.result }}" != 'success' ]] && { RET_CODE=1; echo "Windows tests failed"; } diff --git a/Cargo.lock b/Cargo.lock index bb3b71a3ee45e052670ef3e67c877833253c76b0..e85c8adaad744c4e7940764c5d6710c8f8344352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "portable-pty", "project", "prompt_store", - "rand 0.9.1", + "rand 0.9.2", "serde", "serde_json", "settings", @@ -39,7 +39,6 @@ dependencies = [ "util", "uuid", "watch", - "workspace-hack", ] [[package]] @@ -59,7 +58,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -78,13 +76,12 @@ dependencies = [ "log", "pretty_assertions", "project", - "rand 0.9.1", + "rand 0.9.2", "serde_json", "settings", "text", "util", "watch", - "workspace-hack", "zlog", ] @@ -106,23 +103,22 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli", + "gimli 0.32.3", ] [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -179,11 +175,11 @@ dependencies = [ "pretty_assertions", "project", "prompt_store", - "rand 0.9.1", + "rand 0.9.2", "regex", "reqwest_client", "rust-embed", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -198,7 +194,7 @@ dependencies = [ "terminal", "text", "theme", - "thiserror 2.0.12", + "thiserror 2.0.17", "tree-sitter-rust", "ui", "unindent", @@ -206,7 +202,6 @@ dependencies = [ "uuid", "watch", "web_search", - "workspace-hack", "worktree", "zed_env_vars", "zlog", @@ -225,7 +220,7 @@ dependencies = [ "futures 0.3.31", "log", "parking_lot", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", ] @@ -265,12 +260,11 @@ dependencies = [ "task", "tempfile", "terminal", - "thiserror 2.0.12", + "thiserror 2.0.17", "ui", "util", "uuid", "watch", - "workspace-hack", ] [[package]] @@ -286,13 +280,12 @@ dependencies = [ "language_model", "paths", "project", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", "settings", "util", - "workspace-hack", ] [[package]] @@ -354,12 +347,12 @@ dependencies = [ "project", "prompt_store", "proto", - "rand 0.9.1", + "rand 0.9.2", "ref-cast", "release_channel", "rope", "rules_library", - "schemars 1.0.1", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -385,7 +378,6 @@ dependencies = [ "util", "watch", "workspace", - "workspace-hack", "zed_actions", ] @@ -395,24 +387,24 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "version_check", ] [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", - "getrandom 0.2.15", + "getrandom 0.3.4", "once_cell", "serde", "version_check", - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -437,7 +429,6 @@ dependencies = [ "smallvec", "telemetry", "ui", - "workspace-hack", "zed_actions", ] @@ -448,7 +439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb5f4f1ef69bdb8b2095ddd14b09dd74ee0303aae8bd5372667a54cff689a1b" dependencies = [ "base64 0.22.1", - "bitflags 2.9.0", + "bitflags 2.9.4", "home", "libc", "log", @@ -457,7 +448,7 @@ dependencies = [ "piper", "polling", "regex-automata", - "rustix 1.0.7", + "rustix 1.1.2", "rustix-openpty", "serde", "signal-hook", @@ -474,9 +465,12 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "aligned-vec" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] [[package]] name = "alloc-no-stdlib" @@ -506,7 +500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -540,12 +534,6 @@ dependencies = [ "url", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -563,9 +551,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -578,37 +566,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] @@ -619,13 +607,12 @@ dependencies = [ "chrono", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", "strum 0.27.2", - "thiserror 2.0.12", - "workspace-hack", + "thiserror 2.0.17", ] [[package]] @@ -636,9 +623,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4" [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "approx" @@ -651,9 +638,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -666,7 +653,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -748,13 +735,13 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.1", + "rand 0.9.2", "serde", "serde_repr", "url", "wayland-backend", "wayland-client", - "wayland-protocols 0.32.6", + "wayland-protocols 0.32.9", "zbus", ] @@ -769,7 +756,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.1", + "rand 0.9.2", "serde", "serde_repr", "url", @@ -788,8 +775,7 @@ dependencies = [ "smol", "tempfile", "util", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", "zeroize", ] @@ -800,7 +786,6 @@ dependencies = [ "anyhow", "gpui", "rust-embed", - "workspace-hack", ] [[package]] @@ -832,7 +817,7 @@ dependencies = [ "project", "prompt_store", "proto", - "rand 0.9.1", + "rand 0.9.2", "regex", "rpc", "serde", @@ -847,7 +832,6 @@ dependencies = [ "util", "uuid", "workspace", - "workspace-hack", "zed_env_vars", ] @@ -871,7 +855,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -905,7 +888,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "worktree", "zlog", ] @@ -926,7 +908,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite", @@ -945,9 +927,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -957,9 +939,9 @@ dependencies = [ [[package]] name = "async-compat" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bab94bde396a3f7b4962e396fdad640e241ed797d4d8d77fc8c237d14c58fc0" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" dependencies = [ "futures-core", "futures-io", @@ -970,15 +952,14 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" dependencies = [ - "deflate64", - "flate2", + "compression-codecs", + "compression-core", "futures-core", "futures-io", - "memchr", "pin-project-lite", ] @@ -994,26 +975,27 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", "fastrand 2.3.0", - "futures-lite 2.6.0", + "futures-lite 2.6.1", + "pin-project-lite", "slab", ] [[package]] name = "async-fs" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ "async-lock 3.4.1", "blocking", - "futures-lite 2.6.0", + "futures-lite 2.6.1", ] [[package]] @@ -1022,31 +1004,31 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", "async-io", "async-lock 3.4.1", "blocking", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "once_cell", ] [[package]] name = "async-io" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "async-lock 3.4.1", + "autocfg", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "parking", "polling", - "rustix 1.0.7", + "rustix 1.1.2", "slab", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1064,7 +1046,7 @@ version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite", ] @@ -1077,7 +1059,7 @@ checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ "async-io", "blocking", - "futures-lite 2.6.0", + "futures-lite 2.6.1", ] [[package]] @@ -1091,21 +1073,20 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-io", "async-lock 3.4.1", "async-signal", "async-task", "blocking", "cfg-if", - "event-listener 5.4.0", - "futures-lite 2.6.0", - "rustix 0.38.44", - "tracing", + "event-listener 5.4.1", + "futures-lite 2.6.1", + "rustix 1.1.2", ] [[package]] @@ -1116,14 +1097,14 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock 3.4.1", @@ -1131,17 +1112,17 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.44", + "rustix 1.1.2", "signal-hook-registry", "slab", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "async-std" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-attributes", "async-channel 1.9.0", @@ -1153,7 +1134,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "gloo-timers", "kv-log-macro", "log", @@ -1184,7 +1165,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -1215,14 +1196,14 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "async-tungstenite" -version = "0.29.1" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886" +checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" dependencies = [ "atomic-waker", "futures-core", @@ -1234,7 +1215,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", - "tungstenite 0.26.2", + "tungstenite 0.27.0", ] [[package]] @@ -1245,7 +1226,7 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" dependencies = [ "async-compression", "crc32fast", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "pin-project", "thiserror 1.0.69", ] @@ -1310,9 +1291,8 @@ dependencies = [ "serde", "settings", "smol", - "thiserror 2.0.12", + "thiserror 2.0.17", "util", - "workspace-hack", ] [[package]] @@ -1346,7 +1326,6 @@ dependencies = [ "tempfile", "which 6.0.3", "workspace", - "workspace-hack", ] [[package]] @@ -1356,9 +1335,8 @@ dependencies = [ "anyhow", "log", "simplelog", - "windows 0.61.1", + "windows 0.61.3", "winresource", - "workspace-hack", ] [[package]] @@ -1378,20 +1356,19 @@ dependencies = [ "smol", "util", "workspace", - "workspace-hack", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av1-grain" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" dependencies = [ "anyhow", "arrayvec", @@ -1403,18 +1380,18 @@ dependencies = [ [[package]] name = "avif-serialize" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +checksum = "47c8fbc0f831f4519fe8b810b6a7a91410ec83031b8233f730a0480029f6a23f" dependencies = [ "arrayvec", ] [[package]] name = "aws-config" -version = "1.6.1" +version = "1.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c39646d1a6b51240a1a23bb57ea4eebede7e16fbc237fdc876980233dcecb4f" +checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1442,9 +1419,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.2" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4471bef4c22a06d2c7a1b6492493d3fdf24a805323109d6874f9c94d5906ac14" +checksum = "faf26925f4a5b59eb76722b63c2892b1d70d06fa053c72e4a100ec308c1d47bc" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -1454,9 +1431,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.13.1" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ "aws-lc-sys", "zeroize", @@ -1464,11 +1441,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.29.0" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" dependencies = [ - "bindgen 0.69.5", + "bindgen 0.72.1", "cc", "cmake", "dunce", @@ -1477,9 +1454,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.6" +version = "1.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aff45ffe35196e593ea3b9dd65b320e51e2dda95aff4390bc459e461d09c6ad" +checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -1494,7 +1471,6 @@ dependencies = [ "fastrand 2.3.0", "http 0.2.12", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "tracing", @@ -1503,9 +1479,9 @@ dependencies = [ [[package]] name = "aws-sdk-bedrockruntime" -version = "1.82.0" +version = "1.109.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb95f77abd4321348dd2f52a25e1de199732f54d2a35860ad20f5df21c66b44" +checksum = "fbfdfd941dcb253c17bf70baddbf1e5b22f19e29d313d2e049bad4b1dadb2011" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1522,16 +1498,15 @@ dependencies = [ "fastrand 2.3.0", "http 0.2.12", "hyper 0.14.32", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-kinesis" -version = "1.66.0" +version = "1.91.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43e5fb05c78cdad4fef5be4503465e4b42292f472fc991823ea4c50078208e4" +checksum = "699a3d645a2ab5cb12ca02eb23979753953414429fd6584ea8841af6bc4e0516" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1546,16 +1521,15 @@ dependencies = [ "bytes 1.10.1", "fastrand 2.3.0", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-s3" -version = "1.82.0" +version = "1.108.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6eab2900764411ab01c8e91a76fd11a63b4e12bc3da97d9e14a0ce1343d86d3" +checksum = "200be4aed61e3c0669f7268bacb768f283f1c32a7014ce57225e1160be2f6ccb" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1578,7 +1552,6 @@ dependencies = [ "http 1.3.1", "http-body 0.4.6", "lru", - "once_cell", "percent-encoding", "regex-lite", "sha2", @@ -1588,9 +1561,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.64.0" +version = "1.86.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d4bdb0e5f80f0689e61c77ab678b2b9304af329616af38aef5b6b967b8e736" +checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1604,16 +1577,15 @@ dependencies = [ "bytes 1.10.1", "fastrand 2.3.0", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.65.0" +version = "1.88.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbbb3ce8da257aedbccdcb1aadafbbb6a5fe9adf445db0e1ea897bdc7e22d08" +checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1627,16 +1599,15 @@ dependencies = [ "bytes 1.10.1", "fastrand 2.3.0", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.65.0" +version = "1.88.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a78a8f50a1630db757b60f679c8226a8a70ee2ab5f5e6e51dc67f6c61c7cfd" +checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1651,16 +1622,15 @@ dependencies = [ "aws-types", "fastrand 2.3.0", "http 0.2.12", - "once_cell", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.0" +version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d03c3c05ff80d54ff860fe38c726f6f494c639ae975203a101335f223386db" +checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -1674,7 +1644,6 @@ dependencies = [ "hmac", "http 0.2.12", "http 1.3.1", - "once_cell", "p256", "percent-encoding", "ring", @@ -1687,9 +1656,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.5" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" dependencies = [ "futures-util", "pin-project-lite", @@ -1698,16 +1667,14 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.1" +version = "0.63.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65d21e1ba6f2cdec92044f904356a19f5ad86961acf015741106cdfafd747c0" +checksum = "165d8583d8d906e2fb5511d29201d447cc710864f075debcdd9c31c265412806" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes 1.10.1", - "crc32c", - "crc32fast", - "crc64fast-nvme", + "crc-fast", "hex", "http 0.2.12", "http-body 0.4.6", @@ -1720,9 +1687,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.8" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c45d3dddac16c5c59d553ece225a88870cf81b7b813c9cc17b78cf4685eac7a" +checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa" dependencies = [ "aws-smithy-types", "bytes 1.10.1", @@ -1731,9 +1698,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.0" +version = "0.62.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5949124d11e538ca21142d1fba61ab0a2a2c1bc3ed323cdb3e4b878bfb83166" +checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -1744,7 +1711,6 @@ dependencies = [ "http 0.2.12", "http 1.3.1", "http-body 0.4.6", - "once_cell", "percent-encoding", "pin-project-lite", "pin-utils", @@ -1753,56 +1719,57 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.0.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aff1159006441d02e57204bf57a1b890ba68bedb6904ffd2873c1c4c11c546b" +checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", - "h2 0.4.9", + "h2 0.3.27", + "h2 0.4.12", "http 0.2.12", "http 1.3.1", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-rustls 0.24.2", - "hyper-rustls 0.27.5", + "hyper-rustls 0.27.7", "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.26", - "rustls-native-certs 0.8.1", + "rustls 0.23.33", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", + "tokio-rustls 0.26.2", "tower 0.5.2", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.3" +version = "0.61.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92144e45819cae7dc62af23eac5a038a58aa544432d2102609654376a900bd07" +checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445d065e76bc1ef54963db400319f1dd3ebb3e0a74af20f7f7630625b0cc7cc0" +checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" dependencies = [ "aws-smithy-runtime-api", - "once_cell", ] [[package]] name = "aws-smithy-query" -version = "0.60.7" +version = "0.60.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" dependencies = [ "aws-smithy-types", "urlencoding", @@ -1810,9 +1777,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.8.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0152749e17ce4d1b47c7747bdfec09dac1ccafdcbc741ebf9daa2a373356730f" +checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -1826,7 +1793,6 @@ dependencies = [ "http 1.3.1", "http-body 0.4.6", "http-body 1.0.1", - "once_cell", "pin-project-lite", "pin-utils", "tokio", @@ -1835,9 +1801,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.7.4" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da37cf5d57011cb1753456518ec76e31691f1f474b73934a284eb2a1c76510f" +checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1852,9 +1818,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.0" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836155caafba616c0ff9b07944324785de2ab016141c3550bd1c07882f8cee8f" +checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457" dependencies = [ "base64-simd", "bytes 1.10.1", @@ -1878,18 +1844,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.9" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" +checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.6" +version = "1.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3873f8deed8927ce8d04487630dc9ff73193bab64742a61d050e57a68dec4125" +checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -1906,7 +1872,6 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "http_client", - "workspace-hack", ] [[package]] @@ -1985,17 +1950,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -2028,9 +1993,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bedrock" @@ -2040,12 +2005,11 @@ dependencies = [ "aws-sdk-bedrockruntime", "aws-smithy-types", "futures 0.3.31", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", - "thiserror 2.0.12", - "workspace-hack", + "thiserror 2.0.17", ] [[package]] @@ -2093,55 +2057,34 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.5" +version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", + "itertools 0.11.0", "log", "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", - "shlex", - "syn 2.0.101", - "which 4.4.2", -] - -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags 2.9.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", + "rustc-hash 2.1.1", "shlex", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.11.0", "log", "prettyplease", "proc-macro2", @@ -2149,7 +2092,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2184,9 +2127,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bit_field" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" @@ -2196,9 +2139,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" dependencies = [ "serde", ] @@ -2229,9 +2172,9 @@ checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae" dependencies = [ "ash", "ash-window", - "bitflags 2.9.0", + "bitflags 2.9.4", "bytemuck", - "codespan-reporting", + "codespan-reporting 0.12.0", "glow", "gpu-alloc", "gpu-alloc-ash", @@ -2264,7 +2207,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2318,23 +2261,23 @@ dependencies = [ [[package]] name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ "objc2", ] [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-task", "futures-io", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "piper", ] @@ -2378,7 +2321,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2398,7 +2341,6 @@ dependencies = [ "theme", "ui", "workspace", - "workspace-hack", "zed_actions", ] @@ -2447,14 +2389,13 @@ dependencies = [ "language", "log", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "rope", "serde_json", "sum_tree", "text", "unindent", "util", - "workspace-hack", "zlog", ] @@ -2466,9 +2407,9 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" dependencies = [ "allocator-api2", ] @@ -2503,28 +2444,28 @@ dependencies = [ [[package]] name = "bytecount" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2594,12 +2535,12 @@ version = "0.56.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "cached_proc_macro", "cached_proc_macro_types", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "once_cell", - "thiserror 2.0.12", + "thiserror 2.0.17", "web-time", ] @@ -2609,10 +2550,10 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -2644,7 +2585,6 @@ dependencies = [ "settings", "telemetry", "util", - "workspace-hack", ] [[package]] @@ -2653,7 +2593,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "log", "polling", "rustix 0.38.44", @@ -2675,11 +2615,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.9" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -2694,13 +2634,13 @@ dependencies = [ "memmap2", "num-traits", "num_cpus", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", "rayon", "safetensors", "thiserror 1.0.69", "ug", - "yoke", + "yoke 0.7.5", "zip 1.1.4", ] @@ -2749,7 +2689,7 @@ checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c" dependencies = [ "cap-primitives", "cap-std", - "rustix 1.0.7", + "rustix 1.1.2", "smallvec", ] @@ -2765,7 +2705,7 @@ dependencies = [ "io-lifetimes", "ipnet", "maybe-owned", - "rustix 1.0.7", + "rustix 1.1.2", "rustix-linux-procfs", "windows-sys 0.59.0", "winx", @@ -2790,7 +2730,7 @@ dependencies = [ "cap-primitives", "io-extras", "io-lifetimes", - "rustix 1.0.7", + "rustix 1.1.2", ] [[package]] @@ -2803,7 +2743,7 @@ dependencies = [ "cap-primitives", "iana-time-zone", "once_cell", - "rustix 1.0.7", + "rustix 1.1.2", "winx", ] @@ -2827,7 +2767,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -2837,7 +2777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" dependencies = [ "serde", - "toml 0.8.20", + "toml 0.8.23", ] [[package]] @@ -2871,23 +2811,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadd868a2ce9ca38de7eeafdcec9c7065ef89b42b32f0839278d55f35c54d1ff" dependencies = [ "heck 0.4.1", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "proc-macro2", "quote", "serde", "serde_json", - "syn 2.0.101", + "syn 2.0.106", "tempfile", - "toml 0.8.20", + "toml 0.8.23", ] [[package]] name = "cc" -version = "1.2.19" +version = "1.2.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -2920,9 +2861,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -2965,22 +2906,20 @@ dependencies = [ "text", "time", "util", - "workspace-hack", ] [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.1.1", + "windows-link 0.2.1", ] [[package]] @@ -3039,9 +2978,9 @@ dependencies = [ [[package]] name = "circular-buffer" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bdce1da528cadbac4654b5632bfcd8c6c63e25b1d42cea919a95958790b51d" +checksum = "14c638459986b83c2b885179bd4ea6a2cbb05697b001501a56adb3a3d230803b" [[package]] name = "clang-sys" @@ -3056,9 +2995,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.37" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", "clap_derive", @@ -3066,9 +3005,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.37" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", @@ -3079,30 +3018,30 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.47" +version = "4.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f5378ea264ad4f82bbc826628b5aad714a75abf6ece087e923010eb937fb6" +checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cli" @@ -3124,8 +3063,7 @@ dependencies = [ "serde", "tempfile", "util", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", ] [[package]] @@ -3155,7 +3093,7 @@ dependencies = [ "parking_lot", "paths", "postage", - "rand 0.9.1", + "rand 0.9.2", "regex", "release_channel", "rpc", @@ -3169,7 +3107,7 @@ dependencies = [ "telemetry", "telemetry_events", "text", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tiny_http", "tokio", @@ -3178,8 +3116,7 @@ dependencies = [ "tokio-socks", "url", "util", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", "worktree", ] @@ -3190,7 +3127,6 @@ dependencies = [ "parking_lot", "serde", "smallvec", - "workspace-hack", ] [[package]] @@ -3205,7 +3141,6 @@ dependencies = [ "http_client", "parking_lot", "serde_json", - "workspace-hack", "yawc", ] @@ -3220,7 +3155,6 @@ dependencies = [ "pretty_assertions", "serde", "serde_json", - "workspace-hack", ] [[package]] @@ -3235,7 +3169,6 @@ dependencies = [ "serde_json", "strum 0.27.2", "uuid", - "workspace-hack", ] [[package]] @@ -3249,7 +3182,6 @@ dependencies = [ "rustc-hash 2.1.1", "serde", "strum 0.27.2", - "workspace-hack", ] [[package]] @@ -3263,9 +3195,12 @@ dependencies = [ [[package]] name = "cobs" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.17", +] [[package]] name = "cocoa" @@ -3289,7 +3224,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "block", "cocoa-foundation 0.2.0", "core-foundation 0.10.0", @@ -3319,7 +3254,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -3338,6 +3273,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "codespan-reporting" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba7a06c0b31fff5ff2e1e7d37dbf940864e2a974b336e1a2938d10af6e8fb283" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + [[package]] name = "codestral" version = "0.1.0" @@ -3356,7 +3302,6 @@ dependencies = [ "serde_json", "smol", "text", - "workspace-hack", ] [[package]] @@ -3424,7 +3369,7 @@ dependencies = [ "prometheus", "prompt_store", "prost 0.9.0", - "rand 0.9.1", + "rand 0.9.2", "recent_projects", "release_channel", "remote", @@ -3452,7 +3397,7 @@ dependencies = [ "theme", "time", "tokio", - "toml 0.8.20", + "toml 0.8.23", "tower 0.4.13", "tower-http 0.4.4", "tracing", @@ -3461,7 +3406,6 @@ dependencies = [ "util", "uuid", "workspace", - "workspace-hack", "worktree", "zlog", ] @@ -3504,16 +3448,14 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] name = "collections" version = "0.1.0" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.4", "rustc-hash 2.1.1", - "workspace-hack", ] [[package]] @@ -3524,9 +3466,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -3551,12 +3493,12 @@ dependencies = [ [[package]] name = "command-fds" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a" +checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" dependencies = [ "nix 0.30.1", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -3589,7 +3531,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -3601,7 +3542,6 @@ dependencies = [ "derive_more", "gpui", "workspace", - "workspace-hack", ] [[package]] @@ -3630,9 +3570,26 @@ dependencies = [ "parking_lot", "strum 0.27.2", "theme", - "workspace-hack", ] +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "compression-core", + "deflate64", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -3676,7 +3633,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "once_cell", "tiny-keccak", ] @@ -3706,7 +3663,7 @@ dependencies = [ "net", "parking_lot", "postage", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -3714,7 +3671,6 @@ dependencies = [ "tempfile", "url", "util", - "workspace-hack", ] [[package]] @@ -3723,15 +3679,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "convert_case" version = "0.8.0" @@ -3781,7 +3728,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zlog", ] @@ -3830,7 +3776,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -3843,7 +3789,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -3867,7 +3813,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.10.0", "libc", ] @@ -3878,7 +3824,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "block", "cfg-if", "core-foundation 0.10.0", @@ -3956,20 +3902,20 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" dependencies = [ - "bindgen 0.70.1", + "bindgen 0.72.1", ] [[package]] name = "cosmic-text" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1ecbb5db9a4c2ee642df67bcfa8f044dd867dbbaa21bfab139cbc204ffbf67" +checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "fontdb 0.16.2", "log", "rangemap", @@ -3998,7 +3944,7 @@ dependencies = [ "jni", "js-sys", "libc", - "mach2 0.4.2", + "mach2 0.4.3", "ndk", "ndk-context", "num-derive", @@ -4014,9 +3960,9 @@ dependencies = [ [[package]] name = "cpp_demangle" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" dependencies = [ "cfg-if", ] @@ -4063,7 +4009,7 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli", + "gimli 0.31.1", "hashbrown 0.14.5", "log", "postcard", @@ -4073,7 +4019,7 @@ dependencies = [ "serde_derive", "sha2", "smallvec", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", ] [[package]] @@ -4120,7 +4066,7 @@ dependencies = [ "cranelift-codegen", "log", "smallvec", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", ] [[package]] @@ -4137,7 +4083,7 @@ checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" dependencies = [ "cranelift-codegen", "libc", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", ] [[package]] @@ -4148,7 +4094,7 @@ checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" dependencies = [ "cfg-if", "libc", - "mach2 0.4.2", + "mach2 0.4.3", ] [[package]] @@ -4160,7 +4106,7 @@ dependencies = [ "cfg-if", "crash-context", "libc", - "mach2 0.4.2", + "mach2 0.4.3", "parking_lot", ] @@ -4180,7 +4126,6 @@ dependencies = [ "serde_json", "smol", "system_specs", - "workspace-hack", "zstd 0.11.2+zstd.1.5.2", ] @@ -4200,32 +4145,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] -name = "crc32c" -version = "0.6.8" +name = "crc-fast" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" dependencies = [ - "rustc_version", + "crc", + "digest", + "libc", + "rand 0.9.2", + "regex", ] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] -[[package]] -name = "crc64fast-nvme" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4955638f00a809894c947f85a024020a20815b65a5eea633798ea7924edab2b3" -dependencies = [ - "crc", -] - [[package]] name = "credentials_provider" version = "0.1.0" @@ -4237,7 +4177,6 @@ dependencies = [ "release_channel", "serde", "serde_json", - "workspace-hack", ] [[package]] @@ -4338,11 +4277,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "crossterm_winapi", "document-features", "parking_lot", - "rustix 1.0.7", + "rustix 1.1.2", "winapi", ] @@ -4357,9 +4296,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" @@ -4414,14 +4353,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "ctor" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5" +checksum = "ec09e802f5081de6157da9a75701d6c713d8dc3ba52571fd4bd25f412644e8a6" dependencies = [ "ctor-proc-macro", "dtor", @@ -4429,83 +4368,87 @@ dependencies = [ [[package]] name = "ctor-proc-macro" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" [[package]] name = "ctrlc" -version = "3.4.6" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c" +checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" dependencies = [ - "nix 0.29.0", - "windows-sys 0.59.0", + "dispatch", + "nix 0.30.1", + "windows-sys 0.61.2", ] [[package]] name = "cursor-icon" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" [[package]] name = "cxx" -version = "1.0.157" +version = "1.0.187" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6354e975ea4ec28033ec3a36fa9baa1a02e3eb22ad740eeb4929370d4f5ba8" +checksum = "d8465678d499296e2cbf9d3acf14307458fd69b471a31b65b3c519efe8b5e187" dependencies = [ "cc", + "cxx-build", "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", - "foldhash", + "foldhash 0.2.0", "link-cplusplus", ] [[package]] name = "cxx-build" -version = "1.0.157" +version = "1.0.187" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4400e26ea4b99417e4263b1ce2d8452404d750ba0809a7bd043072593d430d" +checksum = "d74b6bcf49ebbd91f1b1875b706ea46545032a14003b5557b7dfa4bbeba6766e" dependencies = [ "cc", - "codespan-reporting", + "codespan-reporting 0.13.0", + "indexmap 2.11.4", "proc-macro2", "quote", "scratch", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "cxxbridge-cmd" -version = "1.0.157" +version = "1.0.187" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31860c98f69fc14da5742c5deaf78983e846c7b27804ca8c8319e32eef421bde" +checksum = "94ca2ad69673c4b35585edfa379617ac364bccd0ba0adf319811ba3a74ffa48a" dependencies = [ "clap", - "codespan-reporting", + "codespan-reporting 0.13.0", + "indexmap 2.11.4", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "cxxbridge-flags" -version = "1.0.157" +version = "1.0.187" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0402a66013f3b8d3d9f2d7c9994656cc81e671054822b0728d7454d9231892f" +checksum = "d29b52102aa395386d77d322b3a0522f2035e716171c2c60aa87cc5e9466e523" [[package]] name = "cxxbridge-macro" -version = "1.0.157" +version = "1.0.187" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c0b38f32d68f3324a981645ee39b2d686af36d03c98a386df3716108c9feae" +checksum = "2a8ebf0b6138325af3ec73324cb3a48b64d57721f17291b151206782e61f66cd" dependencies = [ + "indexmap 2.11.4", "proc-macro2", "quote", - "rustversion", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -4531,7 +4474,7 @@ dependencies = [ "parking_lot", "paths", "proto", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -4542,7 +4485,6 @@ dependencies = [ "tree-sitter", "tree-sitter-go", "util", - "workspace-hack", "zlog", ] @@ -4551,7 +4493,7 @@ name = "dap-types" version = "0.0.1" source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8" dependencies = [ - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", ] @@ -4578,7 +4520,6 @@ dependencies = [ "smol", "task", "util", - "workspace-hack", ] [[package]] @@ -4587,8 +4528,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -4602,7 +4553,21 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.106", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.106", ] [[package]] @@ -4611,9 +4576,20 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -4657,9 +4633,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "data-url" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" [[package]] name = "db" @@ -4676,19 +4652,18 @@ dependencies = [ "sqlez_macros", "tempfile", "util", - "workspace-hack", "zed_env_vars", ] [[package]] name = "dbus" -version = "0.9.7" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" dependencies = [ "libc", "libdbus-sys", - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -4704,7 +4679,6 @@ dependencies = [ "serde_json", "task", "util", - "workspace-hack", ] [[package]] @@ -4728,7 +4702,6 @@ dependencies = [ "smol", "util", "workspace", - "workspace-hack", ] [[package]] @@ -4737,7 +4710,7 @@ version = "0.1.0" dependencies = [ "alacritty_terminal", "anyhow", - "bitflags 2.9.0", + "bitflags 2.9.4", "client", "collections", "command_palette_hooks", @@ -4764,7 +4737,7 @@ dependencies = [ "pretty_assertions", "project", "rpc", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -4784,7 +4757,6 @@ dependencies = [ "unindent", "util", "workspace", - "workspace-hack", "zed_actions", "zlog", ] @@ -4805,17 +4777,16 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", - "workspace-hack", ] [[package]] name = "deflate64" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "denoise" @@ -4827,8 +4798,7 @@ dependencies = [ "realfft", "rodio", "rustfft", - "thiserror 2.0.12", - "workspace-hack", + "thiserror 2.0.17", ] [[package]] @@ -4854,12 +4824,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -4870,20 +4840,20 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "derive_more" -version = "0.99.19" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -4892,8 +4862,7 @@ version = "0.1.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", - "workspace-hack", + "syn 2.0.106", ] [[package]] @@ -4920,7 +4889,7 @@ dependencies = [ "markdown", "pretty_assertions", "project", - "rand 0.9.1", + "rand 0.9.2", "serde", "serde_json", "settings", @@ -4930,7 +4899,6 @@ dependencies = [ "unindent", "util", "workspace", - "workspace-hack", "zlog", ] @@ -5033,8 +5001,8 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.0", - "windows-sys 0.61.0", + "redox_users 0.5.2", + "windows-sys 0.59.0", ] [[package]] @@ -5049,7 +5017,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "objc2", ] @@ -5061,7 +5029,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -5088,7 +5056,6 @@ dependencies = [ "task", "theme", "util", - "workspace-hack", "zed", "zlog", ] @@ -5104,28 +5071,28 @@ dependencies = [ [[package]] name = "documented" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6db32f0995bc4553d2de888999075acd0dbeef75ba923503f6a724263dc6f3" +checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" dependencies = [ "documented-macros", - "phf 0.11.3", - "thiserror 1.0.69", + "phf 0.12.1", + "thiserror 2.0.17", ] [[package]] name = "documented-macros" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a394bb35929b58f9a5fd418f7c6b17a4b616efcc1e53e6995ca123948f87e5fa" +checksum = "1149cf7462e5e79e17a3c05fd5b1f9055092bbfa95e04c319395c3beacc9370f" dependencies = [ - "convert_case 0.6.0", - "itertools 0.13.0", + "convert_case 0.8.0", + "itertools 0.14.0", "optfield", "proc-macro2", "quote", - "strum 0.26.3", - "syn 2.0.101", + "strum 0.27.2", + "syn 2.0.106", ] [[package]] @@ -5187,9 +5154,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "dwrote" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be" +checksum = "9e1b35532432acc8b19ceed096e35dfa088d3ea037fe4f3c085f1f97f33b4d02" dependencies = [ "lazy_static", "libc", @@ -5199,9 +5166,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "dyn-stack" @@ -5215,13 +5182,20 @@ dependencies = [ [[package]] name = "dyn-stack" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490bd48eb68fffcfed519b4edbfd82c69cbe741d175b84f0e0cbe8c57cbe0bdd" +checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" dependencies = [ "bytemuck", + "dyn-stack-macros", ] +[[package]] +name = "dyn-stack-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" + [[package]] name = "ec4rs" version = "1.2.0" @@ -5247,7 +5221,6 @@ dependencies = [ "client", "gpui", "language", - "workspace-hack", ] [[package]] @@ -5278,7 +5251,6 @@ dependencies = [ "theme", "ui", "workspace", - "workspace-hack", "zed_actions", "zeta", ] @@ -5294,7 +5266,7 @@ dependencies = [ "collections", "futures 0.3.31", "gpui", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "indoc", "itertools 0.14.0", "language", @@ -5315,7 +5287,6 @@ dependencies = [ "tree-sitter-cpp", "tree-sitter-go", "util", - "workspace-hack", "zlog", ] @@ -5358,11 +5329,11 @@ dependencies = [ "parking_lot", "pretty_assertions", "project", - "rand 0.9.1", + "rand 0.9.2", "regex", "release_channel", "rpc", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -5392,7 +5363,6 @@ dependencies = [ "uuid", "vim_mode_setting", "workspace", - "workspace-hack", "zed_actions", "zlog", ] @@ -5449,16 +5419,16 @@ dependencies = [ [[package]] name = "embed-resource" -version = "3.0.2" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbc6e0d8e0c03a655b53ca813f0463d2c956bc4db8138dbc89f120b066551e3" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.20", + "toml 0.9.8", "vswhom", - "winreg 0.52.0", + "winreg 0.55.0", ] [[package]] @@ -5512,14 +5482,14 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "enumflags2" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -5527,20 +5497,20 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -5582,18 +5552,39 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" dependencies = [ "serde", + "serde_core", "typeid", ] @@ -5610,9 +5601,9 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.59.0", @@ -5672,9 +5663,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -5687,7 +5678,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.0", + "event-listener 5.4.1", "pin-project-lite", ] @@ -5705,10 +5696,9 @@ dependencies = [ name = "explorer_command_injector" version = "0.1.0" dependencies = [ - "windows 0.61.1", - "windows-core 0.61.0", - "windows-registry 0.5.1", - "workspace-hack", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-registry 0.5.3", ] [[package]] @@ -5756,12 +5746,11 @@ dependencies = [ "serde", "serde_json", "task", - "toml 0.8.20", + "toml 0.8.23", "url", "util", "wasm-encoder 0.221.3", "wasmparser 0.221.3", - "workspace-hack", ] [[package]] @@ -5782,10 +5771,9 @@ dependencies = [ "serde_json", "theme", "tokio", - "toml 0.8.20", + "toml 0.8.23", "tree-sitter", "wasmtime", - "workspace-hack", ] [[package]] @@ -5815,7 +5803,7 @@ dependencies = [ "parking_lot", "paths", "project", - "rand 0.9.1", + "rand 0.9.2", "release_channel", "remote", "reqwest_client", @@ -5829,13 +5817,12 @@ dependencies = [ "tempfile", "theme", "theme_extension", - "toml 0.8.20", + "toml 0.8.23", "url", "util", "wasmparser 0.221.3", "wasmtime", "wasmtime-wasi", - "workspace-hack", "zlog", ] @@ -5870,7 +5857,6 @@ dependencies = [ "util", "vim_mode_setting", "workspace", - "workspace-hack", "zed_actions", ] @@ -5935,6 +5921,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "fd-lock" version = "4.0.4" @@ -5942,7 +5948,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.0.7", + "rustix 1.1.2", "windows-sys 0.59.0", ] @@ -5962,7 +5968,6 @@ dependencies = [ "futures 0.3.31", "gpui", "smol", - "workspace-hack", ] [[package]] @@ -5975,7 +5980,6 @@ dependencies = [ "urlencoding", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -6006,7 +6010,7 @@ dependencies = [ "picker", "pretty_assertions", "project", - "schemars 1.0.1", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -6016,7 +6020,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zlog", ] @@ -6028,7 +6031,6 @@ dependencies = [ "serde", "theme", "util", - "workspace-hack", ] [[package]] @@ -6044,16 +6046,22 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -6062,9 +6070,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "libz-rs-sys", @@ -6091,7 +6099,7 @@ checksum = "4203231de188ebbdfb85c11f3c20ca2b063945710de04e7b59268731e728b462" dependencies = [ "half", "num-traits", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", ] @@ -6136,20 +6144,26 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font-types" -version = "0.8.4" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" +checksum = "511e2c18a516c666d27867d2f9821f76e7d591f762e9fc41dd6cc5c90fe54b0b" dependencies = [ "bytemuck", ] [[package]] name = "fontconfig-parser" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" dependencies = [ "roxmltree", ] @@ -6209,7 +6223,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -6235,9 +6249,9 @@ dependencies = [ [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -6293,8 +6307,7 @@ dependencies = [ "text", "time", "util", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", ] [[package]] @@ -6304,7 +6317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes", - "rustix 1.0.7", + "rustix 1.1.2", "windows-sys 0.59.0", ] @@ -6324,19 +6337,10 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" dependencies = [ - "rustix 1.0.7", + "rustix 1.1.2", "windows-sys 0.59.0", ] -[[package]] -name = "fs_benchmarks" -version = "0.1.0" -dependencies = [ - "fs", - "gpui", - "workspace-hack", -] - [[package]] name = "fs_extra" version = "1.3.0" @@ -6347,13 +6351,12 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" name = "fsevent" version = "0.1.0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.10.0", "fsevent-sys 3.1.0", "log", "parking_lot", "tempfile", - "workspace-hack", ] [[package]] @@ -6472,9 +6475,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand 2.3.0", "futures-core", @@ -6491,7 +6494,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -6533,7 +6536,6 @@ dependencies = [ "gpui", "log", "util", - "workspace-hack", ] [[package]] @@ -6580,7 +6582,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-c32 0.18.2", "gemm-c64 0.18.2", "gemm-common 0.18.2", @@ -6615,7 +6617,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -6645,7 +6647,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -6681,7 +6683,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" dependencies = [ "bytemuck", - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "half", "libm", "num-complex", @@ -6719,7 +6721,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "gemm-f32 0.18.2", "half", @@ -6752,7 +6754,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -6782,7 +6784,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" dependencies = [ - "dyn-stack 0.13.0", + "dyn-stack 0.13.2", "gemm-common 0.18.2", "num-complex", "num-traits", @@ -6791,20 +6793,6 @@ dependencies = [ "seq-macro", ] -[[package]] -name = "generator" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.1", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -6817,46 +6805,46 @@ dependencies = [ [[package]] name = "gethostname" -version = "0.4.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "libc", - "windows-targets 0.48.5", + "rustix 1.1.2", + "windows-link 0.2.1", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] name = "gif" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" dependencies = [ "color_quant", "weezl", @@ -6869,10 +6857,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" dependencies = [ "fallible-iterator", - "indexmap 2.9.0", + "indexmap 2.11.4", "stable_deref_trait", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "git" version = "0.1.0" @@ -6890,33 +6884,32 @@ dependencies = [ "log", "parking_lot", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "regex", "rope", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "smol", "sum_tree", "tempfile", "text", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "unindent", "url", "urlencoding", "util", "uuid", - "workspace-hack", ] [[package]] name = "git2" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5220b8ba44c68a9a7f7a7659e864dd73692e417ef0211bea133c7b74e031eeb9" +checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "libc", "libgit2-sys", "log", @@ -6941,7 +6934,6 @@ dependencies = [ "settings", "url", "util", - "workspace-hack", ] [[package]] @@ -6980,7 +6972,7 @@ dependencies = [ "postage", "pretty_assertions", "project", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -6993,9 +6985,8 @@ dependencies = [ "unindent", "util", "watch", - "windows 0.61.1", + "windows 0.61.3", "workspace", - "workspace-hack", "zed_actions", "zeroize", "zlog", @@ -7003,15 +6994,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" dependencies = [ "aho-corasick", "bstr", @@ -7065,7 +7056,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -7086,12 +7076,11 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", "strum 0.27.2", - "workspace-hack", ] [[package]] @@ -7100,7 +7089,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "gpu-alloc-types", ] @@ -7121,7 +7110,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", ] [[package]] @@ -7143,6 +7132,7 @@ dependencies = [ "calloop-wayland-source", "cbindgen", "cocoa 0.26.0", + "cocoa-foundation 0.2.0", "collections", "core-foundation 0.10.0", "core-foundation-sys", @@ -7183,12 +7173,12 @@ dependencies = [ "postage", "pretty_assertions", "profiling", - "rand 0.9.1", + "rand 0.9.2", "raw-window-handle", "refineable", "reqwest_client", "resvg", - "schemars 1.0.1", + "schemars 1.0.4", "seahash", "semantic_version", "serde", @@ -7200,7 +7190,7 @@ dependencies = [ "strum 0.27.2", "sum_tree", "taffy", - "thiserror 2.0.12", + "thiserror 2.0.17", "unicode-segmentation", "usvg", "util", @@ -7212,11 +7202,10 @@ dependencies = [ "wayland-cursor", "wayland-protocols 0.31.2", "wayland-protocols-plasma", - "windows 0.61.1", - "windows-core 0.61.0", + "windows 0.61.3", + "windows-core 0.61.2", "windows-numerics", - "windows-registry 0.5.1", - "workspace-hack", + "windows-registry 0.5.3", "x11-clipboard", "x11rb", "xkbcommon", @@ -7233,8 +7222,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.101", - "workspace-hack", + "syn 2.0.106", ] [[package]] @@ -7245,7 +7233,6 @@ dependencies = [ "gpui", "tokio", "util", - "workspace-hack", ] [[package]] @@ -7267,9 +7254,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ "bytes 1.10.1", "fnv", @@ -7277,7 +7264,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.9.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -7286,9 +7273,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.9" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes 1.10.1", @@ -7296,7 +7283,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.3.1", - "indexmap 2.9.0", + "indexmap 2.11.4", "slab", "tokio", "tokio-util", @@ -7305,16 +7292,17 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "bytemuck", "cfg-if", "crunchy", "num-traits", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", + "zerocopy", ] [[package]] @@ -7361,19 +7349,19 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "allocator-api2", ] [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", "rayon", "serde", ] @@ -7393,7 +7381,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.5", ] [[package]] @@ -7450,7 +7438,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "heed-traits", "heed-types", @@ -7484,15 +7472,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -7561,7 +7543,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -7585,7 +7567,6 @@ dependencies = [ "markup5ever_rcdom", "pretty_assertions", "regex", - "workspace-hack", ] [[package]] @@ -7671,7 +7652,6 @@ dependencies = [ "tempfile", "url", "util", - "workspace-hack", "zed-reqwest", ] @@ -7679,9 +7659,8 @@ dependencies = [ name = "http_client_tls" version = "0.1.0" dependencies = [ - "rustls 0.23.26", + "rustls 0.23.33", "rustls-platform-verifier", - "workspace-hack", ] [[package]] @@ -7704,9 +7683,9 @@ checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" [[package]] name = "humantime" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -7718,14 +7697,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -7734,19 +7713,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes 1.10.1", "futures-channel", - "futures-util", - "h2 0.4.9", + "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -7770,16 +7751,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", "http 1.3.1", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", - "rustls 0.23.26", - "rustls-native-certs 0.8.1", + "rustls 0.23.33", + "rustls-native-certs 0.8.2", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -7801,19 +7781,23 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ + "base64 0.22.1", "bytes 1.10.1", "futures-channel", + "futures-core", "futures-util", "http 1.3.1", "http-body 1.0.1", - "hyper 1.6.0", + "hyper 1.7.0", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -7821,9 +7805,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -7831,7 +7815,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.62.2", ] [[package]] @@ -7849,26 +7833,26 @@ version = "0.1.0" dependencies = [ "serde", "strum 0.27.2", - "workspace-hack", ] [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", - "yoke", + "potential_utf", + "yoke 0.8.0", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -7877,31 +7861,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -7909,67 +7873,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", - "yoke", + "yoke 0.8.0", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "id-arena" version = "2.2.1" @@ -7984,9 +7935,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -7995,9 +7946,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -8005,9 +7956,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" dependencies = [ "crossbeam-deque", "globset", @@ -8021,9 +7972,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" dependencies = [ "bytemuck", "byteorder-lite", @@ -8031,8 +7982,9 @@ dependencies = [ "exr", "gif", "image-webp", + "moxcms", "num-traits", - "png", + "png 0.18.0", "qoi", "ravif", "rayon", @@ -8044,9 +7996,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", "quick-error", @@ -8070,7 +8022,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -8085,14 +8036,14 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.5", ] [[package]] name = "imgref" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" @@ -8107,13 +8058,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "serde", + "serde_core", ] [[package]] @@ -8124,13 +8076,13 @@ checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "inherent" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13" +checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -8150,7 +8102,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "inotify-sys", "libc", ] @@ -8193,7 +8145,6 @@ dependencies = [ "util", "util_macros", "workspace", - "workspace-hack", "zed_actions", ] @@ -8208,7 +8159,6 @@ dependencies = [ "smol", "util", "workspace", - "workspace-hack", ] [[package]] @@ -8228,14 +8178,14 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] @@ -8258,15 +8208,14 @@ checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" [[package]] name = "io-surface" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8283575d5f0b2e7447ec0840363879d71c0fa325d4c699d5b45208ea4a51f45e" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" dependencies = [ "cgl", "core-foundation 0.10.0", "core-foundation-sys", "leaky-cow", - "libc", ] [[package]] @@ -8289,7 +8238,7 @@ dependencies = [ "fnv", "lazy_static", "libc", - "mio 1.0.3", + "mio 1.1.0", "rand 0.8.5", "serde", "tempfile", @@ -8303,6 +8252,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -8318,7 +8277,7 @@ version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.5.0", + "hermit-abi", "libc", "windows-sys 0.59.0", ] @@ -8366,15 +8325,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -8392,9 +8342,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -8405,13 +8355,13 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -8438,11 +8388,11 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", "libc", ] @@ -8459,20 +8409,13 @@ dependencies = [ "settings", "shellexpand 2.1.2", "workspace", - "workspace-hack", ] -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" - [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -8501,7 +8444,7 @@ dependencies = [ "language", "paths", "project", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -8509,7 +8452,6 @@ dependencies = [ "task", "theme", "util", - "workspace-hack", ] [[package]] @@ -8518,7 +8460,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "base64 0.22.1", "bytecount", "email_address", @@ -8533,7 +8475,7 @@ dependencies = [ "referencing", "regex", "regex-syntax", - "reqwest 0.12.15", + "reqwest 0.12.24", "serde", "serde_json", "uuid-simd", @@ -8620,7 +8562,6 @@ dependencies = [ "util", "vim", "workspace", - "workspace-hack", "zed_actions", ] @@ -8636,9 +8577,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.8" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" dependencies = [ "kqueue-sys", "libc", @@ -8656,11 +8597,12 @@ dependencies = [ [[package]] name = "kurbo" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" dependencies = [ "arrayvec", + "euclid", "smallvec", ] @@ -8698,10 +8640,10 @@ dependencies = [ "parking_lot", "postage", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "regex", "rpc", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -8714,7 +8656,7 @@ dependencies = [ "task", "text", "theme", - "toml 0.8.20", + "toml 0.8.23", "tree-sitter", "tree-sitter-elixir", "tree-sitter-embedded-template", @@ -8730,7 +8672,6 @@ dependencies = [ "unindent", "util", "watch", - "workspace-hack", "zlog", ] @@ -8752,7 +8693,6 @@ dependencies = [ "serde", "serde_json", "util", - "workspace-hack", ] [[package]] @@ -8780,9 +8720,8 @@ dependencies = [ "settings", "smol", "telemetry_events", - "thiserror 2.0.12", + "thiserror 2.0.17", "util", - "workspace-hack", ] [[package]] @@ -8824,20 +8763,19 @@ dependencies = [ "partial-json-fixer", "project", "release_channel", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", "smol", "strum 0.27.2", - "thiserror 2.0.12", + "thiserror 2.0.17", "tiktoken-rs", "tokio", "ui", "ui_input", "util", "vercel", - "workspace-hack", "x_ai", "zed_env_vars", ] @@ -8852,7 +8790,6 @@ dependencies = [ "project", "ui", "workspace", - "workspace-hack", ] [[package]] @@ -8872,7 +8809,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -8900,7 +8836,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", "zlog", ] @@ -8946,7 +8881,7 @@ dependencies = [ "task", "text", "theme", - "toml 0.8.20", + "toml 0.8.23", "tree-sitter", "tree-sitter-bash", "tree-sitter-c", @@ -8969,7 +8904,6 @@ dependencies = [ "url", "util", "workspace", - "workspace-hack", ] [[package]] @@ -8981,12 +8915,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "leak" version = "0.1.2" @@ -9016,21 +8944,21 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lebe" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libdbus-sys" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" dependencies = [ "cc", "pkg-config", @@ -9038,9 +8966,9 @@ dependencies = [ [[package]] name = "libfuzzer-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" dependencies = [ "arbitrary", "cc", @@ -9048,9 +8976,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.1+1.9.0" +version = "0.18.2+1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dcb20f84ffcdd825c7a311ae347cce604a6f084a767dec4a4929829645290e" +checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" dependencies = [ "cc", "libc", @@ -9060,25 +8988,25 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmimalloc-sys" -version = "0.1.42" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" dependencies = [ "cc", "libc", @@ -9086,13 +9014,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "libc", - "redox_syscall 0.5.11", + "redox_syscall 0.5.18", ] [[package]] @@ -9162,14 +9090,13 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] name = "link-cplusplus" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" dependencies = [ "cc", ] @@ -9191,15 +9118,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "litrs" @@ -9243,7 +9170,7 @@ dependencies = [ "parking_lot", "pbjson-types", "prost 0.12.6", - "rand 0.9.1", + "rand 0.9.2", "reqwest 0.11.27", "scopeguard", "serde", @@ -9292,7 +9219,6 @@ dependencies = [ "prost-build 0.9.0", "prost-types 0.9.0", "serde", - "workspace-hack", "zed-reqwest", ] @@ -9332,7 +9258,6 @@ dependencies = [ "tokio-tungstenite 0.26.2", "ui", "util", - "workspace-hack", "zed-scap", ] @@ -9354,45 +9279,30 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", - "workspace-hack", ] [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "serde", "value-bag", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "loop9" version = "0.1.5" @@ -9408,9 +9318,15 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.3", + "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lsp" version = "0.1.0" @@ -9426,12 +9342,11 @@ dependencies = [ "parking_lot", "postage", "release_channel", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "smol", "util", - "workspace-hack", "zlog", ] @@ -9448,9 +9363,9 @@ dependencies = [ [[package]] name = "lyon" -version = "1.0.1" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" +checksum = "dbcb7d54d54c8937364c9d41902d066656817dce1e03a44e5533afebd1ef4352" dependencies = [ "lyon_algorithms", "lyon_extra", @@ -9459,9 +9374,9 @@ dependencies = [ [[package]] name = "lyon_algorithms" -version = "1.0.5" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" +checksum = "f4c0829e28c4f336396f250d850c3987e16ce6db057ffe047ce0dd54aab6b647" dependencies = [ "lyon_path", "num-traits", @@ -9479,9 +9394,9 @@ dependencies = [ [[package]] name = "lyon_geom" -version = "1.0.6" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" +checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" dependencies = [ "arrayvec", "euclid", @@ -9490,9 +9405,9 @@ dependencies = [ [[package]] name = "lyon_path" -version = "1.0.7" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0047f508cd7a85ad6bad9518f68cce7b1bf6b943fb71f6da0ee3bc1e8cb75f25" +checksum = "1aeca86bcfd632a15984ba029b539ffb811e0a70bf55e814ef8b0f54f506fdeb" dependencies = [ "lyon_geom", "num-traits", @@ -9500,9 +9415,9 @@ dependencies = [ [[package]] name = "lyon_tessellation" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" +checksum = "f3f586142e1280335b1bc89539f7c97dd80f08fc43e9ab1b74ef0a42b04aa353" dependencies = [ "float_next_after", "lyon_path", @@ -9536,9 +9451,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mach2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] @@ -9589,7 +9504,6 @@ dependencies = [ "theme", "ui", "util", - "workspace-hack", ] [[package]] @@ -9614,7 +9528,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -9662,7 +9575,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -9754,29 +9667,28 @@ dependencies = [ "foreign-types 0.5.0", "metal", "objc", - "workspace-hack", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memfd" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 0.38.44", + "rustix 1.1.2", ] [[package]] name = "memmap2" -version = "0.9.5" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", "stable_deref_trait", @@ -9796,7 +9708,6 @@ name = "menu" version = "0.1.0" dependencies = [ "gpui", - "workspace-hack", ] [[package]] @@ -9805,7 +9716,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "block", "core-graphics-types 0.1.3", "foreign-types 0.5.0", @@ -9830,14 +9741,13 @@ dependencies = [ "tree-sitter", "tree-sitter-json", "unindent", - "workspace-hack", ] [[package]] name = "mimalloc" -version = "0.1.46" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" dependencies = [ "libmimalloc-sys", ] @@ -9864,7 +9774,7 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "debugid", "num-derive", "num-traits", @@ -9879,14 +9789,14 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "cfg-if", "crash-context", "goblin", "libc", "log", - "mach2 0.4.2", + "mach2 0.4.3", "memmap2", "memoffset", "minidump-common", @@ -9923,9 +9833,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -9945,29 +9855,29 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys 0.48.0", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "miow" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044" +checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -9977,32 +9887,40 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "strum 0.27.2", - "workspace-hack", ] [[package]] name = "moka" -version = "0.12.10" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" dependencies = [ "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", - "loom", + "equivalent", "parking_lot", "portable-atomic", "rustc_version", "smallvec", "tagptr", - "thiserror 1.0.69", "uuid", ] +[[package]] +name = "moxcms" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "msvc_spectre_libs" version = "0.1.3" @@ -10029,7 +9947,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "project", - "rand 0.9.1", + "rand 0.9.2", "rope", "serde", "settings", @@ -10040,7 +9958,6 @@ dependencies = [ "theme", "tree-sitter", "util", - "workspace-hack", "zlog", ] @@ -10050,6 +9967,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "naga" version = "25.0.1" @@ -10058,20 +9981,20 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set 0.8.0", - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg_aliases 0.2.1", - "codespan-reporting", + "codespan-reporting 0.12.0", "half", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "hexf-parse", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "num-traits", "once_cell", "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "thiserror 2.0.12", + "thiserror 2.0.17", "unicode-ident", ] @@ -10090,7 +10013,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -10132,7 +10055,6 @@ dependencies = [ "futures 0.3.31", "net", "smol", - "workspace-hack", ] [[package]] @@ -10141,7 +10063,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "jni-sys", "log", "ndk-sys", @@ -10172,8 +10094,7 @@ dependencies = [ "async-io", "smol", "tempfile", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", ] [[package]] @@ -10188,7 +10109,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg-if", "cfg_aliases 0.1.1", "libc", @@ -10200,7 +10121,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -10212,7 +10133,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -10239,7 +10160,6 @@ dependencies = [ "util", "watch", "which 6.0.3", - "workspace-hack", ] [[package]] @@ -10269,11 +10189,11 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "normpath" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed" +checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10294,7 +10214,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -10304,7 +10223,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "crossbeam-channel", "filetime", "fsevent-sys 4.1.0", @@ -10322,14 +10241,14 @@ name = "notify" version = "8.0.0" source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "filetime", "fsevent-sys 4.1.0", "inotify 0.11.0", "kqueue", "libc", "log", - "mio 1.0.3", + "mio 1.1.0", "notify-types", "walkdir", "windows-sys 0.59.0", @@ -10371,11 +10290,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10450,7 +10369,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -10506,33 +10425,34 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", ] [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -10581,9 +10501,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -10594,7 +10514,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -10607,7 +10527,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "libc", "objc2", "objc2-core-audio", @@ -10618,9 +10538,9 @@ dependencies = [ [[package]] name = "objc2-core-audio" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" dependencies = [ "dispatch2", "objc2", @@ -10630,21 +10550,21 @@ dependencies = [ [[package]] name = "objc2-core-audio-types" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "objc2", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "dispatch2", "objc2", ] @@ -10661,16 +10581,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "objc2", "objc2-core-foundation", ] [[package]] name = "objc2-io-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ "libc", "objc2-core-foundation", @@ -10682,7 +10602,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "block2", "dispatch2", "objc2", @@ -10696,7 +10616,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -10709,7 +10629,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -10741,8 +10661,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "crc32fast", - "hashbrown 0.15.3", - "indexmap 2.9.0", + "hashbrown 0.15.5", + "indexmap 2.11.4", + "memchr", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ "memchr", ] @@ -10761,18 +10690,18 @@ dependencies = [ "http 1.3.1", "http-body-util", "humantime", - "hyper 1.6.0", + "hyper 1.7.0", "itertools 0.14.0", "parking_lot", "percent-encoding", "quick-xml 0.38.3", - "rand 0.9.1", - "reqwest 0.12.15", + "rand 0.9.2", + "reqwest 0.12.24", "ring", "serde", "serde_json", "serde_urlencoded", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -10788,11 +10717,10 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", - "workspace-hack", ] [[package]] @@ -10812,7 +10740,7 @@ dependencies = [ "notifications", "picker", "project", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "settings", "telemetry", @@ -10822,7 +10750,6 @@ dependencies = [ "util", "vim_mode_setting", "workspace", - "workspace-hack", "zed_actions", "zlog", ] @@ -10833,6 +10760,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "oo7" version = "0.5.0" @@ -10849,16 +10782,16 @@ dependencies = [ "cipher", "digest", "endi", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "futures-util", - "getrandom 0.3.2", + "getrandom 0.3.4", "hkdf", "hmac", "md-5", "num", "num-bigint-dig", "pbkdf2 0.12.2", - "rand 0.9.1", + "rand 0.9.2", "serde", "sha2", "subtle", @@ -10893,12 +10826,11 @@ dependencies = [ "futures 0.3.31", "http_client", "log", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", "strum 0.27.2", - "workspace-hack", ] [[package]] @@ -10908,13 +10840,12 @@ dependencies = [ "anyhow", "futures 0.3.31", "http_client", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", "strum 0.27.2", - "thiserror 2.0.12", - "workspace-hack", + "thiserror 2.0.17", ] [[package]] @@ -10931,11 +10862,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cfg-if", "foreign-types 0.3.2", "libc", @@ -10952,7 +10883,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -10963,9 +10894,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -10975,13 +10906,13 @@ dependencies = [ [[package]] name = "optfield" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa59f025cde9c698fcb4fcb3533db4621795374065bee908215263488f2d2a1d" +checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -11039,7 +10970,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -11065,7 +10996,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -11097,7 +11027,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "worktree", "zed_actions", ] @@ -11149,7 +11078,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -11162,7 +11091,6 @@ dependencies = [ "theme", "ui", "workspace", - "workspace-hack", ] [[package]] @@ -11173,9 +11101,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -11183,15 +11111,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.11", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -11269,7 +11197,6 @@ dependencies = [ "dirs 4.0.0", "ignore", "util", - "workspace-hack", ] [[package]] @@ -11339,12 +11266,12 @@ checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61" [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -11358,9 +11285,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" @@ -11369,25 +11296,23 @@ dependencies = [ "collections", "serde", "serde_json", - "workspace-hack", ] [[package]] name = "pest" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.12", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -11395,24 +11320,23 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "pest_meta" -version = "2.8.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ - "once_cell", "pest", "sha2", ] @@ -11652,7 +11576,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "toml 0.8.20", + "toml 0.8.23", ] [[package]] @@ -11797,14 +11721,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.11.4", ] [[package]] name = "pgvector" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0e8871b6d7ca78348c6cd29b911b94851f3429f0cd403130ca17f26c1fb91a6" +checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" dependencies = [ "serde", ] @@ -11815,7 +11739,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", + "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -11825,6 +11749,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" dependencies = [ + "phf_macros 0.12.1", "phf_shared 0.12.1", ] @@ -11834,7 +11759,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", ] @@ -11848,17 +11773,40 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" +dependencies = [ + "fastrand 2.3.0", + "phf_shared 0.12.1", +] + [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", +] + +[[package]] +name = "phf_macros" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" +dependencies = [ + "phf_generator 0.12.1", + "phf_shared 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -11889,13 +11837,12 @@ dependencies = [ "env_logger 0.11.8", "gpui", "menu", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "theme", "ui", "workspace", - "workspace-hack", ] [[package]] @@ -11921,7 +11868,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -11997,18 +11944,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3daf8e3d4b712abe1d690838f6e29fb76b76ea19589c4afa39ec30e12f62af71" dependencies = [ "array-init-cursor", - "hashbrown 0.15.3", + "hashbrown 0.15.5", ] [[package]] name = "plist" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.9.0", - "quick-xml 0.32.0", + "indexmap 2.11.4", + "quick-xml 0.38.3", "serde", "time", ] @@ -12054,14 +12001,27 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.9.4", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polars" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5f7feb5d56b954e691dff22a8b2d78d77433dcc93c35fe21c3777fdc121b697" dependencies = [ - "getrandom 0.2.15", - "getrandom 0.3.2", + "getrandom 0.2.16", + "getrandom 0.3.4", "polars-arrow", "polars-core", "polars-error", @@ -12082,16 +12042,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b4fed2343961b3eea3db2cee165540c3e1ad9d5782350cc55a9e76cf440148" dependencies = [ "atoi_simd", - "bitflags 2.9.0", + "bitflags 2.9.4", "bytemuck", "chrono", "chrono-tz", "dyn-clone", "either", "ethnum", - "getrandom 0.2.15", - "getrandom 0.3.2", - "hashbrown 0.15.3", + "getrandom 0.2.16", + "getrandom 0.3.4", + "hashbrown 0.15.5", "itoa", "lz4", "num-traits", @@ -12102,7 +12062,7 @@ dependencies = [ "serde", "simdutf8", "streaming-iterator", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "version_check", "zstd 0.13.3", ] @@ -12128,18 +12088,18 @@ dependencies = [ "chrono", "either", "fast-float2", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "itoa", "num-traits", "polars-arrow", "polars-error", "polars-utils", - "rand 0.9.1", + "rand 0.9.2", "ryu", "serde", "skiplist", "strength_reduce", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "version_check", ] @@ -12149,15 +12109,15 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e77b1f08ef6dbb032bb1d0d3365464be950df9905f6827a95b24c4ca5518901d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "boxcar", "bytemuck", "chrono", "chrono-tz", "comfy-table", "either", - "hashbrown 0.15.3", - "indexmap 2.9.0", + "hashbrown 0.15.5", + "indexmap 2.11.4", "itoa", "num-traits", "polars-arrow", @@ -12167,13 +12127,13 @@ dependencies = [ "polars-row", "polars-schema", "polars-utils", - "rand 0.9.1", + "rand 0.9.2", "rand_distr", "rayon", "regex", "serde", "serde_json", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "uuid", "version_check", "xxhash-rust", @@ -12186,7 +12146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89c43d0ea57168be4546c4d8064479ed8b29a9c79c31a0c7c367ee734b9b7158" dependencies = [ "boxcar", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "polars-arrow", "polars-error", "polars-utils", @@ -12214,8 +12174,8 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343931b818cf136349135ba11dbc18c27683b52c3477b1ba8ca606cf5ab1965c" dependencies = [ - "bitflags 2.9.0", - "hashbrown 0.15.3", + "bitflags 2.9.4", + "hashbrown 0.15.5", "num-traits", "polars-arrow", "polars-compute", @@ -12226,7 +12186,7 @@ dependencies = [ "polars-row", "polars-time", "polars-utils", - "rand 0.9.1", + "rand 0.9.2", "rayon", "recursive", ] @@ -12246,7 +12206,7 @@ dependencies = [ "fs4", "futures 0.3.31", "glob", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "home", "itoa", "memchr", @@ -12263,7 +12223,7 @@ dependencies = [ "polars-utils", "rayon", "regex", - "reqwest 0.12.15", + "reqwest 0.12.24", "ryu", "serde", "serde_json", @@ -12279,7 +12239,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fb6e2c6c2fa4ea0c660df1c06cf56960c81e7c2683877995bae3d4e3d408147" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "chrono", "either", "memchr", @@ -12332,9 +12292,9 @@ dependencies = [ "chrono", "chrono-tz", "either", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "hex", - "indexmap 2.9.0", + "indexmap 2.11.4", "libm", "memchr", "num-traits", @@ -12347,7 +12307,7 @@ dependencies = [ "rayon", "regex", "regex-syntax", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "unicode-normalization", "unicode-reverse", "version_check", @@ -12366,7 +12326,7 @@ dependencies = [ "ethnum", "flate2", "futures 0.3.31", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "lz4", "num-traits", "polars-arrow", @@ -12397,14 +12357,14 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd3a2e33ae4484fe407ab2d2ba5684f0889d1ccf3ad6b844103c03638e6d0a0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "bytemuck", "bytes 1.10.1", "chrono", "chrono-tz", "either", "futures 0.3.31", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "memmap2", "num-traits", "percent-encoding", @@ -12421,7 +12381,7 @@ dependencies = [ "recursive", "regex", "sha2", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "version_check", ] @@ -12431,7 +12391,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18734f17e0e348724df3ae65f3ee744c681117c04b041cac969dfceb05edabc0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "bytemuck", "polars-arrow", "polars-compute", @@ -12446,7 +12406,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6c1ab13e04d5167661a9854ed1ea0482b2ed9b8a0f1118dabed7cd994a85e3" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.4", "polars-error", "polars-utils", "serde", @@ -12459,7 +12419,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4e7766da02cc1d464994404d3e88a7a0ccd4933df3627c325480fbd9bbc0a11" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "hex", "polars-core", "polars-error", @@ -12468,7 +12428,7 @@ dependencies = [ "polars-plan", "polars-time", "polars-utils", - "rand 0.9.1", + "rand 0.9.2", "regex", "serde", "sqlparser", @@ -12480,10 +12440,10 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31f6c6ca1ea01f9dea424d167e4f33f5ec44cd67fbfac9efd40575ed20521f14" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-trait", "atomic-waker", - "bitflags 2.9.0", + "bitflags 2.9.4", "crossbeam-channel", "crossbeam-deque", "crossbeam-queue", @@ -12503,7 +12463,7 @@ dependencies = [ "polars-parquet", "polars-plan", "polars-utils", - "rand 0.9.1", + "rand 0.9.2", "rayon", "recursive", "slotmap", @@ -12532,7 +12492,7 @@ dependencies = [ "polars-utils", "rayon", "regex", - "strum_macros 0.27.1", + "strum_macros 0.27.2", ] [[package]] @@ -12547,14 +12507,14 @@ dependencies = [ "compact_str", "either", "flate2", - "foldhash", - "hashbrown 0.15.3", - "indexmap 2.9.0", + "foldhash 0.1.5", + "hashbrown 0.15.5", + "indexmap 2.11.4", "libc", "memmap2", "num-traits", "polars-error", - "rand 0.9.1", + "rand 0.9.2", "raw-cpuid 11.6.0", "rayon", "regex", @@ -12576,10 +12536,10 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.0", + "hermit-abi", "pin-project-lite", - "rustix 1.0.7", - "windows-sys 0.61.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] @@ -12590,9 +12550,9 @@ checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -12643,9 +12603,9 @@ dependencies = [ [[package]] name = "postcard" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -12653,6 +12613,15 @@ dependencies = [ "serde", ] +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -12665,7 +12634,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.8.24", + "zerocopy", ] [[package]] @@ -12691,7 +12660,6 @@ dependencies = [ "serde", "serde_json", "util", - "workspace-hack", ] [[package]] @@ -12706,12 +12674,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -12725,11 +12693,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.7", ] [[package]] @@ -12751,14 +12719,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -12771,7 +12739,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "version_check", "yansi", ] @@ -12782,27 +12750,27 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "hex", ] [[package]] name = "profiling" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" dependencies = [ "profiling-procmacros", ] [[package]] name = "profiling-procmacros" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -12834,7 +12802,7 @@ dependencies = [ "gpui", "http_client", "image", - "indexmap 2.9.0", + "indexmap 2.11.4", "itertools 0.14.0", "language", "log", @@ -12846,12 +12814,12 @@ dependencies = [ "postage", "prettier", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "regex", "release_channel", "remote", "rpc", - "schemars 1.0.1", + "schemars 1.0.4", "semver", "serde", "serde_json", @@ -12868,13 +12836,12 @@ dependencies = [ "tempfile", "terminal", "text", - "toml 0.8.20", + "toml 0.8.23", "unindent", "url", "util", "watch", "which 6.0.3", - "workspace-hack", "worktree", "zeroize", "zlog", @@ -12900,7 +12867,7 @@ dependencies = [ "pretty_assertions", "project", "rayon", - "schemars 1.0.1", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -12911,7 +12878,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "worktree", "zed_actions", ] @@ -12936,7 +12902,6 @@ dependencies = [ "theme", "util", "workspace", - "workspace-hack", ] [[package]] @@ -12951,7 +12916,7 @@ dependencies = [ "memchr", "parking_lot", "protobuf", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -12978,7 +12943,6 @@ dependencies = [ "text", "util", "uuid", - "workspace-hack", ] [[package]] @@ -13012,7 +12976,7 @@ dependencies = [ "itertools 0.10.5", "lazy_static", "log", - "multimap", + "multimap 0.8.3", "petgraph", "prost 0.9.0", "prost-types 0.9.0", @@ -13031,14 +12995,14 @@ dependencies = [ "heck 0.5.0", "itertools 0.12.1", "log", - "multimap", + "multimap 0.10.1", "once_cell", "petgraph", "prettyplease", "prost 0.12.6", "prost-types 0.12.6", "regex", - "syn 2.0.101", + "syn 2.0.106", "tempfile", ] @@ -13065,7 +13029,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -13097,7 +13061,6 @@ dependencies = [ "prost-build 0.9.0", "serde", "typed-path", - "workspace-hack", ] [[package]] @@ -13122,9 +13085,9 @@ dependencies = [ [[package]] name = "psm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" +checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" dependencies = [ "cc", ] @@ -13155,7 +13118,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "memchr", "pulldown-cmark-escape", "unicase", @@ -13167,7 +13130,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "memchr", "unicase", ] @@ -13216,6 +13179,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + [[package]] name = "qoi" version = "0.4.1" @@ -13242,18 +13214,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", -] - -[[package]] -name = "quick-xml" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] @@ -13270,9 +13233,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.7" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes 1.10.1", "cfg_aliases 0.2.1", @@ -13280,9 +13243,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.26", - "socket2", - "thiserror 2.0.12", + "rustls 0.23.33", + "socket2 0.6.1", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -13290,19 +13253,20 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes 1.10.1", - "getrandom 0.3.2", - "rand 0.9.1", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.26", + "rustls 0.23.33", "rustls-pki-types", "slab", - "thiserror 2.0.12", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -13310,32 +13274,32 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.11" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2", + "socket2 0.6.1", "tracing", "windows-sys 0.59.0", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "radium" @@ -13356,9 +13320,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", @@ -13390,7 +13354,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -13399,7 +13363,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", ] [[package]] @@ -13409,7 +13373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand 0.9.1", + "rand 0.9.2", ] [[package]] @@ -13423,9 +13387,9 @@ dependencies = [ [[package]] name = "rangemap" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" +checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" [[package]] name = "rav1e" @@ -13464,9 +13428,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.11.12" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" dependencies = [ "avif-serialize", "imgref", @@ -13492,7 +13456,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", ] [[package]] @@ -13515,9 +13479,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -13525,9 +13489,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -13535,9 +13499,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.25.3" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f9e8a4f503e5c8750e4cd3b32a4e090035c46374b305a15c70bad833dca05f" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" dependencies = [ "bytemuck", "font-types", @@ -13592,9 +13556,8 @@ dependencies = [ "theme", "ui", "util", - "windows-registry 0.6.0", + "windows-registry 0.6.1", "workspace", - "workspace-hack", "zed_actions", ] @@ -13615,7 +13578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76009fbe0614077fc1a2ce255e3a1881a2e3a3527097d5dc6d8212c585e7e38b" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -13629,11 +13592,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", ] [[package]] @@ -13642,40 +13605,40 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", "thiserror 1.0.69", ] [[package]] name = "redox_users" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "libredox", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -13684,7 +13647,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "fluent-uri", "once_cell", "parking_lot", @@ -13697,7 +13660,6 @@ name = "refineable" version = "0.1.0" dependencies = [ "derive_refineable", - "workspace-hack", ] [[package]] @@ -13708,7 +13670,7 @@ checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" dependencies = [ "allocator-api2", "bumpalo", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "log", "rustc-hash 2.1.1", "serde", @@ -13717,9 +13679,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -13729,9 +13691,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -13740,22 +13702,21 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "release_channel" version = "0.1.0" dependencies = [ "gpui", - "workspace-hack", ] [[package]] @@ -13781,11 +13742,10 @@ dependencies = [ "shlex", "smol", "tempfile", - "thiserror 2.0.12", + "thiserror 2.0.17", "urlencoding", "util", "which 6.0.3", - "workspace-hack", ] [[package]] @@ -13845,8 +13805,8 @@ dependencies = [ "smol", "sysinfo 0.37.2", "task", - "thiserror 2.0.12", - "toml 0.8.20", + "thiserror 2.0.17", + "toml 0.8.23", "unindent", "util", "watch", @@ -13912,7 +13872,6 @@ dependencies = [ "util", "uuid", "workspace", - "workspace-hack", ] [[package]] @@ -13926,7 +13885,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.26", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.32", @@ -13961,33 +13920,29 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes 1.10.1", "futures-channel", "futures-core", "futures-util", - "h2 0.4.9", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper 1.7.0", + "hyper-rustls 0.27.7", "hyper-util", - "ipnet", "js-sys", "log", - "mime", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.26", - "rustls-native-certs 0.8.1", - "rustls-pemfile 2.2.0", + "rustls 0.23.33", + "rustls-native-certs 0.8.2", "rustls-pki-types", "serde", "serde_json", @@ -13997,13 +13952,13 @@ dependencies = [ "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", + "tower-http 0.6.6", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry 0.4.0", ] [[package]] @@ -14020,7 +13975,6 @@ dependencies = [ "regex", "serde", "tokio", - "workspace-hack", "zed-reqwest", ] @@ -14051,9 +14005,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" dependencies = [ "bytemuck", ] @@ -14070,7 +14024,6 @@ dependencies = [ "theme", "ui", "util", - "workspace-hack", ] [[package]] @@ -14081,7 +14034,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -14151,7 +14104,7 @@ dependencies = [ [[package]] name = "rodio" version = "0.21.1" -source = "git+https://github.com/RustAudio/rodio#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b" +source = "git+https://github.com/RustAudio/rodio?rev=e2074c6c2acf07b57cf717e076bdda7a9ac6e70b#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b" dependencies = [ "cpal", "dasp_sample", @@ -14159,7 +14112,7 @@ dependencies = [ "num-rational", "rtrb", "symphonia", - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -14171,13 +14124,12 @@ dependencies = [ "ctor", "gpui", "log", - "rand 0.9.1", + "rand 0.9.2", "rayon", "smallvec", "sum_tree", "unicode-segmentation", "util", - "workspace-hack", "zlog", ] @@ -14200,7 +14152,7 @@ dependencies = [ "gpui", "parking_lot", "proto", - "rand 0.9.1", + "rand 0.9.2", "rsa", "serde", "serde_json", @@ -14208,7 +14160,6 @@ dependencies = [ "strum 0.27.2", "tracing", "util", - "workspace-hack", "zlog", "zstd 0.11.2+zstd.1.5.2", ] @@ -14262,7 +14213,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -14293,9 +14243,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.7.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d" +checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -14304,22 +14254,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e" +checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.101", + "syn 2.0.106", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.0" +version = "8.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21" +checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" dependencies = [ "globset", "sha2", @@ -14338,9 +14288,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.38.0" +version = "1.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8975fc98059f365204d635119cf9c5a60ae67b841ed49b5422a9a7e56cdfac0" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" dependencies = [ "arrayvec", "borsh", @@ -14354,9 +14304,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -14381,9 +14331,9 @@ dependencies = [ [[package]] name = "rustfft" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4" +checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" dependencies = [ "num-complex", "num-integer", @@ -14399,8 +14349,8 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", - "errno 0.3.11", + "bitflags 2.9.4", + "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", @@ -14408,14 +14358,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.0", - "errno 0.3.11", + "bitflags 2.9.4", + "errno 0.3.14", "libc", - "linux-raw-sys 0.9.4", + "linux-raw-sys 0.11.0", "windows-sys 0.59.0", ] @@ -14426,7 +14376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" dependencies = [ "once_cell", - "rustix 1.0.7", + "rustix 1.1.2", ] [[package]] @@ -14435,9 +14385,9 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393" dependencies = [ - "errno 0.3.11", + "errno 0.3.14", "libc", - "rustix 1.0.7", + "rustix 1.1.2", ] [[package]] @@ -14454,16 +14404,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.26" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.1", + "rustls-webpki 0.103.7", "subtle", "zeroize", ] @@ -14482,14 +14432,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework 3.5.1", ] [[package]] @@ -14522,20 +14472,20 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" dependencies = [ "core-foundation 0.10.0", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.26", - "rustls-native-certs 0.8.1", + "rustls 0.23.33", + "rustls-native-certs 0.8.2", "rustls-platform-verifier-android", - "rustls-webpki 0.103.1", - "security-framework 3.2.0", + "rustls-webpki 0.103.7", + "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", "windows-sys 0.59.0", @@ -14559,9 +14509,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" dependencies = [ "aws-lc-rs", "ring", @@ -14571,9 +14521,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" @@ -14581,7 +14531,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "bytemuck", "libm", "smallvec", @@ -14598,7 +14548,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "bytemuck", "core_maths", "log", @@ -14646,11 +14596,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -14662,8 +14612,7 @@ dependencies = [ "chrono", "futures 0.3.31", "parking_lot", - "rand 0.9.1", - "workspace-hack", + "rand 0.9.2", ] [[package]] @@ -14673,11 +14622,10 @@ dependencies = [ "anyhow", "clap", "env_logger 0.11.8", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "theme", - "workspace-hack", ] [[package]] @@ -14694,12 +14642,12 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ "dyn-clone", - "indexmap 2.9.0", + "indexmap 2.11.4", "ref-cast", "schemars_derive", "serde", @@ -14708,14 +14656,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ca9fcb757952f8e8629b9ab066fc62da523c46c2b247b1708a3be06dd82530b" +checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -14732,9 +14680,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scratch" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" [[package]] name = "screencapturekit" @@ -14776,7 +14724,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -14811,7 +14759,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -14836,7 +14784,7 @@ dependencies = [ "serde_json", "sqlx", "strum 0.26.3", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "url", @@ -14853,15 +14801,15 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.101", + "syn 2.0.106", "unicode-ident", ] [[package]] name = "sea-query" -version = "0.32.4" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d99447c24da0cded00089e2021e1624af90878c65f7534319448d01da3df869d" +checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" dependencies = [ "bigdecimal", "chrono", @@ -14901,7 +14849,7 @@ version = "0.1.0" dependencies = [ "any_vec", "anyhow", - "bitflags 2.9.0", + "bitflags 2.9.4", "client", "collections", "editor", @@ -14910,7 +14858,7 @@ dependencies = [ "language", "menu", "project", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -14920,7 +14868,6 @@ dependencies = [ "unindent", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -14944,7 +14891,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -14953,11 +14900,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -14966,9 +14913,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -14986,16 +14933,16 @@ version = "0.1.0" dependencies = [ "anyhow", "serde", - "workspace-hack", ] [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] @@ -15006,9 +14953,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.221" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -15016,22 +14963,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.221" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.221" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -15042,7 +14989,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -15056,14 +15003,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.144" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56177480b00303e689183f110b4e727bb4211d692c62d4fcd16d02be93077d40" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.4", "itoa", "memchr", "ryu", + "serde", "serde_core", ] @@ -15073,7 +15021,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.4", "itoa", "memchr", "ryu", @@ -15082,12 +15030,13 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -15098,18 +15047,27 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_stacker" version = "0.1.14" @@ -15135,18 +15093,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.13.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.11.4", "schemars 0.9.0", - "serde", - "serde_derive", + "schemars 1.0.4", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -15154,21 +15112,21 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.13.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "serial2" -version = "0.2.29" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d1d08630509d69f90eff4afcd02c3bd974d979225cbd815ff5942351b14375" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" dependencies = [ "cfg-if", "libc", @@ -15184,7 +15142,6 @@ dependencies = [ "serde_json", "util", "uuid", - "workspace-hack", ] [[package]] @@ -15205,7 +15162,7 @@ dependencies = [ "pretty_assertions", "release_channel", "rust-embed", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -15219,7 +15176,6 @@ dependencies = [ "tree-sitter-json", "unindent", "util", - "workspace-hack", "zlog", ] @@ -15229,8 +15185,7 @@ version = "0.1.0" dependencies = [ "quote", "settings", - "syn 2.0.101", - "workspace-hack", + "syn 2.0.106", ] [[package]] @@ -15250,7 +15205,6 @@ dependencies = [ "theme", "ui", "workspace", - "workspace-hack", "zed_actions", ] @@ -15276,7 +15230,7 @@ dependencies = [ "paths", "pretty_assertions", "project", - "schemars 1.0.1", + "schemars 1.0.4", "search", "serde", "session", @@ -15288,7 +15242,6 @@ dependencies = [ "ui_input", "util", "workspace", - "workspace-hack", "zed_actions", "zlog", ] @@ -15372,9 +15325,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" dependencies = [ "libc", "signal-hook-registry", @@ -15382,9 +15335,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -15438,7 +15391,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", ] @@ -15474,15 +15427,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f354fd282d3177c2951004953e2fdc4cb342fa159bbee8b829852b6a081c8ea1" dependencies = [ - "rand 0.9.1", - "thiserror 2.0.12", + "rand 0.9.2", + "thiserror 2.0.17", ] [[package]] name = "skrifa" -version = "0.26.6" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc1aa86c26dbb1b63875a7180aa0819709b33348eb5b1491e4321fae388179d" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" dependencies = [ "bytemuck", "read-fonts", @@ -15490,12 +15443,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slash_commands_example" @@ -15515,9 +15465,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" dependencies = [ "serde", ] @@ -15530,7 +15480,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -15539,7 +15489,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", "async-fs", "async-io", @@ -15547,7 +15497,7 @@ dependencies = [ "async-net", "async-process", "blocking", - "futures-lite 2.6.0", + "futures-lite 2.6.1", ] [[package]] @@ -15568,7 +15518,6 @@ version = "0.1.0" dependencies = [ "anyhow", "smallvec", - "workspace-hack", ] [[package]] @@ -15584,13 +15533,12 @@ dependencies = [ "indoc", "parking_lot", "paths", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", "snippet", "util", - "workspace-hack", ] [[package]] @@ -15608,7 +15556,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -15628,24 +15575,34 @@ checksum = "9c09121507da587d3434e5929ce3321162f36bd3eff403873cb163c06b176913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "spdx" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" +checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" dependencies = [ "smallvec", ] @@ -15665,7 +15622,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", ] [[package]] @@ -15710,7 +15667,6 @@ dependencies = [ "thread_local", "util", "uuid", - "workspace-hack", ] [[package]] @@ -15719,8 +15675,7 @@ version = "0.1.0" dependencies = [ "sqlez", "sqlformat", - "syn 2.0.101", - "workspace-hack", + "syn 2.0.106", ] [[package]] @@ -15744,9 +15699,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3a85280daca669cfd3bcb68a337882a8bc57ec882f72c5d13a430613a738e" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" dependencies = [ "sqlx-core", "sqlx-macros", @@ -15757,9 +15712,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f743f2a3cea30a58cd479013f75550e879009e3a02f616f18ca699335aa248c3" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64 0.22.1", "bigdecimal", @@ -15768,25 +15723,25 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener 5.4.0", + "event-listener 5.4.1", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.3", + "hashbrown 0.15.5", "hashlink 0.10.0", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "memchr", "once_cell", "percent-encoding", "rust_decimal", - "rustls 0.23.26", + "rustls 0.23.33", "serde", "serde_json", "sha2", "smallvec", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -15798,22 +15753,22 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4200e0fde19834956d4252347c12a083bdcb237d7a1a1446bffd8768417dce" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" dependencies = [ "proc-macro2", "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "sqlx-macros-core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "882ceaa29cade31beca7129b6beeb05737f44f82dbe2a9806ecea5a7093d00b7" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" dependencies = [ "dotenvy", "either", @@ -15829,22 +15784,21 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.101", - "tempfile", + "syn 2.0.106", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0afdd3aa7a629683c2d750c2df343025545087081ab5942593a5288855b1b7a7" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "bytes 1.10.1", "chrono", @@ -15875,7 +15829,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "uuid", @@ -15884,14 +15838,14 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bedbe1bbb5e2615ef347a5e9d8cd7680fb63e77d9dafc0f29be15e53f1ebe6" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "chrono", "crc", @@ -15918,7 +15872,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "uuid", @@ -15927,9 +15881,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c26083e9a520e8eb87a06b12347679b142dc2ea29e6e409f805644a7a979a5bc" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", "chrono", @@ -15945,7 +15899,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.12", + "thiserror 2.0.17", "time", "tracing", "url", @@ -15954,15 +15908,15 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" dependencies = [ "cc", "cfg-if", @@ -15989,7 +15943,7 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" dependencies = [ "proc-macro-error2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -16014,7 +15968,6 @@ dependencies = [ "gpui", "itertools 0.14.0", "smallvec", - "workspace-hack", ] [[package]] @@ -16045,7 +15998,6 @@ dependencies = [ "title_bar", "ui", "workspace", - "workspace-hack", ] [[package]] @@ -16068,10 +16020,9 @@ name = "streaming_diff" version = "0.1.0" dependencies = [ "ordered-float 2.10.1", - "rand 0.9.1", + "rand 0.9.2", "rope", "util", - "workspace-hack", ] [[package]] @@ -16108,7 +16059,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator", + "phf_generator 0.11.3", "phf_shared 0.11.3", "proc-macro2", "quote", @@ -16146,7 +16097,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros 0.27.1", + "strum_macros 0.27.2", ] [[package]] @@ -16159,20 +16110,19 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "rustversion", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -16188,9 +16138,8 @@ dependencies = [ "arrayvec", "ctor", "log", - "rand 0.9.1", + "rand 0.9.2", "rayon", - "workspace-hack", "zlog", ] @@ -16221,7 +16170,6 @@ dependencies = [ "ui", "unicode-segmentation", "util", - "workspace-hack", ] [[package]] @@ -16236,20 +16184,19 @@ dependencies = [ "serde_json", "smol", "util", - "workspace-hack", ] [[package]] name = "sval" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9739f56c5d0c44a5ed45473ec868af02eb896af8c05f616673a31e1d1bb09" +checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" [[package]] name = "sval_buffer" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39b07436a8c271b34dad5070c634d1d3d76d6776e938ee97b4a66a5e8003d0b" +checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" dependencies = [ "sval", "sval_ref", @@ -16257,18 +16204,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffcb072d857431bf885580dacecf05ed987bac931230736739a79051dbf3499b" +checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f214f427ad94a553e5ca5514c95c6be84667cbc5568cce957f03f3477d03d5c" +checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" dependencies = [ "itoa", "ryu", @@ -16277,9 +16224,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ed34b32e638dec9a99c8ac92d0aa1220d40041026b625474c2b6a4d6f4feb" +checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" dependencies = [ "itoa", "ryu", @@ -16288,9 +16235,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14bae8fcb2f24fee2c42c1f19037707f7c9a29a0cda936d2188d48a961c4bb2a" +checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" dependencies = [ "sval", "sval_buffer", @@ -16299,20 +16246,20 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4eaea3821d3046dcba81d4b8489421da42961889902342691fb7eab491d79e" +checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.14.1" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172dd4aa8cb3b45c8ac8f3b4111d644cd26938b0643ede8f93070812b87fb339" +checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" dependencies = [ - "serde", + "serde_core", "sval", "sval_nested", ] @@ -16333,7 +16280,6 @@ dependencies = [ "multi_buffer", "ui", "workspace", - "workspace-hack", ] [[package]] @@ -16348,9 +16294,9 @@ dependencies = [ [[package]] name = "swash" -version = "0.2.2" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fae9a562c7b46107d9c78cd78b75bbe1e991c16734c0aee8ff0ee711fb8b620a" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" dependencies = [ "skrifa", "yazi", @@ -16359,9 +16305,9 @@ dependencies = [ [[package]] name = "symphonia" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" dependencies = [ "lazy_static", "symphonia-bundle-flac", @@ -16378,9 +16324,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-flac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" dependencies = [ "log", "symphonia-core", @@ -16390,9 +16336,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" dependencies = [ "lazy_static", "log", @@ -16402,9 +16348,9 @@ dependencies = [ [[package]] name = "symphonia-codec-aac" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" dependencies = [ "lazy_static", "log", @@ -16413,9 +16359,9 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" dependencies = [ "log", "symphonia-core", @@ -16423,9 +16369,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" dependencies = [ "log", "symphonia-core", @@ -16434,9 +16380,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" dependencies = [ "arrayvec", "bitflags 1.3.2", @@ -16447,9 +16393,9 @@ dependencies = [ [[package]] name = "symphonia-format-isomp4" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" dependencies = [ "encoding_rs", "log", @@ -16460,9 +16406,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" dependencies = [ "log", "symphonia-core", @@ -16472,9 +16418,9 @@ dependencies = [ [[package]] name = "symphonia-format-riff" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" dependencies = [ "extended", "log", @@ -16484,9 +16430,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" dependencies = [ "encoding_rs", "lazy_static", @@ -16496,9 +16442,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -16517,9 +16463,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -16552,13 +16498,13 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -16576,7 +16522,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "enum-as-inner", "libc", @@ -16590,7 +16536,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "enum-as-inner", "libc", @@ -16609,7 +16555,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.54.0", + "windows 0.57.0", ] [[package]] @@ -16623,7 +16569,7 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows 0.61.1", + "windows 0.61.3", ] [[package]] @@ -16643,7 +16589,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -16677,7 +16623,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.20", + "toml 0.8.23", "version-compare", ] @@ -16687,7 +16633,7 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "cap-fs-ext", "cap-std", "fd-lock", @@ -16709,7 +16655,6 @@ dependencies = [ "release_channel", "serde", "sysinfo 0.37.2", - "workspace-hack", ] [[package]] @@ -16726,7 +16671,7 @@ dependencies = [ "menu", "picker", "project", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "settings", @@ -16735,7 +16680,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zlog", ] @@ -16789,9 +16733,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" [[package]] name = "task" @@ -16806,14 +16750,13 @@ dependencies = [ "parking_lot", "pretty_assertions", "proto", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", "sha2", "shellexpand 2.1.2", "util", - "workspace-hack", "zed_actions", ] @@ -16840,7 +16783,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -16852,7 +16794,6 @@ dependencies = [ "serde", "serde_json", "telemetry_events", - "workspace-hack", ] [[package]] @@ -16862,19 +16803,18 @@ dependencies = [ "semantic_version", "serde", "serde_json", - "workspace-hack", ] [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand 2.3.0", - "getrandom 0.3.2", + "getrandom 0.3.4", "once_cell", - "rustix 1.0.7", + "rustix 1.1.2", "windows-sys 0.59.0", ] @@ -16910,32 +16850,31 @@ dependencies = [ "itertools 0.14.0", "libc", "log", - "rand 0.9.1", + "rand 0.9.2", "regex", "release_channel", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "settings", "smol", "sysinfo 0.37.2", "task", "theme", - "thiserror 2.0.12", + "thiserror 2.0.17", "url", "urlencoding", "util", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", ] [[package]] name = "terminal_size" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.60.2", ] [[package]] @@ -16958,9 +16897,9 @@ dependencies = [ "log", "pretty_assertions", "project", - "rand 0.9.1", + "rand 0.9.2", "regex", - "schemars 1.0.1", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -16973,7 +16912,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -16990,13 +16928,12 @@ dependencies = [ "log", "parking_lot", "postage", - "rand 0.9.1", + "rand 0.9.2", "regex", "rope", "smallvec", "sum_tree", "util", - "workspace-hack", "zlog", ] @@ -17014,16 +16951,15 @@ dependencies = [ "palette", "parking_lot", "refineable", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", "settings", "strum 0.27.2", - "thiserror 2.0.12", + "thiserror 2.0.17", "util", "uuid", - "workspace-hack", ] [[package]] @@ -17035,7 +16971,6 @@ dependencies = [ "fs", "gpui", "theme", - "workspace-hack", ] [[package]] @@ -17046,7 +16981,7 @@ dependencies = [ "clap", "collections", "gpui", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "palette", "serde", @@ -17056,7 +16991,6 @@ dependencies = [ "strum 0.27.2", "theme", "vscode_theme", - "workspace-hack", ] [[package]] @@ -17075,7 +17009,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", "zed_actions", ] @@ -17090,11 +17023,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -17105,39 +17038,41 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "tiff" -version = "0.9.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" dependencies = [ + "fax", "flate2", - "jpeg-decoder", + "half", + "quick-error", "weezl", + "zune-jpeg", ] [[package]] @@ -17156,9 +17091,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -17173,15 +17108,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -17195,7 +17130,6 @@ dependencies = [ "core-foundation-sys", "sys-locale", "time", - "workspace-hack", ] [[package]] @@ -17218,7 +17152,7 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png", + "png 0.17.16", "tiny-skia-path", ] @@ -17248,9 +17182,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -17268,9 +17202,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -17300,7 +17234,7 @@ dependencies = [ "project", "remote", "rpc", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "settings", "smallvec", @@ -17310,28 +17244,26 @@ dependencies = [ "tree-sitter-md", "ui", "util", - "windows 0.61.1", + "windows 0.61.3", "workspace", - "workspace-hack", "zed_actions", ] [[package]] name = "tokio" -version = "1.44.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes 1.10.1", "libc", - "mio 1.0.3", + "mio 1.1.0", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.1", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -17347,13 +17279,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -17382,7 +17314,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.26", + "rustls 0.23.33", "tokio", ] @@ -17442,7 +17374,7 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls 0.23.26", + "rustls 0.23.33", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -17451,16 +17383,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes 1.10.1", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.14.5", "pin-project-lite", "tokio", ] @@ -17476,44 +17407,95 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.20" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.11.4", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.11.4", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "toolchain_selector" @@ -17533,7 +17515,6 @@ dependencies = [ "ui", "util", "workspace", - "workspace-hack", ] [[package]] @@ -17597,7 +17578,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "bytes 1.10.1", "futures-core", "futures-util", @@ -17610,6 +17591,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.4", + "bytes 1.10.1", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -17636,20 +17635,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -17705,7 +17704,7 @@ checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -18027,11 +18026,30 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand 0.9.1", - "rustls 0.23.26", + "rand 0.9.2", + "rustls 0.23.33", + "rustls-pki-types", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes 1.10.1", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.2", + "rustls 0.23.33", "rustls-pki-types", "sha1", - "thiserror 2.0.12", + "thiserror 2.0.17", "utf-8", ] @@ -18049,9 +18067,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "ucd-trie" @@ -18097,7 +18115,7 @@ dependencies = [ "serde", "thiserror 1.0.69", "tracing", - "yoke", + "yoke 0.7.5", ] [[package]] @@ -18112,7 +18130,7 @@ dependencies = [ "icons", "itertools 0.14.0", "menu", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "settings", "smallvec", @@ -18121,8 +18139,7 @@ dependencies = [ "theme", "ui_macros", "util", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", ] [[package]] @@ -18138,7 +18155,6 @@ dependencies = [ "settings", "theme", "ui", - "workspace-hack", ] [[package]] @@ -18147,9 +18163,8 @@ version = "0.1.0" dependencies = [ "component", "quote", - "syn 2.0.101", + "syn 2.0.106", "ui", - "workspace-hack", ] [[package]] @@ -18163,7 +18178,6 @@ dependencies = [ "theme", "ui", "workspace", - "workspace-hack", ] [[package]] @@ -18204,9 +18218,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" @@ -18258,9 +18272,9 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -18294,9 +18308,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -18343,12 +18357,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -18382,10 +18390,10 @@ dependencies = [ "log", "nix 0.29.0", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "regex", "rust-embed", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "serde_json_lenient", @@ -18398,7 +18406,6 @@ dependencies = [ "util_macros", "walkdir", "which 6.0.3", - "workspace-hack", ] [[package]] @@ -18407,17 +18414,16 @@ version = "0.1.0" dependencies = [ "perf", "quote", - "syn 2.0.101", - "workspace-hack", + "syn 2.0.106", ] [[package]] name = "uuid" -version = "1.16.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.4", "js-sys", "serde", "sha1_smol", @@ -18437,9 +18443,9 @@ dependencies = [ [[package]] name = "v_frame" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" dependencies = [ "aligned-vec", "num-traits", @@ -18499,10 +18505,9 @@ name = "vercel" version = "0.1.0" dependencies = [ "anyhow", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "strum 0.27.2", - "workspace-hack", ] [[package]] @@ -18550,7 +18555,7 @@ dependencies = [ "project_panel", "regex", "release_channel", - "schemars 1.0.1", + "schemars 1.0.4", "search", "serde", "serde_json", @@ -18564,7 +18569,6 @@ dependencies = [ "util_macros", "vim_mode_setting", "workspace", - "workspace-hack", "zed_actions", ] @@ -18574,7 +18578,6 @@ version = "0.1.0" dependencies = [ "gpui", "settings", - "workspace-hack", ] [[package]] @@ -18625,7 +18628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" dependencies = [ "arrayvec", - "bitflags 2.9.0", + "bitflags 2.9.4", "cursor-icon", "log", "memchr", @@ -18687,17 +18690,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt 0.39.0", + "wit-bindgen 0.46.0", ] [[package]] @@ -18708,35 +18711,36 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -18747,9 +18751,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -18757,22 +18761,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] @@ -18813,7 +18817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" dependencies = [ "anyhow", - "indexmap 2.9.0", + "indexmap 2.11.4", "serde", "serde_derive", "serde_json", @@ -18831,7 +18835,7 @@ dependencies = [ "anyhow", "auditable-serde", "flate2", - "indexmap 2.9.0", + "indexmap 2.11.4", "serde", "serde_derive", "serde_json", @@ -18860,8 +18864,8 @@ version = "0.201.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" dependencies = [ - "bitflags 2.9.0", - "indexmap 2.9.0", + "bitflags 2.9.4", + "indexmap 2.11.4", "semver", ] @@ -18871,9 +18875,9 @@ version = "0.221.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" dependencies = [ - "bitflags 2.9.0", - "hashbrown 0.15.3", - "indexmap 2.9.0", + "bitflags 2.9.4", + "hashbrown 0.15.5", + "indexmap 2.11.4", "semver", "serde", ] @@ -18884,9 +18888,9 @@ version = "0.227.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ - "bitflags 2.9.0", - "hashbrown 0.15.3", - "indexmap 2.9.0", + "bitflags 2.9.4", + "hashbrown 0.15.5", + "indexmap 2.11.4", "semver", ] @@ -18909,18 +18913,18 @@ checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69" dependencies = [ "anyhow", "async-trait", - "bitflags 2.9.0", + "bitflags 2.9.4", "bumpalo", "cc", "cfg-if", "encoding_rs", "hashbrown 0.14.5", - "indexmap 2.9.0", + "indexmap 2.11.4", "libc", "log", - "mach2 0.4.2", + "mach2 0.4.3", "memfd", - "object", + "object 0.36.7", "once_cell", "paste", "postcard", @@ -18933,7 +18937,7 @@ dependencies = [ "serde_derive", "smallvec", "sptr", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", "trait-variant", "wasmparser 0.221.3", "wasmtime-asm-macros", @@ -18991,7 +18995,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wasmtime-component-util", "wasmtime-wit-bindgen", "wit-parser 0.221.3", @@ -19016,12 +19020,12 @@ dependencies = [ "cranelift-entity", "cranelift-frontend", "cranelift-native", - "gimli", + "gimli 0.31.1", "itertools 0.12.1", "log", - "object", + "object 0.36.7", "smallvec", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", "thiserror 1.0.69", "wasmparser 0.221.3", "wasmtime-environ", @@ -19038,17 +19042,17 @@ dependencies = [ "cpp_demangle", "cranelift-bitset", "cranelift-entity", - "gimli", - "indexmap 2.9.0", + "gimli 0.31.1", + "indexmap 2.11.4", "log", - "object", + "object 0.36.7", "postcard", "rustc-demangle", "semver", "serde", "serde_derive", "smallvec", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", "wasm-encoder 0.221.3", "wasmparser 0.221.3", "wasmprinter", @@ -19105,7 +19109,7 @@ checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -19116,7 +19120,7 @@ checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4" dependencies = [ "anyhow", "async-trait", - "bitflags 2.9.0", + "bitflags 2.9.4", "bytes 1.10.1", "cap-fs-ext", "cap-net-ext", @@ -19147,9 +19151,9 @@ checksum = "fdbabfb8f20502d5e1d81092b9ead3682ae59988487aafcd7567387b7a43cf8f" dependencies = [ "anyhow", "cranelift-codegen", - "gimli", - "object", - "target-lexicon 0.13.2", + "gimli 0.31.1", + "object 0.36.7", + "target-lexicon 0.13.3", "wasmparser 0.221.3", "wasmtime-cranelift", "wasmtime-environ", @@ -19164,7 +19168,7 @@ checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.9.0", + "indexmap 2.11.4", "wit-parser 0.221.3", ] @@ -19185,20 +19189,19 @@ dependencies = [ "futures 0.3.31", "gpui", "parking_lot", - "rand 0.9.1", - "workspace-hack", + "rand 0.9.2", "zlog", ] [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.44", + "rustix 1.1.2", "scoped-tls", "smallvec", "wayland-sys", @@ -19206,23 +19209,23 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.0", - "rustix 0.38.44", + "bitflags 2.9.4", + "rustix 1.1.2", "wayland-backend", "wayland-scanner", ] [[package]] name = "wayland-cursor" -version = "0.31.8" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" dependencies = [ - "rustix 0.38.44", + "rustix 1.1.2", "wayland-client", "xcursor", ] @@ -19233,7 +19236,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "wayland-backend", "wayland-client", "wayland-scanner", @@ -19241,11 +19244,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.6" +version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "wayland-backend", "wayland-client", "wayland-scanner", @@ -19257,7 +19260,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "wayland-backend", "wayland-client", "wayland-protocols 0.31.2", @@ -19266,20 +19269,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", - "quick-xml 0.37.4", + "quick-xml 0.37.5", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.6" +version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" dependencies = [ "dlib", "log", @@ -19289,9 +19292,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", @@ -19309,9 +19312,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "954c5a41f2bcb7314344079d0891505458cc2f4b422bdea1d5bfbe6d1a04903b" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" dependencies = [ "phf 0.11.3", "phf_codegen", @@ -19328,7 +19331,6 @@ dependencies = [ "collections", "gpui", "serde", - "workspace-hack", ] [[package]] @@ -19345,7 +19347,6 @@ dependencies = [ "serde", "serde_json", "web_search", - "workspace-hack", ] [[package]] @@ -19394,9 +19395,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.8" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" [[package]] name = "which" @@ -19424,11 +19425,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" dependencies = [ - "redox_syscall 0.5.11", + "libredox", "wasite", ] @@ -19440,7 +19441,7 @@ checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d" dependencies = [ "anyhow", "async-trait", - "bitflags 2.9.0", + "bitflags 2.9.4", "thiserror 1.0.69", "tracing", "wasmtime", @@ -19458,7 +19459,7 @@ dependencies = [ "proc-macro2", "quote", "shellexpand 2.1.2", - "syn 2.0.101", + "syn 2.0.106", "witx", ] @@ -19470,7 +19471,7 @@ checksum = "08c5c473d4198e6c2d377f3809f713ff0c110cab88a0805ae099a82119ee250c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wiggle-generate", ] @@ -19492,11 +19493,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -19513,10 +19514,10 @@ checksum = "2f849ef2c5f46cb0a20af4b4487aaa239846e52e2c03f13fa3c784684552859c" dependencies = [ "anyhow", "cranelift-codegen", - "gimli", + "gimli 0.31.1", "regalloc2", "smallvec", - "target-lexicon 0.13.2", + "target-lexicon 0.13.3", "thiserror 1.0.69", "wasmparser 0.221.3", "wasmtime-cranelift", @@ -19533,6 +19534,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" @@ -19545,14 +19556,14 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.1" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.61.0", + "windows-core 0.61.2", "windows-future", - "windows-link 0.1.1", + "windows-link 0.1.3", "windows-numerics", ] @@ -19565,8 +19576,8 @@ dependencies = [ "ctrlc", "parking_lot", "rayon", - "thiserror 2.0.12", - "windows 0.61.1", + "thiserror 2.0.17", + "windows 0.61.3", "windows-future", ] @@ -19576,7 +19587,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.0", + "windows-core 0.61.2", ] [[package]] @@ -19589,6 +19600,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" @@ -19604,25 +19627,50 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", - "windows-link 0.1.1", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-future" -version = "0.2.0" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ - "windows-core 0.61.0", - "windows-link 0.1.1", + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -19633,18 +19681,29 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", ] [[package]] @@ -19655,31 +19714,31 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -19687,8 +19746,8 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.0", - "windows-link 0.1.1", + "windows-core 0.61.2", + "windows-link 0.1.3", ] [[package]] @@ -19697,31 +19756,31 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.2", + "windows-result 0.3.4", "windows-strings 0.3.1", - "windows-targets 0.53.2", + "windows-targets 0.53.5", ] [[package]] name = "windows-registry" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link 0.1.1", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] name = "windows-registry" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f91f87ce112ffb7275000ea98eb1940912c21c1567c9312fde20261f3eadd29" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -19744,20 +19803,20 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -19776,25 +19835,25 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.1", + "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -19839,16 +19898,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -19899,18 +19958,28 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.1.3", ] [[package]] @@ -19933,9 +20002,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -19957,9 +20026,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -19981,9 +20050,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -19993,9 +20062,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -20017,9 +20086,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -20041,9 +20110,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -20065,9 +20134,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -20089,15 +20158,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -20121,16 +20190,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -20143,11 +20202,11 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.20" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4a67c78ee5782c0c1cb41bebc7e12c6e79644daa1650ebbc1de5d5b08593f7" +checksum = "edcacf11b6f48dd21b9ba002f991bdd5de29b2da8cc2800412f4b80f677e4957" dependencies = [ - "toml 0.8.20", + "toml 0.8.23", "version_check", ] @@ -20163,7 +20222,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "windows-sys 0.59.0", ] @@ -20182,7 +20241,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "wit-bindgen-rt 0.22.0", "wit-bindgen-rust-macro 0.22.0", ] @@ -20197,6 +20256,12 @@ dependencies = [ "wit-bindgen-rust-macro 0.41.0", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "wit-bindgen-core" version = "0.22.0" @@ -20224,22 +20289,13 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb8738270f32a2d6739973cbbb7c1b6dd8959ce515578a6e19165853272ee64" -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.0", -] - [[package]] name = "wit-bindgen-rt" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "futures 0.3.31", "once_cell", ] @@ -20252,7 +20308,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" dependencies = [ "anyhow", "heck 0.4.1", - "indexmap 2.9.0", + "indexmap 2.11.4", "wasm-metadata 0.201.0", "wit-bindgen-core 0.22.0", "wit-component 0.201.0", @@ -20266,9 +20322,9 @@ checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.9.0", + "indexmap 2.11.4", "prettyplease", - "syn 2.0.101", + "syn 2.0.106", "wasm-metadata 0.227.1", "wit-bindgen-core 0.41.0", "wit-component 0.227.1", @@ -20283,7 +20339,7 @@ dependencies = [ "anyhow", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wit-bindgen-core 0.22.0", "wit-bindgen-rust 0.22.0", ] @@ -20298,7 +20354,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "wit-bindgen-core 0.41.0", "wit-bindgen-rust 0.41.0", ] @@ -20310,8 +20366,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" dependencies = [ "anyhow", - "bitflags 2.9.0", - "indexmap 2.9.0", + "bitflags 2.9.4", + "indexmap 2.11.4", "log", "serde", "serde_derive", @@ -20329,8 +20385,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" dependencies = [ "anyhow", - "bitflags 2.9.0", - "indexmap 2.9.0", + "bitflags 2.9.4", + "indexmap 2.11.4", "log", "serde", "serde_derive", @@ -20349,7 +20405,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" dependencies = [ "anyhow", "id-arena", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "semver", "serde", @@ -20367,7 +20423,7 @@ checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" dependencies = [ "anyhow", "id-arena", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "semver", "serde", @@ -20385,7 +20441,7 @@ checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" dependencies = [ "anyhow", "id-arena", - "indexmap 2.9.0", + "indexmap 2.11.4", "log", "semver", "serde", @@ -20435,7 +20491,7 @@ dependencies = [ "pretty_assertions", "project", "remote", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "serde_json", "session", @@ -20450,209 +20506,11 @@ dependencies = [ "ui", "util", "uuid", - "windows 0.61.1", - "workspace-hack", + "windows 0.61.3", "zed_actions", "zlog", ] -[[package]] -name = "workspace-hack" -version = "0.1.0" -dependencies = [ - "aes", - "ahash 0.8.11", - "aho-corasick", - "anstream", - "arrayvec", - "ashpd 0.11.0", - "async-compression", - "async-std", - "async-tungstenite", - "aws-config", - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "base64 0.22.1", - "base64ct", - "bigdecimal", - "bit-set 0.8.0", - "bit-vec 0.8.0", - "bitflags 2.9.0", - "bstr", - "bytemuck", - "byteorder", - "bytes 1.10.1", - "cc", - "chrono", - "cipher", - "clap", - "clap_builder", - "codespan-reporting", - "concurrent-queue", - "core-foundation 0.9.4", - "core-foundation-sys", - "cranelift-codegen", - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "crypto-common", - "deranged", - "digest", - "either", - "euclid", - "event-listener 5.4.0", - "event-listener-strategy", - "flate2", - "flume", - "foldhash", - "form_urlencoded", - "futures 0.3.31", - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", - "getrandom 0.2.15", - "getrandom 0.3.2", - "gimli", - "half", - "handlebars 4.5.0", - "hashbrown 0.14.5", - "hashbrown 0.15.3", - "heck 0.4.1", - "hmac", - "hyper 0.14.32", - "hyper-rustls 0.27.5", - "idna", - "indexmap 2.9.0", - "inout", - "itertools 0.12.1", - "itertools 0.13.0", - "lazy_static", - "libc", - "libsqlite3-sys", - "linux-raw-sys 0.4.15", - "linux-raw-sys 0.9.4", - "livekit-runtime", - "log", - "lyon", - "lyon_path", - "md-5", - "memchr", - "memmap2", - "mime_guess", - "miniz_oxide", - "mio 1.0.3", - "naga", - "nix 0.28.0", - "nix 0.29.0", - "nix 0.30.1", - "nom 7.1.3", - "num-bigint", - "num-bigint-dig", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", - "objc2", - "objc2-core-foundation", - "objc2-foundation", - "objc2-metal", - "object", - "once_cell", - "percent-encoding", - "phf 0.11.3", - "phf_shared 0.11.3", - "prettyplease", - "proc-macro2", - "prost 0.12.6", - "prost 0.9.0", - "prost-types 0.9.0", - "quote", - "rand 0.8.5", - "rand 0.9.1", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "rand_distr", - "regalloc2", - "regex", - "regex-automata", - "regex-syntax", - "reqwest 0.12.15", - "ring", - "rust_decimal", - "rustc-hash 1.1.0", - "rustix 0.38.44", - "rustix 1.0.7", - "rustls 0.23.26", - "rustls-webpki 0.103.1", - "scopeguard", - "sea-orm", - "sea-query-binder", - "security-framework 3.2.0", - "security-framework-sys", - "semver", - "serde", - "serde_core", - "serde_json", - "simd-adler32", - "smallvec", - "spin", - "sqlx", - "sqlx-macros", - "sqlx-macros-core", - "sqlx-postgres", - "sqlx-sqlite", - "stable_deref_trait", - "strum 0.26.3", - "subtle", - "syn 1.0.109", - "syn 2.0.101", - "thiserror 2.0.12", - "time", - "time-macros", - "tokio", - "tokio-rustls 0.26.2", - "tokio-socks", - "tokio-stream", - "tokio-util", - "toml_datetime", - "toml_edit", - "tower 0.5.2", - "tracing", - "tracing-core", - "tungstenite 0.26.2", - "unicode-properties", - "url", - "uuid", - "wasmparser 0.221.3", - "wasmtime", - "wasmtime-cranelift", - "wasmtime-environ", - "wayland-backend", - "wayland-sys", - "winapi", - "windows 0.61.1", - "windows-core 0.61.0", - "windows-numerics", - "windows-sys 0.48.0", - "windows-sys 0.52.0", - "windows-sys 0.59.0", - "windows-sys 0.61.0", - "zbus_macros", - "zeroize", - "zvariant", -] - [[package]] name = "worktree" version = "0.1.0" @@ -20675,7 +20533,7 @@ dependencies = [ "paths", "postage", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "rpc", "serde", "serde_json", @@ -20685,32 +20543,14 @@ dependencies = [ "sum_tree", "text", "util", - "workspace-hack", "zlog", ] -[[package]] -name = "worktree_benchmarks" -version = "0.1.0" -dependencies = [ - "fs", - "gpui", - "settings", - "workspace-hack", - "worktree", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "wyz" @@ -20743,32 +20583,32 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "rustix 0.38.44", + "rustix 1.1.2", "x11rb-protocol", + "xcursor", ] [[package]] name = "x11rb-protocol" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] name = "x_ai" version = "0.1.0" dependencies = [ "anyhow", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "strum 0.27.2", - "workspace-hack", ] [[package]] @@ -20794,9 +20634,9 @@ dependencies = [ [[package]] name = "xcursor" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" [[package]] name = "xim-ctext" @@ -20811,7 +20651,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", ] [[package]] @@ -20864,9 +20704,8 @@ dependencies = [ "cargo_toml", "clap", "indoc", - "toml 0.8.20", - "toml_edit", - "workspace-hack", + "toml 0.8.23", + "toml_edit 0.22.27", ] [[package]] @@ -20903,7 +20742,7 @@ dependencies = [ "flate2", "futures 0.3.31", "http-body-util", - "hyper 1.6.0", + "hyper 1.7.0", "hyper-util", "js-sys", "nom 8.0.0", @@ -20946,7 +20785,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.8.0", "zerofrom", ] @@ -20958,15 +20809,27 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", "synstructure", ] [[package]] name = "zbus" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" dependencies = [ "async-broadcast", "async-executor", @@ -20978,9 +20841,9 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener 5.4.0", + "event-listener 5.4.1", "futures-core", - "futures-lite 2.6.0", + "futures-lite 2.6.1", "hex", "nix 0.30.1", "ordered-stream", @@ -20988,7 +20851,8 @@ dependencies = [ "serde_repr", "tracing", "uds_windows", - "windows-sys 0.60.2", + "uuid", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", @@ -20997,14 +20861,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "zbus_names", "zvariant", "zvariant_utils", @@ -21163,10 +21027,9 @@ dependencies = [ "watch", "web_search", "web_search_providers", - "windows 0.61.1", + "windows 0.61.3", "winresource", "workspace", - "workspace-hack", "zed-reqwest", "zed_actions", "zed_env_vars", @@ -21182,7 +21045,7 @@ name = "zed-font-kit" version = "0.14.1-zed" source = "git+https://github.com/zed-industries/font-kit?rev=110523127440aefb11ce0cf280ae7c5071337ec5#110523127440aefb11ce0cf280ae7c5071337ec5" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.4", "byteorder", "core-foundation 0.10.0", "core-graphics 0.24.0", @@ -21211,12 +21074,12 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.9", + "h2 0.4.12", "http 1.3.1", "http-body 1.0.1", "http-body-util", - "hyper 1.6.0", - "hyper-rustls 0.27.5", + "hyper 1.7.0", + "hyper-rustls 0.27.7", "hyper-util", "ipnet", "js-sys", @@ -21227,8 +21090,8 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.26", - "rustls-native-certs 0.8.1", + "rustls 0.23.33", + "rustls-native-certs 0.8.2", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -21265,7 +21128,7 @@ dependencies = [ "screencapturekit-sys", "sysinfo 0.31.4", "tao-core-video-sys", - "windows 0.61.1", + "windows 0.61.3", "windows-capture", "x11", "xcb", @@ -21276,7 +21139,7 @@ name = "zed-xim" version = "0.4.0-zed" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "ahash 0.8.11", + "ahash 0.8.12", "hashbrown 0.14.5", "log", "x11rb", @@ -21289,10 +21152,9 @@ name = "zed_actions" version = "0.1.0" dependencies = [ "gpui", - "schemars 1.0.1", + "schemars 1.0.4", "serde", "uuid", - "workspace-hack", ] [[package]] @@ -21300,7 +21162,6 @@ name = "zed_env_vars" version = "0.1.0" dependencies = [ "gpui", - "workspace-hack", ] [[package]] @@ -21364,48 +21225,28 @@ dependencies = [ [[package]] name = "zeno" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc0de2315dc13d00e5df3cd6b8d2124a6eaec6a2d4b6a1c5f37b7efad17fcc17" - -[[package]] -name = "zerocopy" -version = "0.7.35" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive 0.7.35", -] +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive 0.8.24", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -21425,15 +21266,15 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -21446,7 +21287,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -21473,26 +21314,37 @@ dependencies = [ "uuid", ] +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke 0.8.0", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ - "yoke", + "yoke 0.8.0", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", ] [[package]] @@ -21528,7 +21380,7 @@ dependencies = [ "parking_lot", "postage", "project", - "rand 0.9.1", + "rand 0.9.2", "regex", "release_channel", "reqwest_client", @@ -21540,14 +21392,13 @@ dependencies = [ "telemetry", "telemetry_events", "theme", - "thiserror 2.0.12", + "thiserror 2.0.17", "tree-sitter-go", "tree-sitter-rust", "ui", "util", "uuid", "workspace", - "workspace-hack", "worktree", "zed_actions", "zlog", @@ -21580,11 +21431,10 @@ dependencies = [ "serde", "serde_json", "settings", - "thiserror 2.0.12", + "thiserror 2.0.17", "util", "uuid", "workspace", - "workspace-hack", "worktree", ] @@ -21617,7 +21467,6 @@ dependencies = [ "ui_input", "util", "workspace", - "workspace-hack", "zeta2", "zlog", ] @@ -21663,7 +21512,6 @@ dependencies = [ "terminal_view", "util", "watch", - "workspace-hack", "zeta", "zeta2", "zlog", @@ -21699,7 +21547,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "displaydoc", - "indexmap 2.9.0", + "indexmap 2.11.4", "num_enum", "thiserror 1.0.69", ] @@ -21719,7 +21567,6 @@ dependencies = [ "collections", "log", "tempfile", - "workspace-hack", ] [[package]] @@ -21729,7 +21576,6 @@ dependencies = [ "collections", "gpui", "settings", - "workspace-hack", "zlog", ] @@ -21772,9 +21618,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", @@ -21797,18 +21643,18 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.14" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] [[package]] name = "zvariant" -version = "5.7.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" dependencies = [ "endi", "enumflags2", @@ -21821,27 +21667,26 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.106", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", - "syn 2.0.101", + "syn 2.0.106", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 33f3fa2ed3fa912e33bc24fa9303e3c2b4790dad..4828de8895e62213f16db9118a6ae348aaa40a74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -218,8 +218,7 @@ members = [ # "tooling/perf", - "tooling/workspace-hack", - "tooling/xtask", "crates/fs_benchmarks", "crates/worktree_benchmarks", + "tooling/xtask", ] default-members = ["crates/zed"] @@ -372,7 +371,7 @@ remote_server = { path = "crates/remote_server" } repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } rich_text = { path = "crates/rich_text" } -rodio = { git = "https://github.com/RustAudio/rodio" } +rodio = { git = "https://github.com/RustAudio/rodio", rev ="e2074c6c2acf07b57cf717e076bdda7a9ac6e70b", features = ["wav", "playback", "wav_output", "recording"] } rope = { path = "crates/rope" } rpc = { path = "crates/rpc" } rules_library = { path = "crates/rules_library" } @@ -438,7 +437,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = { version = "0.4.3", features = ["unstable"] } +agent-client-protocol = { version = "=0.4.3", features = ["unstable"] } aho-corasick = "1.1" alacritty_terminal = "0.25.1-rc1" any_vec = "0.14" @@ -455,7 +454,7 @@ async-recursion = "1.0.0" async-tar = "0.5.0" async-task = "4.7" async-trait = "0.1" -async-tungstenite = "0.29.1" +async-tungstenite = "0.31.0" async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] } aws-config = { version = "1.6.1", features = ["behavior-version-latest"] } aws-credential-types = { version = "1.2.2", features = [ @@ -481,10 +480,10 @@ chrono = { version = "0.4", features = ["serde"] } ciborium = "0.2" circular-buffer = "1.0" clap = { version = "4.4", features = ["derive"] } -cocoa = "0.26" -cocoa-foundation = "0.2.0" +cocoa = "=0.26.0" +cocoa-foundation = "=0.2.0" convert_case = "0.8.0" -core-foundation = "0.10.0" +core-foundation = "=0.10.0" core-foundation-sys = "0.8.6" core-video = { version = "0.4.3", features = ["metal"] } cpal = "0.16" @@ -547,7 +546,7 @@ nix = "0.29" num-format = "0.4.4" num-traits = "0.2" objc = "0.2" -objc2-foundation = { version = "0.3", default-features = false, features = [ +objc2-foundation = { version = "=0.3.1", default-features = false, features = [ "NSArray", "NSAttributedString", "NSBundle", @@ -713,7 +712,6 @@ wasmtime-wasi = "29" which = "6.0.0" windows-core = "0.61" wit-component = "0.221" -workspace-hack = "0.1.0" yawc = "0.2.5" zeroize = "1.8" zstd = "0.11" @@ -774,9 +772,6 @@ notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5a notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } -# Makes the workspace hack crate refer to the local one, but only when you're building locally -workspace-hack = { path = "tooling/workspace-hack" } - [profile.dev] split-debuginfo = "unpacked" codegen-units = 16 @@ -904,5 +899,4 @@ ignored = [ "serde", "component", "documented", - "workspace-hack", ] diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml index ac24a6ed0f41c75d5c4dcd9b9b4122336022ddf3..09202dc57cb96f5f258e64063f5d61169fa7a045 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -45,7 +45,6 @@ url.workspace = true util.workspace = true uuid.workspace = true watch.workspace = true -workspace-hack.workspace = true [dev-dependencies] env_logger.workspace = true diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml index 7a6d8c21a096364a8468671f4186048559ec8a61..0720c4b6685ecf7fa20d8cacd2b61baa765c961c 100644 --- a/crates/acp_tools/Cargo.toml +++ b/crates/acp_tools/Cargo.toml @@ -26,5 +26,4 @@ settings.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true workspace.workspace = true diff --git a/crates/action_log/Cargo.toml b/crates/action_log/Cargo.toml index 1a389e8859b24a320720ecfc3fa6cf2a13f274ad..a8395a943a2ce4e06d4971548c32bf765adb492d 100644 --- a/crates/action_log/Cargo.toml +++ b/crates/action_log/Cargo.toml @@ -23,7 +23,6 @@ project.workspace = true text.workspace = true util.workspace = true watch.workspace = true -workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 3a80f012f9fb0e5b056a7b2f8763a2019dfcdf2b..4e604b452122c5a8e38b2d02b54f4ee639817ab4 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -25,7 +25,6 @@ proto.workspace = true smallvec.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true workspace.workspace = true [dev-dependencies] diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 5fb8f915b8f19d5adf6132f0fbefda0f5081bbae..86027d01fe3e93d2f6234cce9e935ebace318481 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -69,7 +69,6 @@ util.workspace = true uuid.workspace = true watch.workspace = true web_search.workspace = true -workspace-hack.workspace = true zed_env_vars.workspace = true zstd.workspace = true diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index bdf1b72fdc0c2c71d5e445633d1d4a8ce32a6ba4..fcdba2301ee21254832a81899cff0bc9753e92f2 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -51,7 +51,6 @@ terminal.workspace = true uuid.workspace = true util.workspace = true watch.workspace = true -workspace-hack.workspace = true [target.'cfg(unix)'.dependencies] libc.workspace = true diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml index a8b457a9dddb1f8932d015f895e6d2064944bfe9..8ddcac24fe054d1226f2bbac49498fd35d6ed1c3 100644 --- a/crates/agent_settings/Cargo.toml +++ b/crates/agent_settings/Cargo.toml @@ -24,7 +24,6 @@ schemars.workspace = true serde.workspace = true settings.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] fs.workspace = true diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index d8f495c79614ff1aaf23c017160516c1e54065ab..f763d6f91e45d1e8b5a035c22fbb7ab65de93dd9 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -96,7 +96,6 @@ url.workspace = true urlencoding.workspace = true util.workspace = true watch.workspace = true -workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml index 95a45b1a6fbe103f02532d33c21af707f2f51d45..8fb0570e5cf3da5f5f3d6249f76b42f15b8eed7d 100644 --- a/crates/ai_onboarding/Cargo.toml +++ b/crates/ai_onboarding/Cargo.toml @@ -24,5 +24,4 @@ serde.workspace = true smallvec.workspace = true telemetry.workspace = true ui.workspace = true -workspace-hack.workspace = true zed_actions.workspace = true diff --git a/crates/anthropic/Cargo.toml b/crates/anthropic/Cargo.toml index c8103e5bfb533a0f7f8e88995ac0927073a9793f..a9c7208b0caa9a2660aa723c903554205e672fe6 100644 --- a/crates/anthropic/Cargo.toml +++ b/crates/anthropic/Cargo.toml @@ -26,4 +26,3 @@ serde_json.workspace = true settings.workspace = true strum.workspace = true thiserror.workspace = true -workspace-hack.workspace = true diff --git a/crates/askpass/Cargo.toml b/crates/askpass/Cargo.toml index 6aec7e6d7e011c626a57c478fa65e161f43b2bdd..298d1a736959d1021da49a2c4f4356e12cf014be 100644 --- a/crates/askpass/Cargo.toml +++ b/crates/askpass/Cargo.toml @@ -20,7 +20,6 @@ smol.workspace = true log.workspace = true tempfile.workspace = true util.workspace = true -workspace-hack.workspace = true zeroize.workspace = true [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml index 130394a30b7faf909e40922dd833dfcf9598d848..a56cd109f1be0eaa003d831ba31f4e288c94fd85 100644 --- a/crates/assets/Cargo.toml +++ b/crates/assets/Cargo.toml @@ -15,4 +15,3 @@ workspace = true anyhow.workspace = true gpui.workspace = true rust-embed.workspace = true -workspace-hack.workspace = true diff --git a/crates/assistant_context/Cargo.toml b/crates/assistant_context/Cargo.toml index 3e2761a84674c6c4201165edf856b675843315d9..2d3e8bc4080a314c480bb11e459a745cb7ce6704 100644 --- a/crates/assistant_context/Cargo.toml +++ b/crates/assistant_context/Cargo.toml @@ -51,7 +51,6 @@ ui.workspace = true util.workspace = true uuid.workspace = true workspace.workspace = true -workspace-hack.workspace = true zed_env_vars.workspace = true [dev-dependencies] diff --git a/crates/assistant_slash_command/Cargo.toml b/crates/assistant_slash_command/Cargo.toml index 0908cd61653d35dbb54ae325118a6091cd345a4e..1fc3e8448c5e2d0c278254b369ac49fd2e9ce33a 100644 --- a/crates/assistant_slash_command/Cargo.toml +++ b/crates/assistant_slash_command/Cargo.toml @@ -27,7 +27,6 @@ serde_json.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -workspace-hack.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_slash_commands/Cargo.toml b/crates/assistant_slash_commands/Cargo.toml index 5844d21a51b0642a89fd13f29f53a074331ee10e..85dd92501f93fb79ba1d3f70b3a06f1077356cfa 100644 --- a/crates/assistant_slash_commands/Cargo.toml +++ b/crates/assistant_slash_commands/Cargo.toml @@ -38,7 +38,6 @@ ui.workspace = true util.workspace = true workspace.workspace = true worktree.workspace = true -workspace-hack.workspace = true [dev-dependencies] fs = { workspace = true, features = ["test-support"] } diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 7f2fed80e2315e51fca7d8477b04885998336632..2aee764007a791176c6e41cb77f6efaf19aa3dc4 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -21,13 +21,12 @@ gpui.workspace = true denoise = { path = "../denoise" } log.workspace = true parking_lot.workspace = true -rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] } +rodio.workspace = true serde.workspace = true settings.workspace = true smol.workspace = true thiserror.workspace = true util.workspace = true -workspace-hack.workspace = true [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies] libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" } diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 21df028a88f027b1ce3796ef3e04998ca205ce51..08db9f8a97bb0783da987f84991ad1aaa62c2141 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -27,7 +27,6 @@ settings.workspace = true smol.workspace = true tempfile.workspace = true workspace.workspace = true -workspace-hack.workspace = true [target.'cfg(not(target_os = "windows"))'.dependencies] which.workspace = true diff --git a/crates/auto_update_helper/Cargo.toml b/crates/auto_update_helper/Cargo.toml index 6581de48d27f9975db6ddfe7ad63f49a55e4c22e..4b4a0126a4c178a76fd2a03b7596d42e19aa9d23 100644 --- a/crates/auto_update_helper/Cargo.toml +++ b/crates/auto_update_helper/Cargo.toml @@ -17,7 +17,6 @@ doctest = false anyhow.workspace = true log.workspace = true simplelog.workspace = true -workspace-hack.workspace = true [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/auto_update_ui/Cargo.toml b/crates/auto_update_ui/Cargo.toml index 6a8ba02b82683406e1f9bb3a2c5430fe614820df..0e31f94f5ee268cdc3274dea747bd0b05d9c80eb 100644 --- a/crates/auto_update_ui/Cargo.toml +++ b/crates/auto_update_ui/Cargo.toml @@ -25,4 +25,3 @@ serde_json.workspace = true smol.workspace = true util.workspace = true workspace.workspace = true -workspace-hack.workspace = true diff --git a/crates/aws_http_client/Cargo.toml b/crates/aws_http_client/Cargo.toml index 2749286d4c1361d9dbdb50d6566e3b4043f97b2e..24569a764dc4ab466c62ed3543df484327d1506d 100644 --- a/crates/aws_http_client/Cargo.toml +++ b/crates/aws_http_client/Cargo.toml @@ -18,4 +18,3 @@ default = [] aws-smithy-runtime-api.workspace = true aws-smithy-types.workspace = true http_client.workspace = true -workspace-hack.workspace = true diff --git a/crates/bedrock/Cargo.toml b/crates/bedrock/Cargo.toml index 3000af50bb71be18784a8e6a8f6da0ca8a66d7f9..f8f6fa46017309f3861ef2ec42f98d740cae7200 100644 --- a/crates/bedrock/Cargo.toml +++ b/crates/bedrock/Cargo.toml @@ -25,4 +25,3 @@ serde.workspace = true serde_json.workspace = true strum.workspace = true thiserror.workspace = true -workspace-hack.workspace = true diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index c25cfc3c86f26a72b3af37246ab30a175a68969a..16d0ff10e1cfef058422ed79934bec53f74c4804 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -21,7 +21,6 @@ theme.workspace = true ui.workspace = true workspace.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/buffer_diff/Cargo.toml b/crates/buffer_diff/Cargo.toml index 3d6c2a24e9de8dfb6e5fab7cff250fb3f26ec24d..1be21f3a0f1ef7aafa222a611d858f8adb097454 100644 --- a/crates/buffer_diff/Cargo.toml +++ b/crates/buffer_diff/Cargo.toml @@ -27,7 +27,6 @@ rope.workspace = true sum_tree.workspace = true text.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 1d5fbccb4644d9f168a2afd321a205f01c8f9cdc..ff034f914b0be44e6ec9f6475881ed79c368cd8a 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -41,7 +41,6 @@ telemetry.workspace = true util.workspace = true gpui_tokio.workspace = true livekit_client.workspace = true -workspace-hack.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index ab6e1dfc2b8dd0f89c4e6cd03e5ee66840003d6a..43af27ac8b6f21d4e1e16c9102da3de9c0585db4 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -31,7 +31,6 @@ settings.workspace = true text.workspace = true time.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 812e56d1730b8e2ada34e98aa85a5767eed77997..ea4a8de290921e1c7d4d4eb70a271799a1761dc2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -32,7 +32,6 @@ release_channel.workspace = true serde.workspace = true util.workspace = true tempfile.workspace = true -workspace-hack.workspace = true [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] exec.workspace = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 86ecb1b34e323289b542d3bd6f48520c50867ad6..513a73be4581f3b0c8069dde831cc6811f5e045b 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -57,7 +57,6 @@ tokio-socks = { version = "0.5.2", default-features = false, features = ["future tokio.workspace = true url.workspace = true util.workspace = true -workspace-hack.workspace = true worktree.workspace = true [dev-dependencies] diff --git a/crates/clock/Cargo.toml b/crates/clock/Cargo.toml index c2fa1e003a00a52a315ce5b6179bb07db40ce414..486cf0ba8bebc032481fc6bcbe908be05b7fd353 100644 --- a/crates/clock/Cargo.toml +++ b/crates/clock/Cargo.toml @@ -19,4 +19,3 @@ test-support = ["dep:parking_lot"] parking_lot = { workspace = true, optional = true } serde.workspace = true smallvec.workspace = true -workspace-hack.workspace = true diff --git a/crates/cloud_api_client/Cargo.toml b/crates/cloud_api_client/Cargo.toml index 8e50ccb191373fe2cfadce2e4fd12cc3e397357f..9dc009bf2e59ba848c93a6ebc65be566a2aabd55 100644 --- a/crates/cloud_api_client/Cargo.toml +++ b/crates/cloud_api_client/Cargo.toml @@ -20,5 +20,4 @@ gpui_tokio.workspace = true http_client.workspace = true parking_lot.workspace = true serde_json.workspace = true -workspace-hack.workspace = true yawc.workspace = true diff --git a/crates/cloud_api_types/Cargo.toml b/crates/cloud_api_types/Cargo.toml index 28e0a36a44f023e883bea98e4facacd9085e0efb..46d5d109b1bd5328c9c4d8b7cb1fbb8325e27656 100644 --- a/crates/cloud_api_types/Cargo.toml +++ b/crates/cloud_api_types/Cargo.toml @@ -17,7 +17,6 @@ chrono.workspace = true ciborium.workspace = true cloud_llm_client.workspace = true serde.workspace = true -workspace-hack.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/cloud_llm_client/Cargo.toml b/crates/cloud_llm_client/Cargo.toml index df432e79e2d0d900196c6a9205b36252d969fb06..c6a551a1fbd8a83e50f68fbcf47f26a6e96a1d24 100644 --- a/crates/cloud_llm_client/Cargo.toml +++ b/crates/cloud_llm_client/Cargo.toml @@ -21,7 +21,6 @@ serde = { workspace = true, features = ["derive", "rc"] } serde_json.workspace = true strum = { workspace = true, features = ["derive"] } uuid = { workspace = true, features = ["serde"] } -workspace-hack.workspace = true [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/cloud_zeta2_prompt/Cargo.toml b/crates/cloud_zeta2_prompt/Cargo.toml index f5b23d653bd84faed6ce1fca02f6c7436a0badc8..43446f460c872afcdfe1d4bc47d14f894f0c9c09 100644 --- a/crates/cloud_zeta2_prompt/Cargo.toml +++ b/crates/cloud_zeta2_prompt/Cargo.toml @@ -19,4 +19,3 @@ ordered-float.workspace = true rustc-hash.workspace = true serde.workspace = true strum.workspace = true -workspace-hack.workspace = true diff --git a/crates/codestral/Cargo.toml b/crates/codestral/Cargo.toml index 932834827f3516f48fed06ccf6c430935c725fee..b402274a33530424349081da764a4b6766e419e9 100644 --- a/crates/codestral/Cargo.toml +++ b/crates/codestral/Cargo.toml @@ -23,6 +23,5 @@ serde.workspace = true serde_json.workspace = true smol.workspace = true text.workspace = true -workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index d95b318b0e791b532a340bda94d945fb7c9485c1..d3ded583d6a8147f40ddc23f65114b666727a8d3 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -20,7 +20,7 @@ test-support = ["sqlite"] [dependencies] anyhow.workspace = true async-trait.workspace = true -async-tungstenite.workspace = true +async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots" ] } aws-config = { version = "1.1.5" } aws-sdk-kinesis = "1.51.0" aws-sdk-s3 = { version = "1.15.0" } @@ -47,7 +47,7 @@ reqwest = { version = "0.11", features = ["json"] } reqwest_client.workspace = true rpc.workspace = true scrypt = "0.11" -sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } +sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } semantic_version.workspace = true semver.workspace = true serde.workspace = true @@ -68,7 +68,6 @@ tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry", "tracing-log"] } # workaround for https://github.com/tokio-rs/tracing/issues/2927 util.workspace = true uuid.workspace = true -workspace-hack.workspace = true [dev-dependencies] agent_settings.workspace = true @@ -116,7 +115,7 @@ release_channel.workspace = true remote = { workspace = true, features = ["test-support"] } remote_server.workspace = true rpc = { workspace = true, features = ["test-support"] } -sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] } +sea-orm = { version = "=1.1.10", features = ["sqlx-sqlite"] } serde_json.workspace = true session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } diff --git a/crates/collab/src/db/queries/extensions.rs b/crates/collab/src/db/queries/extensions.rs index f218ff28507cf51a72cd0aa00a044ad75f64f839..914e78c6e3c327bf7f37a465771add478b3c68f5 100644 --- a/crates/collab/src/db/queries/extensions.rs +++ b/crates/collab/src/db/queries/extensions.rs @@ -255,7 +255,7 @@ impl Database { let insert = extension::Entity::insert(extension::ActiveModel { name: ActiveValue::Set(latest_version.name.clone()), - external_id: ActiveValue::Set(external_id.to_string()), + external_id: ActiveValue::Set((*external_id).to_owned()), id: ActiveValue::NotSet, latest_version: ActiveValue::Set(latest_version.version.to_string()), total_download_count: ActiveValue::NotSet, diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index cc22ee99b53b8590ff5e95e2c7bf46b1cb8ba71e..e92c269b7e8324cce5a042a56fc9cb395127b959 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -17,7 +17,7 @@ impl Database { .any(|existing| existing.name == **kind) }) .map(|kind| notification_kind::ActiveModel { - name: ActiveValue::Set(kind.to_string()), + name: ActiveValue::Set((*kind).to_owned()), ..Default::default() }) .collect(); @@ -260,7 +260,7 @@ pub fn model_to_proto(this: &Database, row: notification::Model) -> Result Vec {}: {}", src_path.display(), - dest_path.to_string(), + dest_path, String::from_utf8_lossy(&output.stderr) ); diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index a2ed840a95f56c4610c9ad479606b3fb371aeaab..4eca7c4d5295e4baf8b2812763a02c32701959f7 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -407,7 +407,7 @@ impl RemoteConnection for WslRemoteConnection { anyhow!( "failed to upload directory {} -> {}: {}", src_path.display(), - dest_path.to_string(), + dest_path, e ) })?; diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml index 6386dc330af8fd1eb46380cb39c71f4adffea1e6..14040ba4847d710be0a24a8bbddeb67a6aeb748b 100644 --- a/crates/repl/Cargo.toml +++ b/crates/repl/Cargo.toml @@ -16,7 +16,7 @@ doctest = false alacritty_terminal.workspace = true anyhow.workspace = true async-dispatcher.workspace = true -async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] } +async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots", "tokio-runtime"] } base64.workspace = true client.workspace = true collections.workspace = true @@ -51,7 +51,6 @@ util.workspace = true uuid.workspace = true workspace.workspace = true picker.workspace = true -workspace-hack.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/reqwest_client/Cargo.toml b/crates/reqwest_client/Cargo.toml index 68a354c13b94c01336791d021a926cacc6da4d62..7fd50237d9dc257f0ee7fe75134cd0456ad4928f 100644 --- a/crates/reqwest_client/Cargo.toml +++ b/crates/reqwest_client/Cargo.toml @@ -26,7 +26,6 @@ log.workspace = true tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } regex.workspace = true reqwest.workspace = true -workspace-hack.workspace = true [dev-dependencies] gpui.workspace = true diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml index 5d788abea82780a7e90c7f279bdaa0b7e1438828..17bd8d2a4b8977b2bf0079b84dc8f27a9999974b 100644 --- a/crates/rich_text/Cargo.toml +++ b/crates/rich_text/Cargo.toml @@ -27,4 +27,3 @@ pulldown-cmark.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true diff --git a/crates/rope/Cargo.toml b/crates/rope/Cargo.toml index 682b9aad92e355538333da358713d8c97f765b97..9dfe4cb333a2311982f1c49206214e270fd288c0 100644 --- a/crates/rope/Cargo.toml +++ b/crates/rope/Cargo.toml @@ -19,7 +19,6 @@ smallvec.workspace = true sum_tree.workspace = true unicode-segmentation.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 81764917a7e888a766571e4114f614f7391bc000..10ebde26b6b9242ecee9ef52cdb4a00323efaf3f 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -36,7 +36,6 @@ strum.workspace = true tracing = { version = "0.1.34", features = ["log"] } util.workspace = true zstd.workspace = true -workspace-hack.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/rules_library/Cargo.toml b/crates/rules_library/Cargo.toml index 298f77a2d2472dc8e01bbf0355d5193ed8832ff8..d2fdd765e044181ecb16535076fd31175ddb87c9 100644 --- a/crates/rules_library/Cargo.toml +++ b/crates/rules_library/Cargo.toml @@ -30,6 +30,5 @@ theme.workspace = true title_bar.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index 44436b34d490b94588af54b79abfbf3d60974a93..bbab41dcdb04bad70f390aac3625dcf73e68baa6 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -22,4 +22,3 @@ chrono.workspace = true futures.workspace = true parking_lot.workspace = true rand.workspace = true -workspace-hack.workspace = true diff --git a/crates/schema_generator/Cargo.toml b/crates/schema_generator/Cargo.toml index 09fe20adc3b0056ff6b8b269d2918445a684fd37..865f76f4af917606af5d61d173950493fdde07c7 100644 --- a/crates/schema_generator/Cargo.toml +++ b/crates/schema_generator/Cargo.toml @@ -16,4 +16,3 @@ schemars = { workspace = true, features = ["indexmap2"] } serde.workspace = true serde_json.workspace = true theme.workspace = true -workspace-hack.workspace = true diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 613f229d4d0ed0c0097a64ef4ead331331860ef8..3f06b4228714e67b87d83e69b1afeb2a8cb6a155 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -41,7 +41,6 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/semantic_version/Cargo.toml b/crates/semantic_version/Cargo.toml index add883e386983b6c8205d72aca3d5c72a98caef2..a8bd3ab5ccba24700cc8de9607f825d022967b0b 100644 --- a/crates/semantic_version/Cargo.toml +++ b/crates/semantic_version/Cargo.toml @@ -15,4 +15,3 @@ path = "src/semantic_version.rs" [dependencies] anyhow.workspace = true serde.workspace = true -workspace-hack.workspace = true diff --git a/crates/session/Cargo.toml b/crates/session/Cargo.toml index a0b9c5f2200f40f5c5583b9d23739551f79218f1..15c3acb8f08e3dce0a4c8a0698d0dd383d79cdd9 100644 --- a/crates/session/Cargo.toml +++ b/crates/session/Cargo.toml @@ -23,4 +23,3 @@ gpui.workspace = true uuid.workspace = true util.workspace = true serde_json.workspace = true -workspace-hack.workspace = true diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index cc2a39ce230ea0a8e8f9774cc2d8ee33d4e13037..c4b6bb878a4a1d960c3774fc393d138b530aa7ca 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -41,7 +41,6 @@ strum.workspace = true tree-sitter-json.workspace = true tree-sitter.workspace = true util.workspace = true -workspace-hack.workspace = true zlog.workspace = true [dev-dependencies] diff --git a/crates/settings_macros/Cargo.toml b/crates/settings_macros/Cargo.toml index 06ce1d01e5fa9d25b5c0d3c742a2d325ec996e39..175c2f26a3a37e5f93db25ad69aa10912c3c6adb 100644 --- a/crates/settings_macros/Cargo.toml +++ b/crates/settings_macros/Cargo.toml @@ -18,7 +18,6 @@ default = [] [dependencies] quote.workspace = true syn.workspace = true -workspace-hack.workspace = true [dev-dependencies] settings.workspace = true diff --git a/crates/settings_profile_selector/Cargo.toml b/crates/settings_profile_selector/Cargo.toml index 189272e54be02ac46840838f6874be64d1e06321..23ccac2e43dec6c1ab335eeb2ffb4d9159d85859 100644 --- a/crates/settings_profile_selector/Cargo.toml +++ b/crates/settings_profile_selector/Cargo.toml @@ -18,7 +18,6 @@ gpui.workspace = true picker.workspace = true settings.workspace = true ui.workspace = true -workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index ab5e1b839510a990e17e7abea63e6412e4a10e4b..b8a75694ecb7fdc4ce1d78f43c93832524f2d0e7 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -36,7 +36,6 @@ theme.workspace = true ui_input.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true log.workspace = true diff --git a/crates/snippet/Cargo.toml b/crates/snippet/Cargo.toml index f4c2d9a87465be8c319e373e6dfed9399b1ba4a4..2dde5c2d005ba699e644589219be94123edfb3b9 100644 --- a/crates/snippet/Cargo.toml +++ b/crates/snippet/Cargo.toml @@ -15,4 +15,3 @@ doctest = false [dependencies] anyhow.workspace = true smallvec.workspace = true -workspace-hack.workspace = true diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml index af7ffcf30ef71a21a6cdfd2efaf1ce3cf763016b..d71439118e90213335213e1365c766eb760bff44 100644 --- a/crates/snippet_provider/Cargo.toml +++ b/crates/snippet_provider/Cargo.toml @@ -23,7 +23,6 @@ serde_json_lenient.workspace = true snippet.workspace = true util.workspace = true schemars.workspace = true -workspace-hack.workspace = true [dev-dependencies] fs = { workspace = true, features = ["test-support"] } diff --git a/crates/snippets_ui/Cargo.toml b/crates/snippets_ui/Cargo.toml index 102374fc73cf8db4bd04c1db05b2b04a6ef38526..3139a41dada1c42f94de41d37e69be68a8de49a5 100644 --- a/crates/snippets_ui/Cargo.toml +++ b/crates/snippets_ui/Cargo.toml @@ -22,5 +22,4 @@ picker.workspace = true settings.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true workspace.workspace = true diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml index 6eb75aa171979283325d22300f95d584cee2cffb..5f4a0bef67efe3cf021d9d113922ca14f269fe85 100644 --- a/crates/sqlez/Cargo.toml +++ b/crates/sqlez/Cargo.toml @@ -21,4 +21,3 @@ sqlformat.workspace = true thread_local = "1.1.4" util.workspace = true uuid.workspace = true -workspace-hack.workspace = true diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml index dca7921450547e0c603e5485388173afa0a11a4d..cff96d0b8949757761421c9003250343297bd14c 100644 --- a/crates/sqlez_macros/Cargo.toml +++ b/crates/sqlez_macros/Cargo.toml @@ -17,4 +17,3 @@ doctest = false sqlez.workspace = true sqlformat.workspace = true syn.workspace = true -workspace-hack.workspace = true diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index a9db0b66b04f174c0e0fa10cdd4d485c0f571346..798461402de00c102af9325c091eb9edfdf89b09 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -15,4 +15,3 @@ workspace = true gpui.workspace = true itertools.workspace = true smallvec.workspace = true -workspace-hack.workspace = true diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index f545cb63dad18e31ce3b22019c6a918cfb2d059a..638d070cba14cb871d33d53a0df0acb19ecb3840 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -37,7 +37,6 @@ theme.workspace = true title_bar = { workspace = true, features = ["stories"] } ui = { workspace = true, features = ["stories"] } workspace.workspace = true -workspace-hack.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/streaming_diff/Cargo.toml b/crates/streaming_diff/Cargo.toml index 3774925289b9480c7e1a7362f15c97d47950feb9..b3645a182c3abf52c6ee2f2c23feaedeacf8574a 100644 --- a/crates/streaming_diff/Cargo.toml +++ b/crates/streaming_diff/Cargo.toml @@ -14,7 +14,6 @@ path = "src/streaming_diff.rs" [dependencies] ordered-float.workspace = true rope.workspace = true -workspace-hack.workspace = true [dev-dependencies] rand.workspace = true diff --git a/crates/sum_tree/Cargo.toml b/crates/sum_tree/Cargo.toml index fe7cfcec5f89a3c703132502323b06eb3a7633ae..81916c842225085ceec4721dbd8d212608f6bcb9 100644 --- a/crates/sum_tree/Cargo.toml +++ b/crates/sum_tree/Cargo.toml @@ -17,7 +17,6 @@ doctest = false arrayvec = "0.7.1" rayon.workspace = true log.workspace = true -workspace-hack.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml index 1ee8ca4ffc094210dd1edf231d9160556829745a..5b86367f35d508579ac6ba999fc8c9236e7fd66a 100644 --- a/crates/supermaven/Cargo.toml +++ b/crates/supermaven/Cargo.toml @@ -31,7 +31,6 @@ text.workspace = true ui.workspace = true unicode-segmentation.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/supermaven_api/Cargo.toml b/crates/supermaven_api/Cargo.toml index 6b6823095d5b4958856f552e14305c3d4f53e630..28868a9a7433f995e99b861cf7f6e9aeeb28942f 100644 --- a/crates/supermaven_api/Cargo.toml +++ b/crates/supermaven_api/Cargo.toml @@ -21,4 +21,3 @@ serde.workspace = true serde_json.workspace = true smol.workspace = true util.workspace = true -workspace-hack.workspace = true diff --git a/crates/svg_preview/Cargo.toml b/crates/svg_preview/Cargo.toml index 63e9e41bbe4d40945f854319a0adc88388f485cd..f64e60afe282da0da6780cc45097c751a8e7e8c1 100644 --- a/crates/svg_preview/Cargo.toml +++ b/crates/svg_preview/Cargo.toml @@ -18,4 +18,3 @@ gpui.workspace = true multi_buffer.workspace = true ui.workspace = true workspace.workspace = true -workspace-hack.workspace = true diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml index 8ef1b581ae21632c4894b132c9c52f617e016e7f..86ac3c09116a00d8061f88fb52c5fe884a1a3fe4 100644 --- a/crates/system_specs/Cargo.toml +++ b/crates/system_specs/Cargo.toml @@ -22,7 +22,6 @@ human_bytes.workspace = true release_channel.workspace = true serde.workspace = true sysinfo.workspace = true -workspace-hack.workspace = true [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] pciid-parser.workspace = true diff --git a/crates/tab_switcher/Cargo.toml b/crates/tab_switcher/Cargo.toml index d578c76f349d0f7b27764974ab2d1a9c84529dd6..36e4ba77342796ae5967e81cd34e01b8d41aecf6 100644 --- a/crates/tab_switcher/Cargo.toml +++ b/crates/tab_switcher/Cargo.toml @@ -27,7 +27,6 @@ smol.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -workspace-hack.workspace = true [dev-dependencies] anyhow.workspace = true diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml index dceaa63616decf21ec6ee6aaf99a3b2d6e2f715a..b3cb63bf006a972e08fc0ee56e01b6e31c6beee6 100644 --- a/crates/task/Cargo.toml +++ b/crates/task/Cargo.toml @@ -34,7 +34,6 @@ serde_json_lenient.workspace = true sha2.workspace = true shellexpand.workspace = true util.workspace = true -workspace-hack.workspace = true zed_actions.workspace = true [dev-dependencies] diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml index 77b143471871eb7d0578aefd7dfcbe968a0d9c5f..2f75a0b57c68161787634e42de7cdfd8d8d7b7a9 100644 --- a/crates/tasks_ui/Cargo.toml +++ b/crates/tasks_ui/Cargo.toml @@ -29,7 +29,6 @@ util.workspace = true workspace.workspace = true language.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/telemetry/Cargo.toml b/crates/telemetry/Cargo.toml index 680f12e5732b3e09261252d8463f0cb1f86b2195..ed166ea4c711df2779e067cb94b5e5a1f8869f25 100644 --- a/crates/telemetry/Cargo.toml +++ b/crates/telemetry/Cargo.toml @@ -16,4 +16,3 @@ serde.workspace = true serde_json.workspace = true telemetry_events.workspace = true futures.workspace = true -workspace-hack.workspace = true diff --git a/crates/telemetry_events/Cargo.toml b/crates/telemetry_events/Cargo.toml index d2bdcf20d73dc534c3695703c49dda856228d143..87a02baf06549748e7ac5ccf6ee6ae396681f87c 100644 --- a/crates/telemetry_events/Cargo.toml +++ b/crates/telemetry_events/Cargo.toml @@ -15,4 +15,3 @@ path = "src/telemetry_events.rs" semantic_version.workspace = true serde.workspace = true serde_json.workspace = true -workspace-hack.workspace = true diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 3c08c1b8617b0ae9c9ca1ec02a25243070e6f4db..0dc7338e04b79e2a50effbea180dccf1587c66b1 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -39,7 +39,6 @@ thiserror.workspace = true util.workspace = true regex.workspace = true urlencoding.workspace = true -workspace-hack.workspace = true itertools.workspace = true [target.'cfg(windows)'.dependencies] diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index 85ee506d69444fa7d58b536acac3a00088e3f047..1800562e2fd262d040ef957b402cc650681956a5 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -46,7 +46,6 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index e6c7d814948ea2657b2d7ef1786bd106fa4ea78a..ed02381eb83db5daececd159171a90072244a340 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -28,7 +28,6 @@ rope.workspace = true smallvec.workspace = true sum_tree.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 306733bf3496ae0c122b73fbd109eb46f3662b8a..ef193c500d461201e8746ad3ec0f33b01e423b18 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -36,7 +36,6 @@ strum.workspace = true thiserror.workspace = true util.workspace = true uuid.workspace = true -workspace-hack.workspace = true [dev-dependencies] fs = { workspace = true, features = ["test-support"] } diff --git a/crates/theme_extension/Cargo.toml b/crates/theme_extension/Cargo.toml index 718c35d4e268d3f23be771396cce791eeb2b3741..d94e15914b2dfbc8250641e8957366c27c2616a4 100644 --- a/crates/theme_extension/Cargo.toml +++ b/crates/theme_extension/Cargo.toml @@ -17,4 +17,3 @@ extension.workspace = true fs.workspace = true gpui.workspace = true theme.workspace = true -workspace-hack.workspace = true diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index 2fef5a62498d9ac0abfef3913edbd1dc711e5e64..a91ffc44544f898be35c4514910a6081b10b4a26 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -23,4 +23,3 @@ simplelog.workspace= true strum = { workspace = true, features = ["derive"] } theme.workspace = true vscode_theme = "0.2.0" -workspace-hack.workspace = true diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 8ec3e5b63f5341dcacfb1d60728844bcd9e89a26..1a563e81f202b484c846ed620aee3edd122fc80b 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -26,6 +26,5 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [dev-dependencies] diff --git a/crates/time_format/Cargo.toml b/crates/time_format/Cargo.toml index 5175a26a7803f81b928fba22a820e516515d3f34..b598d19887e128a0c5951c1d1bd5ec42f27f975b 100644 --- a/crates/time_format/Cargo.toml +++ b/crates/time_format/Cargo.toml @@ -15,7 +15,6 @@ doctest = false [dependencies] sys-locale.workspace = true time.workspace = true -workspace-hack.workspace = true [target.'cfg(target_os = "macos")'.dependencies] core-foundation.workspace = true diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 127fad3d8bdbf0348b946288007c81b952d14b58..829dea3a55ba9fee7f2ede503139e1348dabc57f 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -50,7 +50,6 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/toolchain_selector/Cargo.toml b/crates/toolchain_selector/Cargo.toml index a17f82564093e2ae17f95ec82559f308b910b2dd..94a655b7270c8e084a7b74b7711bb62c0a6a18aa 100644 --- a/crates/toolchain_selector/Cargo.toml +++ b/crates/toolchain_selector/Cargo.toml @@ -20,7 +20,6 @@ project.workspace = true ui.workspace = true util.workspace = true workspace.workspace = true -workspace-hack.workspace = true [lints] workspace = true diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 985a2bcdc7dadf3b28241b3d59744e48c654e76e..5eb58bf1da1f25cc273a9fc5d7c08b920d3471e9 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -30,7 +30,6 @@ strum.workspace = true theme.workspace = true ui_macros.workspace = true util.workspace = true -workspace-hack.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml index 0f107e42c382d55c2e2d6725336bc3af569a222d..7aa74591b82c7455de080a40d9c0955ad0384b59 100644 --- a/crates/ui_input/Cargo.toml +++ b/crates/ui_input/Cargo.toml @@ -21,7 +21,6 @@ picker.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true -workspace-hack.workspace = true [features] default = [] diff --git a/crates/ui_macros/Cargo.toml b/crates/ui_macros/Cargo.toml index 830b9dca8d5c42ec54db3c2aa323ede7d71aa5c9..74bd2186a7576fba067bf321972e4228c5292dec 100644 --- a/crates/ui_macros/Cargo.toml +++ b/crates/ui_macros/Cargo.toml @@ -15,7 +15,6 @@ proc-macro = true [dependencies] quote.workspace = true syn.workspace = true -workspace-hack.workspace = true [dev-dependencies] component.workspace = true diff --git a/crates/ui_prompt/Cargo.toml b/crates/ui_prompt/Cargo.toml index eefc71d257517c44a127ea75cb28ca28f7533705..55a98288433a7b31507310e20c4209a9d419e45f 100644 --- a/crates/ui_prompt/Cargo.toml +++ b/crates/ui_prompt/Cargo.toml @@ -22,4 +22,3 @@ settings.workspace = true theme.workspace = true ui.workspace = true workspace.workspace = true -workspace-hack.workspace = true diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 90054067673cb08f75393af4d1d1be26436e7c67..d7c5aae569ec0542c263d704e257ed6114bbe245 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -45,7 +45,6 @@ unicase.workspace = true util_macros = { workspace = true, optional = true } walkdir.workspace = true which.workspace = true -workspace-hack.workspace = true [target.'cfg(unix)'.dependencies] command-fds = "0.3.1" diff --git a/crates/util_macros/Cargo.toml b/crates/util_macros/Cargo.toml index d142abb0a655bb5db05bddd68a47c6a9137db756..f72955b3aeec58369fd8a24962524c144fdf3bc5 100644 --- a/crates/util_macros/Cargo.toml +++ b/crates/util_macros/Cargo.toml @@ -18,7 +18,6 @@ doctest = false quote.workspace = true syn.workspace = true perf.workspace = true -workspace-hack.workspace = true [features] perf-enabled = [] diff --git a/crates/vercel/Cargo.toml b/crates/vercel/Cargo.toml index 60fa1a2390b2ea4e1169765e55f62a36d3d281bf..98b26c91041ab59dfa479d6b619b1891b8d1397d 100644 --- a/crates/vercel/Cargo.toml +++ b/crates/vercel/Cargo.toml @@ -20,4 +20,3 @@ anyhow.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true strum.workspace = true -workspace-hack.workspace = true diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 8550550c9eae2bcf8e0f8c3a1cc65a48e7031ad2..9d6381f8e6aa9afdc8b6ce5fa81bbcf47cca21f5 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -53,7 +53,6 @@ util_macros.workspace = true vim_mode_setting.workspace = true workspace.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [dev-dependencies] assets.workspace = true diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml index 8371cca401fa77c63cba6748dc428645340f48b6..6306d125b27a5342a61f503520692c099ab9c4f6 100644 --- a/crates/vim_mode_setting/Cargo.toml +++ b/crates/vim_mode_setting/Cargo.toml @@ -14,4 +14,3 @@ path = "src/vim_mode_setting.rs" [dependencies] gpui.workspace = true settings.workspace = true -workspace-hack.workspace = true diff --git a/crates/watch/Cargo.toml b/crates/watch/Cargo.toml index 439a9af49f2906fc28768008a2c06d265b382584..9d77eaeddec66a08dd2e9d5056249671c9b02670 100644 --- a/crates/watch/Cargo.toml +++ b/crates/watch/Cargo.toml @@ -14,7 +14,6 @@ doctest = true [dependencies] parking_lot.workspace = true -workspace-hack.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/web_search/Cargo.toml b/crates/web_search/Cargo.toml index 4ba46faec4362ac98fffaffb6c606608c02373e8..d0e32e71f08a4b6fa9585d91dc9d5e8c459a8828 100644 --- a/crates/web_search/Cargo.toml +++ b/crates/web_search/Cargo.toml @@ -17,4 +17,3 @@ cloud_llm_client.workspace = true collections.workspace = true gpui.workspace = true serde.workspace = true -workspace-hack.workspace = true diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml index f7a248d10649dc83d7d76b454e8db2d37b55cbef..ecdca5883ff541459e94170986df3b7f16036c5a 100644 --- a/crates/web_search_providers/Cargo.toml +++ b/crates/web_search_providers/Cargo.toml @@ -22,4 +22,3 @@ language_model.workspace = true serde.workspace = true serde_json.workspace = true web_search.workspace = true -workspace-hack.workspace = true diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322eba7fdaf417606dd62ae73a0c3702b3..d5d3016ab2704392c6cc9cc4bcebf6d50701d3be 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -63,7 +63,6 @@ ui.workspace = true util.workspace = true uuid.workspace = true zed_actions.workspace = true -workspace-hack.workspace = true [target.'cfg(target_os = "windows")'.dependencies] windows.workspace = true diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml index 611091c11ff724ad43570f653b1c3644400f1f09..6d132fbd2cb8c7a1282bffcea6577260a15c4572 100644 --- a/crates/worktree/Cargo.toml +++ b/crates/worktree/Cargo.toml @@ -47,7 +47,6 @@ smol.workspace = true sum_tree.workspace = true text.workspace = true util.workspace = true -workspace-hack.workspace = true [dev-dependencies] clock = { workspace = true, features = ["test-support"] } diff --git a/crates/worktree_benchmarks/Cargo.toml b/crates/worktree_benchmarks/Cargo.toml index 6fcb66fea856cf2e47db5e79680eb83fb8c85a30..29681573adc9da43e579342194b971770f0a8743 100644 --- a/crates/worktree_benchmarks/Cargo.toml +++ b/crates/worktree_benchmarks/Cargo.toml @@ -9,7 +9,6 @@ fs.workspace = true gpui = { workspace = true, features = ["windows-manifest"] } settings.workspace = true worktree.workspace = true -workspace-hack = { version = "0.1", path = "../../tooling/workspace-hack" } [lints] workspace = true diff --git a/crates/x_ai/Cargo.toml b/crates/x_ai/Cargo.toml index 7ca0ca09397111404a59dff85d1ccf0659c0ea45..8ff020df8c1ccaf284157d8b46ddaa0e678b3cd7 100644 --- a/crates/x_ai/Cargo.toml +++ b/crates/x_ai/Cargo.toml @@ -20,4 +20,3 @@ anyhow.workspace = true schemars = { workspace = true, optional = true } serde.workspace = true strum.workspace = true -workspace-hack.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 44ab6c2285cf2a9d75393edbb053d21d35ea1840..409bee33ee4dee6562f6f8ea1388a2063f639ea3 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -158,7 +158,6 @@ vim_mode_setting.workspace = true watch.workspace = true web_search.workspace = true web_search_providers.workspace = true -workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true zed_env_vars.workspace = true diff --git a/crates/zed_actions/Cargo.toml b/crates/zed_actions/Cargo.toml index 3778d19621dc287d96adcf86239674f8d907d8ee..1a140c483fff56b8b045b381adc67cbce778be39 100644 --- a/crates/zed_actions/Cargo.toml +++ b/crates/zed_actions/Cargo.toml @@ -12,5 +12,4 @@ workspace = true gpui.workspace = true schemars.workspace = true serde.workspace = true -workspace-hack.workspace = true uuid.workspace = true diff --git a/crates/zed_env_vars/Cargo.toml b/crates/zed_env_vars/Cargo.toml index f56e3dd529cc7a8001d0021e96902f55034f88e2..1cf32174c351c28ec7eb16deab7b7986655d4a48 100644 --- a/crates/zed_env_vars/Cargo.toml +++ b/crates/zed_env_vars/Cargo.toml @@ -15,5 +15,4 @@ path = "src/zed_env_vars.rs" default = [] [dependencies] -workspace-hack.workspace = true gpui.workspace = true diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 09bcfa7f542ce9c01802c9cebc11dfc9a8da2542..821d3e0b9e7a5ff37302cf613f4e09b047f121f1 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -55,7 +55,6 @@ thiserror.workspace = true ui.workspace = true util.workspace = true uuid.workspace = true -workspace-hack.workspace = true workspace.workspace = true worktree.workspace = true zed_actions.workspace = true diff --git a/crates/zeta2/Cargo.toml b/crates/zeta2/Cargo.toml index f2ab34b2fb80036ff1e29393f2e9eaac1f85f444..7ca140fa353b6404e451fdb79cccfed982b64e27 100644 --- a/crates/zeta2/Cargo.toml +++ b/crates/zeta2/Cargo.toml @@ -34,7 +34,6 @@ serde_json.workspace = true thiserror.workspace = true util.workspace = true uuid.workspace = true -workspace-hack.workspace = true workspace.workspace = true worktree.workspace = true diff --git a/crates/zeta2_tools/Cargo.toml b/crates/zeta2_tools/Cargo.toml index 1cc9fa561a5cad964e2ef9617aaf9dde67407faf..b56b806e783b7e6acc946a9dadb00703e4a7f2c1 100644 --- a/crates/zeta2_tools/Cargo.toml +++ b/crates/zeta2_tools/Cargo.toml @@ -31,7 +31,6 @@ text.workspace = true ui.workspace = true ui_input.workspace = true util.workspace = true -workspace-hack.workspace = true workspace.workspace = true zeta2.workspace = true diff --git a/crates/zeta_cli/Cargo.toml b/crates/zeta_cli/Cargo.toml index bc78ac8bbad7b639612fad54ab004cc064912c4c..19dafefbdcf8ed577a54e686b6b0c4ed90cf4512 100644 --- a/crates/zeta_cli/Cargo.toml +++ b/crates/zeta_cli/Cargo.toml @@ -50,7 +50,6 @@ soa-rs = "0.8.1" terminal_view.workspace = true util.workspace = true watch.workspace = true -workspace-hack.workspace = true zeta.workspace = true zeta2.workspace = true zlog.workspace = true diff --git a/crates/zlog/Cargo.toml b/crates/zlog/Cargo.toml index 4b758437d5e1608aaa68e86f90215f57b928e883..2799592c8ebebbe088c17644bcba0378052e49bc 100644 --- a/crates/zlog/Cargo.toml +++ b/crates/zlog/Cargo.toml @@ -18,7 +18,6 @@ default = [] collections.workspace = true chrono.workspace = true log.workspace = true -workspace-hack.workspace = true anyhow.workspace = true [dev-dependencies] diff --git a/crates/zlog_settings/Cargo.toml b/crates/zlog_settings/Cargo.toml index 8ec63cefe447d944b4556f1e72e481d5461391f1..39c3b6a193481a276aea61b3f11c9959fb7e0e4a 100644 --- a/crates/zlog_settings/Cargo.toml +++ b/crates/zlog_settings/Cargo.toml @@ -19,4 +19,3 @@ gpui.workspace = true collections.workspace = true settings.workspace = true zlog.workspace = true -workspace-hack.workspace = true diff --git a/renovate.json b/renovate.json index 6e5630ad840223787447b9e9532d8afcfd86c18e..01ca7a46a1bfe63a383318aa65b5884d05ed0e4e 100644 --- a/renovate.json +++ b/renovate.json @@ -12,7 +12,7 @@ "timezone": "America/New_York", "schedule": ["after 3pm on Wednesday"], "prFooter": "Release Notes:\n\n- N/A", - "ignorePaths": ["**/node_modules/**", "tooling/workspace-hack/**"], + "ignorePaths": ["**/node_modules/**"], "packageRules": [ { "description": "Group wasmtime crates together.", diff --git a/script/new-crate b/script/new-crate index 1ac2d9262133c788969fe594b5b06480f1293fa7..52ee900b30837cbf77fa1e3145e0282fa5e19b7c 100755 --- a/script/new-crate +++ b/script/new-crate @@ -63,7 +63,6 @@ anyhow.workspace = true gpui.workspace = true ui.workspace = true util.workspace = true -workspace-hack.workspace = true # Uncomment other workspace dependencies as needed # assistant.workspace = true diff --git a/script/update-workspace-hack b/script/update-workspace-hack deleted file mode 100755 index 55f8aa9f926ace44652735f76c001f11be79b921..0000000000000000000000000000000000000000 --- a/script/update-workspace-hack +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -HAKARI_VERSION="0.9" - -cd "$(dirname "$0")/.." || exit 1 - -if ! cargo hakari --version | grep "cargo-hakari $HAKARI_VERSION" >/dev/null; then - echo "Installing cargo-hakari@^$HAKARI_VERSION..." - cargo install "cargo-hakari@^$HAKARI_VERSION" -else - echo "cargo-hakari@^$HAKARI_VERSION is already installed." -fi - -# update the workspace-hack crate -cargo hakari generate - -# make sure workspace-hack is added as a dep for all crates in the workspace -cargo hakari manage-deps diff --git a/script/update-workspace-hack.ps1 b/script/update-workspace-hack.ps1 deleted file mode 100644 index 060660724965da0c9bd05a30d5505afec6cea5e8..0000000000000000000000000000000000000000 --- a/script/update-workspace-hack.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -$ErrorActionPreference = "Stop" - -$HAKARI_VERSION = "0.9" - -$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path -Set-Location (Split-Path -Parent $scriptPath) - -$hakariInstalled = $false -try { - $versionOutput = cargo hakari --version 2>&1 - if ($versionOutput -match "cargo-hakari $HAKARI_VERSION") { - $hakariInstalled = $true - } -} -catch { - $hakariInstalled = $false -} - -if (-not $hakariInstalled) { - Write-Host "Installing cargo-hakari@^$HAKARI_VERSION..." - cargo install "cargo-hakari@^$HAKARI_VERSION" - if ($LASTEXITCODE -ne 0) { - throw "Failed to install cargo-hakari@^$HAKARI_VERSION" - } -} -else { - Write-Host "cargo-hakari@^$HAKARI_VERSION is already installed." -} - -# update the workspace-hack crate -cargo hakari generate -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - -# make sure workspace-hack is added as a dep for all crates in the workspace -cargo hakari manage-deps -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/tooling/perf/Cargo.toml b/tooling/perf/Cargo.toml index 15f951a165e48b36734d8a3d134bfadf408c9de0..d4acad1fdb80be1850582885a548162771619698 100644 --- a/tooling/perf/Cargo.toml +++ b/tooling/perf/Cargo.toml @@ -30,4 +30,3 @@ disallowed_methods = { level = "allow", priority = 1} collections.workspace = true serde.workspace = true serde_json.workspace = true -workspace-hack.workspace = true diff --git a/tooling/workspace-hack/.gitattributes b/tooling/workspace-hack/.gitattributes deleted file mode 100644 index 3e9dba4b64b5c01d9eeff4542140973313165470..0000000000000000000000000000000000000000 --- a/tooling/workspace-hack/.gitattributes +++ /dev/null @@ -1,4 +0,0 @@ -# Avoid putting conflict markers in the generated Cargo.toml file, since their presence breaks -# Cargo. -# Also do not check out the file as CRLF on Windows, as that's what hakari needs. -Cargo.toml merge=binary -crlf diff --git a/tooling/workspace-hack/.ignore b/tooling/workspace-hack/.ignore deleted file mode 100644 index 0eded960b456938b5f0937756a07b257451f9a26..0000000000000000000000000000000000000000 --- a/tooling/workspace-hack/.ignore +++ /dev/null @@ -1,2 +0,0 @@ -# prevent cargo-machete from analyzing this crate -Cargo.toml diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml deleted file mode 100644 index 794b74901c63eaa8b0ed9fc2722cd90105db9345..0000000000000000000000000000000000000000 --- a/tooling/workspace-hack/Cargo.toml +++ /dev/null @@ -1,700 +0,0 @@ -# This file is generated by `cargo hakari`. -# To regenerate, run: -# cargo install cargo-hakari -# cargo hakari generate - -[package] -name = "workspace-hack" -version = "0.1.0" -description = "workspace-hack package, managed by hakari" -edition.workspace = true -publish.workspace = true - -# The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments -# are managed by hakari. - -### BEGIN HAKARI SECTION -[dependencies] -ahash = { version = "0.8", features = ["serde"] } -aho-corasick = { version = "1" } -anstream = { version = "0.6" } -arrayvec = { version = "0.7", features = ["serde"] } -async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] } -async-std = { version = "1", features = ["attributes", "unstable"] } -async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] } -aws-config = { version = "1", features = ["behavior-version-latest"] } -aws-credential-types = { version = "1", default-features = false, features = ["hardcoded-credentials", "test-util"] } -aws-runtime = { version = "1", default-features = false, features = ["event-stream", "http-02x", "sigv4a"] } -aws-sigv4 = { version = "1", features = ["http0-compat", "sign-eventstream", "sigv4a"] } -aws-smithy-async = { version = "1", default-features = false, features = ["rt-tokio"] } -aws-smithy-http = { version = "0.62", default-features = false, features = ["event-stream"] } -aws-smithy-runtime = { version = "1", default-features = false, features = ["client", "default-https-client", "rt-tokio", "tls-rustls"] } -aws-smithy-runtime-api = { version = "1", features = ["client", "http-02x", "http-auth", "test-util"] } -aws-smithy-types = { version = "1", default-features = false, features = ["byte-stream-poll-next", "http-body-0-4-x", "http-body-1-x", "rt-tokio", "test-util"] } -base64 = { version = "0.22" } -base64ct = { version = "1", default-features = false, features = ["std"] } -bigdecimal = { version = "0.4", features = ["serde"] } -bit-set = { version = "0.8", default-features = false, features = ["std"] } -bit-vec = { version = "0.8", default-features = false, features = ["std"] } -bitflags = { version = "2", default-features = false, features = ["serde", "std"] } -bstr = { version = "1" } -bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "derive", "extern_crate_alloc", "must_cast"] } -byteorder = { version = "1" } -bytes = { version = "1", features = ["serde"] } -chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] } -clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] } -concurrent-queue = { version = "2" } -cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] } -crossbeam-channel = { version = "0.5" } -crossbeam-epoch = { version = "0.9" } -crossbeam-utils = { version = "0.8" } -deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] } -digest = { version = "0.10", features = ["mac", "oid", "std"] } -either = { version = "1", features = ["serde", "use_std"] } -euclid = { version = "0.22" } -event-listener = { version = "5" } -event-listener-strategy = { version = "0.5" } -flate2 = { version = "1", features = ["zlib-rs"] } -foldhash = { version = "0.1" } -form_urlencoded = { version = "1" } -futures = { version = "0.3", features = ["io-compat"] } -futures-channel = { version = "0.3", features = ["sink"] } -futures-core = { version = "0.3" } -futures-executor = { version = "0.3" } -futures-io = { version = "0.3" } -futures-sink = { version = "0.3" } -futures-task = { version = "0.3", default-features = false, features = ["std"] } -futures-util = { version = "0.3", features = ["channel", "io-compat", "sink"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } -half = { version = "2", features = ["bytemuck", "num-traits", "rand_distr", "use-intrinsics"] } -handlebars = { version = "4", features = ["rust-embed"] } -hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["rayon", "serde"] } -hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] } -hmac = { version = "0.12", default-features = false, features = ["reset"] } -hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] } -idna = { version = "1" } -indexmap = { version = "2", features = ["serde"] } -itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } -lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] } -libc = { version = "0.2", features = ["extra_traits"] } -libsqlite3-sys = { version = "0.30", features = ["bundled", "unlock_notify"] } -log = { version = "0.4", default-features = false, features = ["kv_unstable_serde"] } -lyon = { version = "1", default-features = false, features = ["extra"] } -lyon_path = { version = "1" } -md-5 = { version = "0.10" } -memchr = { version = "2" } -memmap2 = { version = "0.9", default-features = false, features = ["stable_deref_trait"] } -mime_guess = { version = "2" } -miniz_oxide = { version = "0.8", features = ["simd"] } -nom = { version = "7" } -num-bigint = { version = "0.4" } -num-integer = { version = "0.1", features = ["i128"] } -num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] } -num-rational = { version = "0.4", features = ["num-bigint-std"] } -num-traits = { version = "0.2", features = ["i128", "libm"] } -once_cell = { version = "1" } -percent-encoding = { version = "2" } -phf = { version = "0.11", features = ["macros"] } -phf_shared = { version = "0.11" } -prost-274715c4dabd11b0 = { package = "prost", version = "0.9" } -prost-types = { version = "0.9" } -rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] } -rand_chacha = { version = "0.3", default-features = false, features = ["std"] } -rand_core = { version = "0.6", default-features = false, features = ["std"] } -rand_distr = { version = "0.5" } -regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] } -regex = { version = "1" } -regex-automata = { version = "0.4" } -regex-syntax = { version = "0.8" } -reqwest = { version = "0.12", default-features = false, features = ["blocking", "http2", "json", "rustls-tls-native-roots", "stream"] } -ring = { version = "0.17", features = ["std"] } -rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] } -rustc-hash = { version = "1" } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net"] } -rustls = { version = "0.23", features = ["ring"] } -rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } -sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } -sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } -semver = { version = "1", features = ["serde"] } -serde = { version = "1", features = ["alloc", "derive", "rc"] } -serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } -serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] } -simd-adler32 = { version = "0.3" } -smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union"] } -spin = { version = "0.9" } -sqlx = { version = "0.8", features = ["bigdecimal", "chrono", "postgres", "runtime-tokio-rustls", "rust_decimal", "sqlite", "time", "uuid"] } -sqlx-postgres = { version = "0.8", default-features = false, features = ["any", "bigdecimal", "chrono", "json", "migrate", "offline", "rust_decimal", "time", "uuid"] } -sqlx-sqlite = { version = "0.8", default-features = false, features = ["any", "bundled", "chrono", "json", "migrate", "offline", "time", "uuid"] } -stable_deref_trait = { version = "1" } -strum = { version = "0.26", features = ["derive"] } -subtle = { version = "2" } -thiserror = { version = "2" } -time = { version = "0.3", features = ["local-offset", "macros", "serde-well-known"] } -tokio = { version = "1", features = ["full"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] } -tokio-util = { version = "0.7", features = ["codec", "compat", "io-util", "rt"] } -toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } -toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] } -tracing = { version = "0.1", features = ["log"] } -tracing-core = { version = "0.1" } -tungstenite = { version = "0.26", default-features = false, features = ["__rustls-tls", "handshake"] } -unicode-properties = { version = "0.1" } -url = { version = "2", features = ["serde"] } -uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] } -wasmparser = { version = "0.221" } -wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] } -wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] } -wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] } - -[build-dependencies] -ahash = { version = "0.8", features = ["serde"] } -aho-corasick = { version = "1" } -anstream = { version = "0.6" } -arrayvec = { version = "0.7", features = ["serde"] } -async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] } -async-std = { version = "1", features = ["attributes", "unstable"] } -async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] } -aws-config = { version = "1", features = ["behavior-version-latest"] } -aws-credential-types = { version = "1", default-features = false, features = ["hardcoded-credentials", "test-util"] } -aws-runtime = { version = "1", default-features = false, features = ["event-stream", "http-02x", "sigv4a"] } -aws-sigv4 = { version = "1", features = ["http0-compat", "sign-eventstream", "sigv4a"] } -aws-smithy-async = { version = "1", default-features = false, features = ["rt-tokio"] } -aws-smithy-http = { version = "0.62", default-features = false, features = ["event-stream"] } -aws-smithy-runtime = { version = "1", default-features = false, features = ["client", "default-https-client", "rt-tokio", "tls-rustls"] } -aws-smithy-runtime-api = { version = "1", features = ["client", "http-02x", "http-auth", "test-util"] } -aws-smithy-types = { version = "1", default-features = false, features = ["byte-stream-poll-next", "http-body-0-4-x", "http-body-1-x", "rt-tokio", "test-util"] } -base64 = { version = "0.22" } -base64ct = { version = "1", default-features = false, features = ["std"] } -bigdecimal = { version = "0.4", features = ["serde"] } -bit-set = { version = "0.8", default-features = false, features = ["std"] } -bit-vec = { version = "0.8", default-features = false, features = ["std"] } -bitflags = { version = "2", default-features = false, features = ["serde", "std"] } -bstr = { version = "1" } -bytemuck = { version = "1", default-features = false, features = ["aarch64_simd", "derive", "extern_crate_alloc", "must_cast"] } -byteorder = { version = "1" } -bytes = { version = "1", features = ["serde"] } -cc = { version = "1", default-features = false, features = ["parallel"] } -chrono = { version = "0.4", features = ["serde"] } -clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] } -clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] } -concurrent-queue = { version = "2" } -cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] } -crossbeam-channel = { version = "0.5" } -crossbeam-epoch = { version = "0.9" } -crossbeam-utils = { version = "0.8" } -deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] } -digest = { version = "0.10", features = ["mac", "oid", "std"] } -either = { version = "1", features = ["serde", "use_std"] } -euclid = { version = "0.22" } -event-listener = { version = "5" } -event-listener-strategy = { version = "0.5" } -flate2 = { version = "1", features = ["zlib-rs"] } -foldhash = { version = "0.1" } -form_urlencoded = { version = "1" } -futures = { version = "0.3", features = ["io-compat"] } -futures-channel = { version = "0.3", features = ["sink"] } -futures-core = { version = "0.3" } -futures-executor = { version = "0.3" } -futures-io = { version = "0.3" } -futures-sink = { version = "0.3" } -futures-task = { version = "0.3", default-features = false, features = ["std"] } -futures-util = { version = "0.3", features = ["channel", "io-compat", "sink"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["std"] } -half = { version = "2", features = ["bytemuck", "num-traits", "rand_distr", "use-intrinsics"] } -handlebars = { version = "4", features = ["rust-embed"] } -hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features = ["rayon", "serde"] } -hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] } -heck = { version = "0.4", features = ["unicode"] } -hmac = { version = "0.12", default-features = false, features = ["reset"] } -hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] } -idna = { version = "1" } -indexmap = { version = "2", features = ["serde"] } -itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" } -itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } -lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] } -libc = { version = "0.2", features = ["extra_traits"] } -libsqlite3-sys = { version = "0.30", features = ["bundled", "unlock_notify"] } -log = { version = "0.4", default-features = false, features = ["kv_unstable_serde"] } -lyon = { version = "1", default-features = false, features = ["extra"] } -lyon_path = { version = "1" } -md-5 = { version = "0.10" } -memchr = { version = "2" } -memmap2 = { version = "0.9", default-features = false, features = ["stable_deref_trait"] } -mime_guess = { version = "2" } -miniz_oxide = { version = "0.8", features = ["simd"] } -nom = { version = "7" } -num-bigint = { version = "0.4" } -num-integer = { version = "0.1", features = ["i128"] } -num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] } -num-rational = { version = "0.4", features = ["num-bigint-std"] } -num-traits = { version = "0.2", features = ["i128", "libm"] } -once_cell = { version = "1" } -percent-encoding = { version = "2" } -phf = { version = "0.11", features = ["macros"] } -phf_shared = { version = "0.11" } -prettyplease = { version = "0.2", default-features = false, features = ["verbatim"] } -proc-macro2 = { version = "1" } -prost-274715c4dabd11b0 = { package = "prost", version = "0.9" } -prost-types = { version = "0.9" } -quote = { version = "1" } -rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] } -rand_chacha = { version = "0.3", default-features = false, features = ["std"] } -rand_core = { version = "0.6", default-features = false, features = ["std"] } -rand_distr = { version = "0.5" } -regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] } -regex = { version = "1" } -regex-automata = { version = "0.4" } -regex-syntax = { version = "0.8" } -reqwest = { version = "0.12", default-features = false, features = ["blocking", "http2", "json", "rustls-tls-native-roots", "stream"] } -ring = { version = "0.17", features = ["std"] } -rust_decimal = { version = "1", default-features = false, features = ["maths", "serde", "std"] } -rustc-hash = { version = "1" } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net"] } -rustls = { version = "0.23", features = ["ring"] } -rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] } -sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] } -sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] } -semver = { version = "1", features = ["serde"] } -serde = { version = "1", features = ["alloc", "derive", "rc"] } -serde_core = { version = "1", default-features = false, features = ["alloc", "rc", "result", "std"] } -serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] } -simd-adler32 = { version = "0.3" } -smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union"] } -spin = { version = "0.9" } -sqlx = { version = "0.8", features = ["bigdecimal", "chrono", "postgres", "runtime-tokio-rustls", "rust_decimal", "sqlite", "time", "uuid"] } -sqlx-macros = { version = "0.8", features = ["_rt-tokio", "_tls-rustls-ring-webpki", "bigdecimal", "chrono", "derive", "json", "macros", "migrate", "postgres", "rust_decimal", "sqlite", "time", "uuid"] } -sqlx-macros-core = { version = "0.8", features = ["_rt-tokio", "_tls-rustls-ring-webpki", "bigdecimal", "chrono", "derive", "json", "macros", "migrate", "postgres", "rust_decimal", "sqlite", "time", "uuid"] } -sqlx-postgres = { version = "0.8", default-features = false, features = ["any", "bigdecimal", "chrono", "json", "migrate", "offline", "rust_decimal", "time", "uuid"] } -sqlx-sqlite = { version = "0.8", default-features = false, features = ["any", "bundled", "chrono", "json", "migrate", "offline", "time", "uuid"] } -stable_deref_trait = { version = "1" } -strum = { version = "0.26", features = ["derive"] } -subtle = { version = "2" } -syn-dff4ba8e3ae991db = { package = "syn", version = "1", features = ["extra-traits", "full"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -thiserror = { version = "2" } -time = { version = "0.3", features = ["local-offset", "macros", "serde-well-known"] } -time-macros = { version = "0.2", default-features = false, features = ["formatting", "parsing", "serde"] } -tokio = { version = "1", features = ["full"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["tls12"] } -tokio-util = { version = "0.7", features = ["codec", "compat", "io-util", "rt"] } -toml_datetime = { version = "0.6", default-features = false, features = ["serde"] } -toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] } -tracing = { version = "0.1", features = ["log"] } -tracing-core = { version = "0.1" } -tungstenite = { version = "0.26", default-features = false, features = ["__rustls-tls", "handshake"] } -unicode-properties = { version = "0.1" } -url = { version = "2", features = ["serde"] } -uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] } -wasmparser = { version = "0.221" } -wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] } -wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] } -wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] } - -[target.x86_64-apple-darwin.dependencies] -codespan-reporting = { version = "0.12" } -core-foundation = { version = "0.9" } -core-foundation-sys = { version = "0.8" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -objc2 = { version = "0.6" } -objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] } -objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } -objc2-metal = { version = "0.3" } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -security-framework = { version = "3", features = ["OSX_10_14"] } -security-framework-sys = { version = "2", features = ["OSX_10_14"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } - -[target.x86_64-apple-darwin.build-dependencies] -codespan-reporting = { version = "0.12" } -core-foundation = { version = "0.9" } -core-foundation-sys = { version = "0.8" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -objc2 = { version = "0.6" } -objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] } -objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } -objc2-metal = { version = "0.3" } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -security-framework = { version = "3", features = ["OSX_10_14"] } -security-framework-sys = { version = "2", features = ["OSX_10_14"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } - -[target.aarch64-apple-darwin.dependencies] -codespan-reporting = { version = "0.12" } -core-foundation = { version = "0.9" } -core-foundation-sys = { version = "0.8" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -objc2 = { version = "0.6" } -objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] } -objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } -objc2-metal = { version = "0.3" } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -security-framework = { version = "3", features = ["OSX_10_14"] } -security-framework-sys = { version = "2", features = ["OSX_10_14"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } - -[target.aarch64-apple-darwin.build-dependencies] -codespan-reporting = { version = "0.12" } -core-foundation = { version = "0.9" } -core-foundation-sys = { version = "0.8" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -naga = { version = "25", features = ["msl-out", "wgsl-in"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -objc2 = { version = "0.6" } -objc2-core-foundation = { version = "0.3", default-features = false, features = ["CFArray", "CFBase", "CFCGTypes", "CFData", "CFDate", "CFDictionary", "CFError", "CFNumber", "CFPlugInCOM", "CFRunLoop", "CFString", "CFURL", "CFUUID", "objc2", "std"] } -objc2-foundation = { version = "0.3", default-features = false, features = ["NSArray", "NSAttributedString", "NSBundle", "NSCoder", "NSData", "NSDate", "NSDictionary", "NSEnumerator", "NSError", "NSGeometry", "NSNotification", "NSNull", "NSObjCRuntime", "NSObject", "NSProcessInfo", "NSRange", "NSRunLoop", "NSString", "NSURL", "NSUndoManager", "NSValue", "objc2-core-foundation", "std"] } -objc2-metal = { version = "0.3" } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "process"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -security-framework = { version = "3", features = ["OSX_10_14"] } -security-framework-sys = { version = "2", features = ["OSX_10_14"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } - -[target.x86_64-unknown-linux-gnu.dependencies] -aes = { version = "0.8", default-features = false, features = ["zeroize"] } -ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] } -bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] } -cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } -codespan-reporting = { version = "0.12" } -crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -inout = { version = "0.1", default-features = false, features = ["block-padding"] } -linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } -linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -mio = { version = "1", features = ["net", "os-ext"] } -naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] } -num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } -num-complex = { version = "0.4", features = ["bytemuck"] } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -quote = { version = "1" } -rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -smallvec = { version = "1", default-features = false, features = ["write"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] } -wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] } -zeroize = { version = "1", features = ["zeroize_derive"] } -zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] } - -[target.x86_64-unknown-linux-gnu.build-dependencies] -aes = { version = "0.8", default-features = false, features = ["zeroize"] } -ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] } -bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] } -cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } -codespan-reporting = { version = "0.12" } -crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -inout = { version = "0.1", default-features = false, features = ["block-padding"] } -linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } -linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -mio = { version = "1", features = ["net", "os-ext"] } -naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] } -num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } -num-complex = { version = "0.4", features = ["bytemuck"] } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -smallvec = { version = "1", default-features = false, features = ["write"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] } -wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] } -zbus_macros = { version = "5", features = ["gvariant"] } -zeroize = { version = "1", features = ["zeroize_derive"] } -zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] } - -[target.aarch64-unknown-linux-gnu.dependencies] -aes = { version = "0.8", default-features = false, features = ["zeroize"] } -ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] } -bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] } -cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } -codespan-reporting = { version = "0.12" } -crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -inout = { version = "0.1", default-features = false, features = ["block-padding"] } -linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } -linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -mio = { version = "1", features = ["net", "os-ext"] } -naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] } -num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } -num-complex = { version = "0.4", features = ["bytemuck"] } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -quote = { version = "1" } -rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -smallvec = { version = "1", default-features = false, features = ["write"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] } -wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] } -zeroize = { version = "1", features = ["zeroize_derive"] } -zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] } - -[target.aarch64-unknown-linux-gnu.build-dependencies] -aes = { version = "0.8", default-features = false, features = ["zeroize"] } -ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] } -bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] } -cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } -codespan-reporting = { version = "0.12" } -crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -inout = { version = "0.1", default-features = false, features = ["block-padding"] } -linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } -linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -mio = { version = "1", features = ["net", "os-ext"] } -naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] } -num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } -num-complex = { version = "0.4", features = ["bytemuck"] } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -smallvec = { version = "1", default-features = false, features = ["write"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] } -wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] } -zbus_macros = { version = "5", features = ["gvariant"] } -zeroize = { version = "1", features = ["zeroize_derive"] } -zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] } - -[target.x86_64-pc-windows-msvc.dependencies] -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "fs", "net"] } -scopeguard = { version = "1" } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } -windows = { version = "0.61", features = ["Foundation_Numerics", "Storage_Search", "Storage_Streams", "System_Threading", "UI_ViewManagement", "Wdk_System_SystemInformation", "Wdk_System_SystemServices", "Wdk_System_Threading", "Win32_Globalization", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D_Fxc", "Win32_Graphics_DirectComposition", "Win32_Graphics_DirectWrite", "Win32_Graphics_Dwm", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Hlsl", "Win32_Graphics_Imaging", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_NetworkManagement_NetManagement", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Authorization", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com_StructuredStorage", "Win32_System_Console", "Win32_System_DataExchange", "Win32_System_Diagnostics_Debug", "Win32_System_Diagnostics_ToolHelp", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Power", "Win32_System_ProcessStatus", "Win32_System_Registry", "Win32_System_RemoteDesktop", "Win32_System_Rpc", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Variant", "Win32_System_WinRT", "Win32_System_WindowsProgramming", "Win32_System_Wmi", "Win32_UI_Controls", "Win32_UI_HiDpi", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging"] } -windows-core = { version = "0.61" } -windows-numerics = { version = "0.2" } -windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } -windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } -windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } -windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } - -[target.x86_64-pc-windows-msvc.build-dependencies] -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "fs", "net"] } -scopeguard = { version = "1" } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } -windows = { version = "0.61", features = ["Foundation_Numerics", "Storage_Search", "Storage_Streams", "System_Threading", "UI_ViewManagement", "Wdk_System_SystemInformation", "Wdk_System_SystemServices", "Wdk_System_Threading", "Win32_Globalization", "Win32_Graphics_Direct3D11", "Win32_Graphics_Direct3D_Fxc", "Win32_Graphics_DirectComposition", "Win32_Graphics_DirectWrite", "Win32_Graphics_Dwm", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Hlsl", "Win32_Graphics_Imaging", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_NetworkManagement_NetManagement", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Authorization", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com_StructuredStorage", "Win32_System_Console", "Win32_System_DataExchange", "Win32_System_Diagnostics_Debug", "Win32_System_Diagnostics_ToolHelp", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Power", "Win32_System_ProcessStatus", "Win32_System_Registry", "Win32_System_RemoteDesktop", "Win32_System_Rpc", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Variant", "Win32_System_WinRT", "Win32_System_WindowsProgramming", "Win32_System_Wmi", "Win32_UI_Controls", "Win32_UI_HiDpi", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging"] } -windows-core = { version = "0.61" } -windows-numerics = { version = "0.2" } -windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } -windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } -windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } -windows-sys-d4189bed749088b6 = { package = "windows-sys", version = "0.61", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] } - -[target.x86_64-unknown-linux-musl.dependencies] -aes = { version = "0.8", default-features = false, features = ["zeroize"] } -ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] } -bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] } -cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } -codespan-reporting = { version = "0.12" } -crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -inout = { version = "0.1", default-features = false, features = ["block-padding"] } -linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } -linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -mio = { version = "1", features = ["net", "os-ext"] } -naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] } -num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } -num-complex = { version = "0.4", features = ["bytemuck"] } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -quote = { version = "1" } -rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -smallvec = { version = "1", default-features = false, features = ["write"] } -syn-f595c2ba2a3f28df = { package = "syn", version = "2", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] } -wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] } -zeroize = { version = "1", features = ["zeroize_derive"] } -zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] } - -[target.x86_64-unknown-linux-musl.build-dependencies] -aes = { version = "0.8", default-features = false, features = ["zeroize"] } -ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std", "wayland"] } -bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] } -cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] } -codespan-reporting = { version = "0.12" } -crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] } -flume = { version = "0.11" } -getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } -getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] } -gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } -inout = { version = "0.1", default-features = false, features = ["block-padding"] } -linux-raw-sys-274715c4dabd11b0 = { package = "linux-raw-sys", version = "0.9", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "xdp"] } -linux-raw-sys-9fbad63c4bcf4a8f = { package = "linux-raw-sys", version = "0.4", default-features = false, features = ["elf", "errno", "general", "if_ether", "ioctl", "net", "netlink", "no_std", "prctl", "system", "xdp"] } -livekit-runtime = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d" } -mio = { version = "1", features = ["net", "os-ext"] } -naga = { version = "25", features = ["spv-out", "wgsl-in"] } -nix-1f5adca70f036a62 = { package = "nix", version = "0.28", features = ["fs", "mman", "ptrace", "signal", "term", "user"] } -nix-b73a96c0a5f6a7d9 = { package = "nix", version = "0.29", features = ["fs", "pthread", "signal", "user"] } -nix-fa1f6196edfd7249 = { package = "nix", version = "0.30", features = ["fs", "socket", "uio", "user"] } -num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] } -num-complex = { version = "0.4", features = ["bytemuck"] } -object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] } -proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } -prost-5ef9efb8ec2df382 = { package = "prost", version = "0.12", features = ["prost-derive"] } -rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } -rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "net", "param", "pipe", "process", "shm", "system"] } -rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", default-features = false, features = ["event", "pipe", "process", "pty", "stdio", "termios", "time"] } -scopeguard = { version = "1" } -smallvec = { version = "1", default-features = false, features = ["write"] } -tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "ring"] } -tokio-socks = { version = "0.5", features = ["futures-io"] } -tokio-stream = { version = "0.1", features = ["fs"] } -tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } -wayland-backend = { version = "0.3", default-features = false, features = ["client_system", "dlopen"] } -wayland-sys = { version = "0.31", default-features = false, features = ["client", "dlopen"] } -zbus_macros = { version = "5", features = ["gvariant"] } -zeroize = { version = "1", features = ["zeroize_derive"] } -zvariant = { version = "5", features = ["enumflags2", "gvariant", "url"] } - -### END HAKARI SECTION diff --git a/tooling/workspace-hack/LICENSE-GPL b/tooling/workspace-hack/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/tooling/workspace-hack/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/tooling/workspace-hack/build.rs b/tooling/workspace-hack/build.rs deleted file mode 100644 index 92518ef04cb3b9a0f5463a0c387e4f9a7ad39f93..0000000000000000000000000000000000000000 --- a/tooling/workspace-hack/build.rs +++ /dev/null @@ -1,2 +0,0 @@ -// A build script is required for cargo to consider build dependencies. -fn main() {} diff --git a/tooling/workspace-hack/src/lib.rs b/tooling/workspace-hack/src/lib.rs deleted file mode 100644 index 22489f632bdc1d52c2de57686c95b5081fce706b..0000000000000000000000000000000000000000 --- a/tooling/workspace-hack/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// This is a stub lib.rs. diff --git a/tooling/xtask/Cargo.toml b/tooling/xtask/Cargo.toml index 8f968e0ca6eb81b6bebaec5c17bfac2baf3d5c79..aa06e6164683edd3bb011136a127b9fb99215e52 100644 --- a/tooling/xtask/Cargo.toml +++ b/tooling/xtask/Cargo.toml @@ -16,4 +16,3 @@ clap = { workspace = true, features = ["derive"] } toml.workspace = true indoc.workspace = true toml_edit.workspace = true -workspace-hack.workspace = true diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs index c8bed4bb35185430d7942d4e2a6b704b6fcddff3..e1fd15112fda6f1192cfc65a3d92b89b2e88777a 100644 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ b/tooling/xtask/src/tasks/package_conformity.rs @@ -38,11 +38,6 @@ pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> { continue; } - // Ignore `workspace-hack`, as it produces a lot of false positives. - if package.name == "workspace-hack" { - continue; - } - for dependencies in [ &cargo_toml.dependencies, &cargo_toml.dev_dependencies, From 7e97fcaacb1dd83eaf666f52d5a88bf4c6c29127 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Fri, 17 Oct 2025 21:56:57 +0200 Subject: [PATCH 008/202] Reduce `display_map` snapshot creation (#39354) Re-applies https://github.com/zed-industries/zed/pull/30840 This PR re-applies the initial [PR](https://github.com/zed-industries/zed/pull/30840). As it was closed because it was hard to land, because of the many conflicts. This PR re-applies the changes for it. In several cases we were creating multiple display_map snapshots within the same root-level function call. Creating a display_map snapshot is quite slow, and in some cases we were creating the snapshot multiple times. Release Notes: - N/A --- .../agent_ui/src/acp/completion_provider.rs | 4 +- crates/agent_ui/src/agent_diff.rs | 41 ++- crates/agent_ui/src/context_picker.rs | 8 +- crates/agent_ui/src/inline_assistant.rs | 17 +- crates/agent_ui/src/text_thread_editor.rs | 45 ++- .../src/selection_command.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 4 +- crates/collab/src/tests/following_tests.rs | 59 +++- crates/collab_ui/src/channel_view.rs | 9 +- crates/command_palette/src/command_palette.rs | 6 +- crates/debugger_ui/src/debugger_ui.rs | 11 +- .../src/session/running/console.rs | 8 +- crates/debugger_ui/src/stack_trace_view.rs | 5 +- .../src/tests/new_process_modal.rs | 5 +- crates/diagnostics/src/items.rs | 5 +- crates/editor/src/editor.rs | 289 +++++++++++------- crates/editor/src/editor_tests.rs | 263 +++++++++++----- crates/editor/src/element.rs | 19 +- crates/editor/src/indent_guides.rs | 2 +- crates/editor/src/items.rs | 14 +- crates/editor/src/linked_editing_ranges.rs | 2 +- crates/editor/src/mouse_context_menu.rs | 6 +- crates/editor/src/scroll/actions.rs | 21 +- crates/editor/src/scroll/autoscroll.rs | 4 +- crates/editor/src/selections_collection.rs | 115 ++++--- crates/editor/src/signature_help.rs | 2 +- crates/editor/src/tasks.rs | 2 +- crates/editor/src/test.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 14 +- crates/file_finder/src/file_finder_tests.rs | 4 +- crates/git_ui/src/text_diff_view.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 6 +- crates/language_tools/src/lsp_log_view.rs | 7 +- crates/language_tools/src/syntax_tree_view.rs | 5 +- .../src/markdown_preview_view.rs | 8 +- crates/outline/src/outline.rs | 7 +- crates/outline_panel/src/outline_panel.rs | 19 +- .../project_panel/src/project_panel_tests.rs | 6 +- crates/repl/src/repl_editor.rs | 10 +- crates/vim/src/change_list.rs | 9 +- crates/vim/src/command.rs | 22 +- crates/vim/src/helix.rs | 28 +- crates/vim/src/helix/duplicate.rs | 3 +- crates/vim/src/helix/paste.rs | 3 +- crates/vim/src/insert.rs | 2 +- crates/vim/src/normal.rs | 28 +- crates/vim/src/normal/convert.rs | 2 +- crates/vim/src/normal/increment.rs | 2 +- crates/vim/src/normal/mark.rs | 31 +- crates/vim/src/normal/paste.rs | 5 +- crates/vim/src/normal/scroll.rs | 10 +- crates/vim/src/normal/substitute.rs | 5 +- crates/vim/src/normal/yank.rs | 4 +- crates/vim/src/replace.rs | 12 +- crates/vim/src/state.rs | 4 +- crates/vim/src/surrounds.rs | 15 +- crates/vim/src/test.rs | 5 +- crates/vim/src/vim.rs | 17 +- crates/vim/src/visual.rs | 11 +- .../zed/src/zed/quick_action_bar/repl_menu.rs | 3 +- 60 files changed, 853 insertions(+), 426 deletions(-) diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 73f0622df878c2abc1d2feef945ef2e771dceaf9..c5ab47fe18970791c047ef157f6664188c95e346 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -652,7 +652,9 @@ impl ContextPickerCompletionProvider { .active_item(cx) .and_then(|item| item.downcast::()) .is_some_and(|editor| { - editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx)) + editor.update(cx, |editor, cx| { + editor.has_non_empty_selection(&editor.display_snapshot(cx)) + }) }); if has_selection { entries.push(ContextPickerEntry::Action( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 67014e3c3a4c8bd9b43f34d9cad3c23832efdc13..c2b624ecfca2bed4c9480bf681fe17b43b569685 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -452,7 +452,10 @@ fn update_editor_selection( window: &mut Window, cx: &mut Context, ) { - let newest_cursor = editor.selections.newest::(cx).head(); + let newest_cursor = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); if !diff_hunks.iter().any(|hunk| { hunk.row_range @@ -1895,7 +1898,9 @@ mod tests { ); assert_eq!( editor - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(1, 0)..Point::new(1, 0) ); @@ -1909,7 +1914,9 @@ mod tests { ); assert_eq!( editor - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(3, 0)..Point::new(3, 0) ); @@ -1930,7 +1937,9 @@ mod tests { ); assert_eq!( editor - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(3, 0)..Point::new(3, 0) ); @@ -1962,7 +1971,9 @@ mod tests { ); assert_eq!( editor - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(3, 0)..Point::new(3, 0) ); @@ -2119,7 +2130,9 @@ mod tests { ); assert_eq!( editor1 - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(1, 0)..Point::new(1, 0) ); @@ -2160,7 +2173,9 @@ mod tests { ); assert_eq!( editor1 - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(3, 0)..Point::new(3, 0) ); @@ -2181,7 +2196,9 @@ mod tests { ); assert_eq!( editor1 - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(3, 0)..Point::new(3, 0) ); @@ -2207,7 +2224,9 @@ mod tests { ); assert_eq!( editor1 - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(3, 0)..Point::new(3, 0) ); @@ -2240,7 +2259,9 @@ mod tests { ); assert_eq!( editor2 - .update(cx, |editor, cx| editor.selections.newest::(cx)) + .update(cx, |editor, cx| editor + .selections + .newest::(&editor.display_snapshot(cx))) .range(), Point::new(0, 0)..Point::new(0, 0) ); diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index cfb2ce0a60441c18d62965dddf6a626c4b4a4243..caffb31521e397ca7cd6b1fa0c8f4ae73d5ab9ff 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -606,7 +606,11 @@ pub(crate) fn available_context_picker_entries( .read(cx) .active_item(cx) .and_then(|item| item.downcast::()) - .is_some_and(|editor| editor.update(cx, |editor, cx| editor.has_non_empty_selection(cx))); + .is_some_and(|editor| { + editor.update(cx, |editor, cx| { + editor.has_non_empty_selection(&editor.display_snapshot(cx)) + }) + }); if has_selection { entries.push(ContextPickerEntry::Action( ContextPickerAction::AddSelections, @@ -725,7 +729,7 @@ pub(crate) fn selection_ranges( }; editor.update(cx, |editor, cx| { - let selections = editor.selections.all_adjusted(cx); + let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx)); let buffer = editor.buffer().clone().read(cx); let snapshot = buffer.snapshot(cx); diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 4c09e475b10881ab9bc2327b5b18b1c66e2ba4ad..0f7f1f1d78056553f758796c7e6b2f14781fce0f 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -363,9 +363,12 @@ impl InlineAssistant { cx: &mut App, ) { let (snapshot, initial_selections, newest_selection) = editor.update(cx, |editor, cx| { - let selections = editor.selections.all::(cx); - let newest_selection = editor.selections.newest::(cx); - (editor.snapshot(window, cx), selections, newest_selection) + let snapshot = editor.snapshot(window, cx); + let selections = editor.selections.all::(&snapshot.display_snapshot); + let newest_selection = editor + .selections + .newest::(&snapshot.display_snapshot); + (snapshot, selections, newest_selection) }); // Check if there is already an inline assistant that contains the @@ -798,7 +801,9 @@ impl InlineAssistant { if editor.read(cx).selections.count() == 1 { let (selection, buffer) = editor.update(cx, |editor, cx| { ( - editor.selections.newest::(cx), + editor + .selections + .newest::(&editor.display_snapshot(cx)), editor.buffer().read(cx).snapshot(cx), ) }); @@ -829,7 +834,9 @@ impl InlineAssistant { if editor.read(cx).selections.count() == 1 { let (selection, buffer) = editor.update(cx, |editor, cx| { ( - editor.selections.newest::(cx), + editor + .selections + .newest::(&editor.display_snapshot(cx)), editor.buffer().read(cx).snapshot(cx), ) }); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 1b19ef1aa4046b1159d849e56a9e959d13dc53ce..2d28d95450726554787f6a9cb211e852ceaccddf 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -431,9 +431,9 @@ impl TextThreadEditor { } fn cursors(&self, cx: &mut App) -> Vec { - let selections = self - .editor - .update(cx, |editor, cx| editor.selections.all::(cx)); + let selections = self.editor.update(cx, |editor, cx| { + editor.selections.all::(&editor.display_snapshot(cx)) + }); selections .into_iter() .map(|selection| selection.head()) @@ -446,7 +446,10 @@ impl TextThreadEditor { editor.transact(window, cx, |editor, window, cx| { editor.change_selections(Default::default(), window, cx, |s| s.try_cancel()); let snapshot = editor.buffer().read(cx).snapshot(cx); - let newest_cursor = editor.selections.newest::(cx).head(); + let newest_cursor = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); if newest_cursor.column > 0 || snapshot .chars_at(newest_cursor) @@ -1248,11 +1251,19 @@ impl TextThreadEditor { let context_editor = context_editor_view.read(cx).editor.clone(); context_editor.update(cx, |context_editor, cx| { - if context_editor.selections.newest::(cx).is_empty() { + let display_map = context_editor.display_snapshot(cx); + if context_editor + .selections + .newest::(&display_map) + .is_empty() + { let snapshot = context_editor.buffer().read(cx).snapshot(cx); let (_, _, snapshot) = snapshot.as_singleton()?; - let head = context_editor.selections.newest::(cx).head(); + let head = context_editor + .selections + .newest::(&display_map) + .head(); let offset = snapshot.point_to_offset(head); let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?; @@ -1269,7 +1280,7 @@ impl TextThreadEditor { (!text.is_empty()).then_some((text, true)) } else { - let selection = context_editor.selections.newest_adjusted(cx); + let selection = context_editor.selections.newest_adjusted(&display_map); let buffer = context_editor.buffer().read(cx).snapshot(cx); let selected_text = buffer.text_for_range(selection.range()).collect::(); @@ -1457,7 +1468,7 @@ impl TextThreadEditor { let selections = editor.update(cx, |editor, cx| { editor .selections - .all_adjusted(cx) + .all_adjusted(&editor.display_snapshot(cx)) .into_iter() .filter_map(|s| { (!s.is_empty()) @@ -1489,7 +1500,10 @@ impl TextThreadEditor { self.editor.update(cx, |editor, cx| { editor.insert("\n", window, cx); for (text, crease_title) in creases { - let point = editor.selections.newest::(cx).head(); + let point = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); let start_row = MultiBufferRow(point.row); editor.insert(&text, window, cx); @@ -1561,7 +1575,9 @@ impl TextThreadEditor { cx: &mut Context, ) -> (String, CopyMetadata, Vec>) { let (mut selection, creases) = self.editor.update(cx, |editor, cx| { - let mut selection = editor.selections.newest_adjusted(cx); + let mut selection = editor + .selections + .newest_adjusted(&editor.display_snapshot(cx)); let snapshot = editor.buffer().read(cx).snapshot(cx); selection.goal = SelectionGoal::None; @@ -1680,7 +1696,10 @@ impl TextThreadEditor { if images.is_empty() { self.editor.update(cx, |editor, cx| { - let paste_position = editor.selections.newest::(cx).head(); + let paste_position = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); editor.paste(action, window, cx); if let Some(metadata) = metadata { @@ -1727,13 +1746,13 @@ impl TextThreadEditor { editor.transact(window, cx, |editor, _window, cx| { let edits = editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|selection| (selection.start..selection.end, "\n")); editor.edit(edits, cx); let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all::(cx) { + for selection in editor.selections.all::(&editor.display_snapshot(cx)) { image_positions.push(snapshot.anchor_before(selection.end)); } }); diff --git a/crates/assistant_slash_commands/src/selection_command.rs b/crates/assistant_slash_commands/src/selection_command.rs index c8692dec718a03af777753f35ae646f245878ed9..ce6c0b931411d8073ffd6c97b648bb044ad857e7 100644 --- a/crates/assistant_slash_commands/src/selection_command.rs +++ b/crates/assistant_slash_commands/src/selection_command.rs @@ -79,7 +79,7 @@ impl SlashCommand for SelectionCommand { editor.update(cx, |editor, cx| { let selection_ranges = editor .selections - .all_adjusted(cx) + .all_adjusted(&editor.display_snapshot(cx)) .iter() .map(|selection| selection.range()) .collect::>(); diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 9a923c3fb3a11e0ecd01706c19c0087de6475a09..6e6a815a0b4c0fc8e8e2f367738a60aea604b5e1 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -877,7 +877,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T 6..9 ); rename.editor.update(cx, |rename_editor, cx| { - let rename_selection = rename_editor.selections.newest::(cx); + let rename_selection = rename_editor.selections.newest::(&rename_editor.display_snapshot(cx)); assert_eq!( rename_selection.range(), 0..3, @@ -924,7 +924,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T let lsp_rename_end = rename.range.end.to_offset(&buffer); assert_eq!(lsp_rename_start..lsp_rename_end, 6..9); rename.editor.update(cx, |rename_editor, cx| { - let rename_selection = rename_editor.selections.newest::(cx); + let rename_selection = rename_editor.selections.newest::(&rename_editor.display_snapshot(cx)); assert_eq!( rename_selection.range(), 1..2, diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 6f4a819f440929a9cc004cc018169420f758d264..ab72ce3605b7c93bac05dc6321b44b7abb964d93 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -122,13 +122,19 @@ async fn test_basic_following( editor.handle_input("b", window, cx); editor.handle_input("c", window, cx); editor.select_left(&Default::default(), window, cx); - assert_eq!(editor.selections.ranges(cx), vec![3..2]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![3..2] + ); }); editor_a2.update_in(cx_a, |editor, window, cx| { editor.handle_input("d", window, cx); editor.handle_input("e", window, cx); editor.select_left(&Default::default(), window, cx); - assert_eq!(editor.selections.ranges(cx), vec![2..1]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![2..1] + ); }); // When client B starts following client A, only the active view state is replicated to client B. @@ -149,11 +155,15 @@ async fn test_basic_following( Some((worktree_id, rel_path("2.txt")).into()) ); assert_eq!( - editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + editor_b2.update(cx_b, |editor, cx| editor + .selections + .ranges(&editor.display_snapshot(cx))), vec![2..1] ); assert_eq!( - editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + editor_b1.update(cx_b, |editor, cx| editor + .selections + .ranges(&editor.display_snapshot(cx))), vec![3..3] ); @@ -384,7 +394,10 @@ async fn test_basic_following( cx_b.background_executor.run_until_parked(); editor_b1.update(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + &[1..1, 2..2] + ); }); editor_a1.update_in(cx_a, |editor, window, cx| { @@ -402,7 +415,10 @@ async fn test_basic_following( executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); executor.run_until_parked(); editor_b1.update(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), &[3..3]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + &[3..3] + ); }); // After unfollowing, client B stops receiving updates from client A. @@ -1679,7 +1695,10 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); cx_a.run_until_parked(); editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), vec![1..1]) + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![1..1] + ) }); // a unshares the project @@ -1701,7 +1720,10 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T .advance_clock(workspace::item::LEADER_UPDATE_THROTTLE); cx_a.run_until_parked(); editor_b.update(cx_b, |editor, cx| { - assert_eq!(editor.selections.ranges(cx), vec![1..1]) + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![1..1] + ) }); cx_b.update(|_, cx| { let room = ActiveCall::global(cx).read(cx).room().unwrap().read(cx); @@ -1799,13 +1821,19 @@ async fn test_following_into_excluded_file( editor.handle_input("b", window, cx); editor.handle_input("c", window, cx); editor.select_left(&Default::default(), window, cx); - assert_eq!(editor.selections.ranges(cx), vec![3..2]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![3..2] + ); }); editor_for_excluded_a.update_in(cx_a, |editor, window, cx| { editor.select_all(&Default::default(), window, cx); editor.handle_input("new commit message", window, cx); editor.select_left(&Default::default(), window, cx); - assert_eq!(editor.selections.ranges(cx), vec![18..17]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![18..17] + ); }); // When client B starts following client A, currently visible file is replicated @@ -1827,7 +1855,9 @@ async fn test_following_into_excluded_file( Some((worktree_id, rel_path(".git/COMMIT_EDITMSG")).into()) ); assert_eq!( - editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)), + editor_for_excluded_b.update(cx_b, |editor, cx| editor + .selections + .ranges(&editor.display_snapshot(cx))), vec![18..17] ); @@ -2037,7 +2067,12 @@ async fn test_following_to_channel_notes_without_a_shared_project( assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); notes.editor.update(cx, |editor, cx| { assert_eq!(editor.text(cx), "Hello from A."); - assert_eq!(editor.selections.ranges::(cx), &[3..4]); + assert_eq!( + editor + .selections + .ranges::(&editor.display_snapshot(cx)), + &[3..4] + ); }) }); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index e37abbbccdbcdb7335b45b3fbe01d8797541e336..4e4bd2ca958d20225a7188b1f7f601e879e22835 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -287,9 +287,12 @@ impl ChannelView { } fn copy_link(&mut self, _: &CopyLink, window: &mut Window, cx: &mut Context) { - let position = self - .editor - .update(cx, |editor, cx| editor.selections.newest_display(cx).start); + let position = self.editor.update(cx, |editor, cx| { + editor + .selections + .newest_display(&editor.display_snapshot(cx)) + .start + }); self.copy_link_for_position(position, window, cx) } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index e718fe99297802ea90d425aef5063f3b55a86579..f9ed9ec6faf6b1cefbd9159a06e145b32c752c1f 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -698,7 +698,11 @@ mod tests { editor.update_in(cx, |editor, window, cx| { assert!(editor.focus_handle(cx).is_focused(window)); assert_eq!( - editor.selections.last::(cx).range().start, + editor + .selections + .last::(&editor.display_snapshot(cx)) + .range() + .start, Point::new(2, 0) ); }); diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 689e3cd878b574d31963231df9bcff317ea6d64c..78cc9e9bd28beb31474c12662d7e118eae6f066e 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -341,8 +341,10 @@ pub fn init(cx: &mut App) { maybe!({ let (buffer, position, _) = editor .update(cx, |editor, cx| { - let cursor_point: language::Point = - editor.selections.newest(cx).head(); + let cursor_point: language::Point = editor + .selections + .newest(&editor.display_snapshot(cx)) + .head(); editor .buffer() @@ -392,7 +394,10 @@ pub fn init(cx: &mut App) { let text = editor .update(cx, |editor, cx| { editor.text_for_range( - editor.selections.newest(cx).range(), + editor + .selections + .newest(&editor.display_snapshot(cx)) + .range(), &mut None, window, cx, diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 635955dda9c1c26d7e501c7991d2afc9fa1c9bb1..29cdf9a8067c099a8454ad21b459853cf3982f1a 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -963,8 +963,12 @@ mod tests { ) { cx.set_state(input); - let buffer_position = - cx.editor(|editor, _, cx| editor.selections.newest::(cx).start); + let buffer_position = cx.editor(|editor, _, cx| { + editor + .selections + .newest::(&editor.display_snapshot(cx)) + .start + }); let snapshot = &cx.buffer_snapshot(); diff --git a/crates/debugger_ui/src/stack_trace_view.rs b/crates/debugger_ui/src/stack_trace_view.rs index 3806e77b6e932b90f4dec143ddacd40a02e6e421..07caabaacaf00d2752a04c5ba68be07a5678c40a 100644 --- a/crates/debugger_ui/src/stack_trace_view.rs +++ b/crates/debugger_ui/src/stack_trace_view.rs @@ -55,7 +55,10 @@ impl StackTraceView { cx.subscribe_in(&editor, window, |this, editor, event, window, cx| { if let EditorEvent::SelectionsChanged { local: true } = event { let excerpt_id = editor.update(cx, |editor, cx| { - let position: Point = editor.selections.newest(cx).head(); + let position: Point = editor + .selections + .newest(&editor.display_snapshot(cx)) + .head(); editor .snapshot(window, cx) diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 80e27ee6bdeb1d1a2627ad7aa46bf68c38464510..2f470560d5a58a1ed9e56ebe89257572d195689e 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -231,7 +231,10 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut editor.update(cx, |editor, cx| { assert_eq!( - editor.selections.newest::(cx).head(), + editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(), Point::new(5, 2) ) }); diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index b03b686c996ea85310bd446100255896870549f1..d3947b9b5d56b3ae71c3af7c8bf829676041123b 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -170,7 +170,10 @@ impl DiagnosticIndicator { fn update(&mut self, editor: Entity, window: &mut Window, cx: &mut Context) { let (buffer, cursor_position) = editor.update(cx, |editor, cx| { let buffer = editor.buffer().read(cx).snapshot(cx); - let cursor_position = editor.selections.newest::(cx).head(); + let cursor_position = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); (buffer, cursor_position) }); let new_diagnostic = buffer diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5d881f221b238c35224f804c1b95094a2db957e8..66e5b4df9d035e85114d177ba0456fba9d2f3d10 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2323,21 +2323,22 @@ impl Editor { } EditorEvent::Edited { .. } => { if !vim_enabled(cx) { - let (map, selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_adjusted_display(&display_map); let pop_state = editor .change_list .last() .map(|previous| { previous.len() == selections.len() && previous.iter().enumerate().all(|(ix, p)| { - p.to_display_point(&map).row() + p.to_display_point(&display_map).row() == selections[ix].head().row() }) }) .unwrap_or(false); let new_positions = selections .into_iter() - .map(|s| map.display_point_to_anchor(s.head(), Bias::Left)) + .map(|s| display_map.display_point_to_anchor(s.head(), Bias::Left)) .collect(); editor .change_list @@ -2408,6 +2409,10 @@ impl Editor { editor } + pub fn display_snapshot(&self, cx: &mut App) -> DisplaySnapshot { + self.selections.display_map(cx) + } + pub fn deploy_mouse_context_menu( &mut self, position: gpui::Point, @@ -2443,7 +2448,7 @@ impl Editor { } self.selections - .disjoint_in_range::(range.clone(), cx) + .disjoint_in_range::(range.clone(), &self.display_snapshot(cx)) .into_iter() .any(|selection| { // This is needed to cover a corner case, if we just check for an existing @@ -3039,7 +3044,7 @@ impl Editor { // Copy selections to primary selection buffer #[cfg(any(target_os = "linux", target_os = "freebsd"))] if local { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); let buffer_handle = self.buffer.read(cx).read(cx); let mut text = String::new(); @@ -3491,7 +3496,7 @@ impl Editor { cx: &mut Context, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self.selections.newest::(cx).tail(); + let tail = self.selections.newest::(&display_map).tail(); let click_count = click_count.max(match self.selections.select_mode() { SelectMode::Character => 1, SelectMode::Word(_) => 2, @@ -3610,7 +3615,7 @@ impl Editor { let point_to_delete: Option = { let selected_points: Vec> = - self.selections.disjoint_in_range(start..end, cx); + self.selections.disjoint_in_range(start..end, &display_map); if !add || click_count > 1 { None @@ -3686,7 +3691,7 @@ impl Editor { ); }; - let tail = self.selections.newest::(cx).tail(); + let tail = self.selections.newest::(&display_map).tail(); let selection_anchor = display_map.buffer_snapshot().anchor_before(tail); self.columnar_selection_state = match mode { ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse { @@ -3813,7 +3818,7 @@ impl Editor { fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { self.columnar_selection_state.take(); if let Some(pending_mode) = self.selections.pending_mode() { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select(selections); s.clear_pending(); @@ -3902,9 +3907,9 @@ impl Editor { cx.notify(); } - pub fn has_non_empty_selection(&self, cx: &mut App) -> bool { + pub fn has_non_empty_selection(&self, snapshot: &DisplaySnapshot) -> bool { self.selections - .all_adjusted(cx) + .all_adjusted(snapshot) .iter() .any(|selection| !selection.is_empty()) } @@ -4053,7 +4058,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let selections = self.selections.all_adjusted(cx); + let selections = self.selections.all_adjusted(&self.display_snapshot(cx)); let mut bracket_inserted = false; let mut edits = Vec::new(); let mut linked_edits = HashMap::<_, Vec<_>>::default(); @@ -4403,7 +4408,7 @@ impl Editor { let trigger_in_words = this.show_edit_predictions_in_menu() || !had_active_edit_prediction; if this.hard_wrap.is_some() { - let latest: Range = this.selections.newest(cx).range(); + let latest: Range = this.selections.newest(&map).range(); if latest.is_empty() && this .buffer() @@ -4479,7 +4484,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { let (edits_with_flags, selection_info): (Vec<_>, Vec<_>) = { - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); let multi_buffer = this.buffer.read(cx); let buffer = multi_buffer.snapshot(cx); selections @@ -4771,7 +4776,12 @@ impl Editor { let mut edits = Vec::new(); let mut rows = Vec::new(); - for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() { + for (rows_inserted, selection) in self + .selections + .all_adjusted(&self.display_snapshot(cx)) + .into_iter() + .enumerate() + { let cursor = selection.head(); let row = cursor.row; @@ -4831,7 +4841,7 @@ impl Editor { let mut rows = Vec::new(); let mut rows_inserted = 0; - for selection in self.selections.all_adjusted(cx) { + for selection in self.selections.all_adjusted(&self.display_snapshot(cx)) { let cursor = selection.head(); let row = cursor.row; @@ -4903,7 +4913,7 @@ impl Editor { let text: Arc = text.into(); self.transact(window, cx, |this, window, cx| { - let old_selections = this.selections.all_adjusted(cx); + let old_selections = this.selections.all_adjusted(&this.display_snapshot(cx)); let selection_anchors = this.buffer.update(cx, |buffer, cx| { let anchors = { let snapshot = buffer.read(cx); @@ -5013,7 +5023,7 @@ impl Editor { /// If any empty selections is touching the start of its innermost containing autoclose /// region, expand it to select the brackets. fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx).read(cx); let new_selections = self .selections_with_autoclose_regions(selections, &buffer) @@ -6010,7 +6020,7 @@ impl Editor { let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; let suffix = &old_text[lookbehind.min(old_text.len())..]; - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); let mut ranges = Vec::new(); let mut linked_edits = HashMap::<_, Vec<_>>::default(); @@ -6162,7 +6172,10 @@ impl Editor { Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { DisplayPoint::new(*row, 0).to_point(&snapshot) } - _ => self.selections.newest::(cx).head(), + _ => self + .selections + .newest::(&snapshot.display_snapshot) + .head(), }; let Some((buffer, buffer_row)) = snapshot .buffer_snapshot() @@ -6624,7 +6637,9 @@ impl Editor { if newest_selection.head().diff_base_anchor.is_some() { return None; } - let newest_selection_adjusted = this.selections.newest_adjusted(cx); + let display_snapshot = this.display_snapshot(cx); + let newest_selection_adjusted = + this.selections.newest_adjusted(&display_snapshot); let buffer = this.buffer.read(cx); let (start_buffer, start) = @@ -6699,7 +6714,10 @@ impl Editor { pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context) { let snapshot = self.snapshot(window, cx); - let cursor = self.selections.newest::(cx).head(); + let cursor = self + .selections + .newest::(&snapshot.display_snapshot) + .head(); let Some((buffer, point, _)) = snapshot.buffer_snapshot().point_to_buffer_point(cursor) else { return; @@ -7572,7 +7590,10 @@ impl Editor { // Find an insertion that starts at the cursor position. let snapshot = self.buffer.read(cx).snapshot(cx); - let cursor_offset = self.selections.newest::(cx).head(); + let cursor_offset = self + .selections + .newest::(&self.display_snapshot(cx)) + .head(); let insertion = edits.iter().find_map(|(range, text)| { let range = range.to_offset(&snapshot); if range.is_empty() && range.start == cursor_offset { @@ -8528,7 +8549,11 @@ impl Editor { &mut self, cx: &mut Context, ) -> Option<(Entity, u32, Arc)> { - let cursor_row = self.selections.newest_adjusted(cx).head().row; + let cursor_row = self + .selections + .newest_adjusted(&self.display_snapshot(cx)) + .head() + .row; let ((buffer_id, row), tasks) = self .tasks @@ -8545,7 +8570,10 @@ impl Editor { cx: &mut Context, ) -> Option<(Entity, u32, Arc)> { let snapshot = self.buffer.read(cx).snapshot(cx); - let offset = self.selections.newest::(cx).head(); + let offset = self + .selections + .newest::(&self.display_snapshot(cx)) + .head(); let excerpt = snapshot.excerpt_containing(offset..offset)?; let buffer_id = excerpt.buffer().remote_id(); @@ -9862,8 +9890,7 @@ impl Editor { // Check whether the just-entered snippet ends with an auto-closable bracket. if self.autoclose_regions.is_empty() { let snapshot = self.buffer.read(cx).snapshot(cx); - let mut all_selections = self.selections.all::(cx); - for selection in &mut all_selections { + for selection in &mut self.selections.all::(&self.display_snapshot(cx)) { let selection_head = selection.head(); let Some(scope) = snapshot.language_scope_at(selection_head) else { continue; @@ -10001,9 +10028,12 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); + + let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut linked_ranges = HashMap::<_, Vec<_>>::default(); if !this.linked_edit_ranges.is_empty() { - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&display_map); let snapshot = this.buffer.read(cx).snapshot(cx); for selection in selections.iter() { @@ -10022,8 +10052,7 @@ impl Editor { } } - let mut selections = this.selections.all::(cx); - let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); + let mut selections = this.selections.all::(&display_map); for selection in &mut selections { if selection.is_empty() { let old_head = selection.head(); @@ -10138,7 +10167,7 @@ impl Editor { return; } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let mut selections = self.selections.all_adjusted(cx); + let mut selections = self.selections.all_adjusted(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); let rows_iter = selections.iter().map(|s| s.head().row); @@ -10254,7 +10283,7 @@ impl Editor { } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); - let mut selections = self.selections.all::(cx); + let mut selections = self.selections.all::(&self.display_snapshot(cx)); let mut prev_edited_row = 0; let mut row_delta = 0; let mut edits = Vec::new(); @@ -10363,7 +10392,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut deletion_ranges = Vec::new(); let mut last_outdent = None; { @@ -10424,7 +10453,7 @@ impl Editor { cx, ); }); - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); }); } @@ -10441,7 +10470,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections - .all::(cx) + .all::(&self.display_snapshot(cx)) .into_iter() .map(|s| s.range()); @@ -10449,7 +10478,7 @@ impl Editor { this.buffer.update(cx, |buffer, cx| { buffer.autoindent_ranges(selections, cx); }); - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); }); } @@ -10457,7 +10486,7 @@ impl Editor { pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut new_cursors = Vec::new(); let mut edit_ranges = Vec::new(); @@ -10551,7 +10580,7 @@ impl Editor { return; } let mut row_ranges = Vec::>::new(); - for selection in self.selections.all::(cx) { + for selection in self.selections.all::(&self.display_snapshot(cx)) { let start = MultiBufferRow(selection.start.row); // Treat single line selections as if they include the next line. Otherwise this action // would do nothing for single line selections individual cursors. @@ -10694,7 +10723,11 @@ impl Editor { let mut edits = Vec::new(); let mut boundaries = Vec::new(); - for selection in self.selections.all::(cx).iter() { + for selection in self + .selections + .all::(&self.display_snapshot(cx)) + .iter() + { let Some(wrap_config) = snapshot .language_at(selection.start) .and_then(|lang| lang.config().wrap_characters.clone()) @@ -10764,7 +10797,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let mut buffer_ids = HashSet::default(); let snapshot = self.buffer().read(cx).snapshot(cx); - for selection in self.selections.all::(cx) { + for selection in self.selections.all::(&self.display_snapshot(cx)) { buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) } @@ -10781,7 +10814,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let selections = self .selections - .all(cx) + .all(&self.display_snapshot(cx)) .into_iter() .map(|s| s.range()) .collect(); @@ -11189,7 +11222,7 @@ impl Editor { let mut edits = Vec::new(); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut selections = selections.iter().peekable(); let mut contiguous_row_selections = Vec::new(); let mut new_selections = Vec::new(); @@ -11591,7 +11624,7 @@ impl Editor { let mut edits = Vec::new(); let mut selection_adjustment = 0i32; - for selection in self.selections.all_adjusted(cx) { + for selection in self.selections.all_adjusted(&self.display_snapshot(cx)) { let selection_is_empty = selection.is_empty(); let (start, end) = if selection_is_empty { @@ -11683,7 +11716,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = display_map.buffer_snapshot(); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut edits = Vec::new(); let mut selections_iter = selections.iter().peekable(); @@ -11791,7 +11824,7 @@ impl Editor { let mut unfold_ranges = Vec::new(); let mut refold_creases = Vec::new(); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut selections = selections.iter().peekable(); let mut contiguous_row_selections = Vec::new(); let mut new_selections = Vec::new(); @@ -11902,7 +11935,7 @@ impl Editor { let mut unfold_ranges = Vec::new(); let mut refold_creases = Vec::new(); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut selections = selections.iter().peekable(); let mut contiguous_row_selections = Vec::new(); let mut new_selections = Vec::new(); @@ -12035,7 +12068,7 @@ impl Editor { }); this.buffer .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); this.change_selections(Default::default(), window, cx, |s| { s.select(selections); }); @@ -12054,7 +12087,7 @@ impl Editor { pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context) { let buffer = self.buffer.read(cx).snapshot(cx); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); #[derive(Clone, Debug, PartialEq)] enum CommentFormat { @@ -12430,7 +12463,7 @@ impl Editor { ) -> ClipboardItem { let mut text = String::new(); let buffer = self.buffer.read(cx).snapshot(cx); - let mut selections = self.selections.all::(cx); + let mut selections = self.selections.all::(&self.display_snapshot(cx)); let mut clipboard_selections = Vec::with_capacity(selections.len()); { let max_point = buffer.max_point(); @@ -12526,7 +12559,7 @@ impl Editor { } fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context) { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx).read(cx); let mut text = String::new(); @@ -12637,8 +12670,9 @@ impl Editor { self.transact(window, cx, |this, window, cx| { let had_active_edit_prediction = this.has_active_edit_prediction(); - let old_selections = this.selections.all::(cx); - let cursor_offset = this.selections.last::(cx).head(); + let display_map = this.display_snapshot(cx); + let old_selections = this.selections.all::(&display_map); + let cursor_offset = this.selections.last::(&display_map).head(); if let Some(mut clipboard_selections) = clipboard_selections { let all_selections_were_entire_line = @@ -12719,7 +12753,7 @@ impl Editor { ); }); - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); } else { let url = url::Url::parse(&clipboard_text).ok(); @@ -12784,7 +12818,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); if selections.is_empty() { log::warn!("There should always be at least one selection in Zed. This is a bug."); @@ -14047,7 +14081,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let mut selection = self.selections.last::(cx); + let mut selection = self.selections.last::(&self.display_snapshot(cx)); selection.set_head(Point::zero(), SelectionGoal::None); self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); self.change_selections(Default::default(), window, cx, |s| { @@ -14126,7 +14160,7 @@ impl Editor { pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); - let mut selection = self.selections.first::(cx); + let mut selection = self.selections.first::(&self.display_snapshot(cx)); selection.set_head(buffer.len(), SelectionGoal::None); self.change_selections(Default::default(), window, cx, |s| { s.select(vec![selection]); @@ -14144,7 +14178,7 @@ impl Editor { pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.selections.all::(cx); + let mut selections = self.selections.all::(&display_map); let max_point = display_map.buffer_snapshot().max_point(); for selection in &mut selections { let rows = selection.spanned_rows(true, &display_map); @@ -14165,7 +14199,7 @@ impl Editor { ) { let selections = self .selections - .all::(cx) + .all::(&self.display_snapshot(cx)) .into_iter() .map(|selection| selection.start..selection.end) .collect::>(); @@ -14244,7 +14278,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let all_selections = self.selections.all::(cx); + let all_selections = self.selections.all::(&display_map); let text_layout_details = self.text_layout_details(window); let (mut columnar_selections, new_selections_to_columnarize) = { @@ -14378,7 +14412,7 @@ impl Editor { let final_selection_ids: HashSet<_> = self .selections - .all::(cx) + .all::(&display_map) .iter() .map(|s| s.id) .collect(); @@ -14436,7 +14470,7 @@ impl Editor { cx: &mut Context, ) -> Result<()> { let buffer = display_map.buffer_snapshot(); - let mut selections = self.selections.all::(cx); + let mut selections = self.selections.all::(&display_map); if let Some(mut select_next_state) = self.select_next_state.take() { let query = &select_next_state.query; if !select_next_state.done { @@ -14610,7 +14644,7 @@ impl Editor { let mut new_selections = Vec::new(); - let reversed = self.selections.oldest::(cx).reversed; + let reversed = self.selections.oldest::(&display_map).reversed; let buffer = display_map.buffer_snapshot(); let query_matches = select_next_state .query @@ -14674,7 +14708,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = display_map.buffer_snapshot(); - let mut selections = self.selections.all::(cx); + let mut selections = self.selections.all::(&display_map); if let Some(mut select_prev_state) = self.select_prev_state.take() { let query = &select_prev_state.query; if !select_prev_state.done { @@ -14862,7 +14896,9 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); let text_layout_details = &self.text_layout_details(window); self.transact(window, cx, |this, window, cx| { - let mut selections = this.selections.all::(cx); + let mut selections = this + .selections + .all::(&this.display_snapshot(cx)); let mut edits = Vec::new(); let mut selection_edit_ranges = Vec::new(); let mut last_toggled_row = None; @@ -15093,7 +15129,7 @@ impl Editor { // Adjust selections so that they end before any comment suffixes that // were inserted. let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); - let mut selections = this.selections.all::(cx); + let mut selections = this.selections.all::(&this.display_snapshot(cx)); let snapshot = this.buffer.read(cx).read(cx); for selection in &mut selections { while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { @@ -15119,7 +15155,7 @@ impl Editor { drop(snapshot); this.change_selections(Default::default(), window, cx, |s| s.select(selections)); - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); let selections_on_single_row = selections.windows(2).all(|selections| { selections[0].start.row == selections[1].start.row && selections[0].end.row == selections[1].end.row @@ -15163,7 +15199,10 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx).into_boxed_slice(); + let old_selections = self + .selections + .all::(&self.display_snapshot(cx)) + .into_boxed_slice(); fn update_selection( selection: &Selection, @@ -15218,7 +15257,10 @@ impl Editor { let Some(visible_row_count) = self.visible_row_count() else { return; }; - let old_selections: Box<[_]> = self.selections.all::(cx).into(); + let old_selections: Box<[_]> = self + .selections + .all::(&self.display_snapshot(cx)) + .into(); if old_selections.is_empty() { return; } @@ -15376,7 +15418,7 @@ impl Editor { let buffer = self.buffer.read(cx).snapshot(cx); let selections = self .selections - .all::(cx) + .all::(&self.display_snapshot(cx)) .into_iter() // subtracting the offset requires sorting .sorted_by_key(|i| i.start); @@ -15448,7 +15490,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let old_selections: Box<[_]> = self.selections.all::(cx).into(); + let old_selections: Box<[_]> = self + .selections + .all::(&self.display_snapshot(cx)) + .into(); if old_selections.is_empty() { return; } @@ -15497,7 +15542,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let old_selections: Box<[_]> = self.selections.all::(cx).into(); + let old_selections: Box<[_]> = self + .selections + .all::(&self.display_snapshot(cx)) + .into(); if old_selections.is_empty() { return; } @@ -16046,7 +16094,7 @@ impl Editor { cx: &mut Context, ) { let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&self.display_snapshot(cx)); let mut active_group_id = None; if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics @@ -16127,7 +16175,7 @@ impl Editor { pub fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&self.display_snapshot(cx)); self.go_to_hunk_before_or_after_position( &snapshot, selection.head(), @@ -16188,7 +16236,7 @@ impl Editor { ) { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&snapshot.display_snapshot); self.go_to_hunk_before_or_after_position( &snapshot, selection.head(), @@ -16278,7 +16326,10 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let snapshot = self.snapshot(window, cx); let buffer = &snapshot.buffer_snapshot(); - let position = self.selections.newest::(cx).head(); + let position = self + .selections + .newest::(&snapshot.display_snapshot) + .head(); let anchor_position = buffer.anchor_after(position); // Get all document highlights (both read and write) @@ -16461,7 +16512,10 @@ impl Editor { let Some(provider) = self.semantics_provider.clone() else { return Task::ready(Ok(Navigated::No)); }; - let head = self.selections.newest::(cx).head(); + let head = self + .selections + .newest::(&self.display_snapshot(cx)) + .head(); let buffer = self.buffer.read(cx); let Some((buffer, head)) = buffer.text_anchor_for_position(head, cx) else { return Task::ready(Ok(Navigated::No)); @@ -16792,7 +16846,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&self.display_snapshot(cx)); let multi_buffer = self.buffer.read(cx); let head = selection.head(); @@ -17267,7 +17321,10 @@ impl Editor { if moving_cursor { let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| { - editor.selections.newest::(cx).head() + editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head() }); // Update the selection to match the position of the selection inside @@ -17330,7 +17387,7 @@ impl Editor { let ranges = self .selections - .all_adjusted(cx) + .all_adjusted(&self.display_snapshot(cx)) .into_iter() .map(|selection| selection.range()) .collect_vec(); @@ -18029,9 +18086,9 @@ impl Editor { cx: &mut Context, ) { if self.buffer_kind(cx) == ItemBufferKind::Singleton { - let selection = self.selections.newest::(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selection = self.selections.newest::(&display_map); + let range = if selection.is_empty() { let point = selection.head().to_display_point(&display_map); let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); @@ -18074,7 +18131,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&self.display_snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let range = if selection.is_empty() { @@ -18097,7 +18154,7 @@ impl Editor { if self.buffer_kind(cx) == ItemBufferKind::Singleton { let mut to_fold = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all_adjusted(cx); + let selections = self.selections.all_adjusted(&display_map); for selection in selections { let range = selection.range().sorted(); @@ -18204,7 +18261,7 @@ impl Editor { let row_ranges_to_keep: Vec> = self .selections - .all::(cx) + .all::(&self.display_snapshot(cx)) .into_iter() .map(|sel| sel.start.row..sel.end.row) .collect(); @@ -18379,7 +18436,7 @@ impl Editor { ) { let mut to_fold = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all_adjusted(cx); + let selections = self.selections.all_adjusted(&display_map); for selection in selections { let range = selection.range().sorted(); @@ -18423,7 +18480,7 @@ impl Editor { if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) { let autoscroll = self .selections - .all::(cx) + .all::(&display_map) .iter() .any(|selection| crease.range().overlaps(&selection.range())); @@ -18435,7 +18492,7 @@ impl Editor { if self.buffer_kind(cx) == ItemBufferKind::Singleton { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = display_map.buffer_snapshot(); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let ranges = selections .iter() .map(|s| { @@ -18469,7 +18526,7 @@ impl Editor { cx: &mut Context, ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let ranges = selections .iter() .map(|s| { @@ -18501,7 +18558,7 @@ impl Editor { let autoscroll = self .selections - .all::(cx) + .all::(&display_map) .iter() .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); @@ -18536,8 +18593,8 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let selections = self.selections.all_adjusted(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self.selections.all_adjusted(&display_map); let ranges = selections .into_iter() .map(|s| Crease::simple(s.range(), display_map.fold_placeholder.clone())) @@ -18870,7 +18927,10 @@ impl Editor { self.stage_or_unstage_diff_hunks(stage, ranges, cx); let snapshot = self.snapshot(window, cx); - let position = self.selections.newest::(cx).head(); + let position = self + .selections + .newest::(&snapshot.display_snapshot) + .head(); let mut row = snapshot .buffer_snapshot() .diff_hunks_in_range(position..snapshot.buffer_snapshot().max_point()) @@ -19018,7 +19078,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let hunks = snapshot.hunks_for_ranges( self.selections - .all(cx) + .all(&snapshot.display_snapshot) .into_iter() .map(|selection| selection.range()), ); @@ -19754,7 +19814,10 @@ impl Editor { ) -> Option<()> { let blame = self.blame.as_ref()?; let snapshot = self.snapshot(window, cx); - let cursor = self.selections.newest::(cx).head(); + let cursor = self + .selections + .newest::(&snapshot.display_snapshot) + .head(); let (buffer, point, _) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?; let (_, blame_entry) = blame .update(cx, |blame, cx| { @@ -19896,7 +19959,7 @@ impl Editor { fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { let buffer_and_selection = maybe!({ - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&self.display_snapshot(cx)); let selection_range = selection.range(); let multi_buffer = self.buffer().read(cx); @@ -19974,7 +20037,12 @@ impl Editor { _: &mut Window, cx: &mut Context, ) { - let selection = self.selections.newest::(cx).start.row + 1; + let selection = self + .selections + .newest::(&self.display_snapshot(cx)) + .start + .row + + 1; if let Some(file) = self.target_file(cx) { let path = file.path().display(file.path_style(cx)); cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); @@ -20045,7 +20113,7 @@ impl Editor { self.transact(window, cx, |this, window, cx| { let edits = this .selections - .all::(cx) + .all::(&this.display_snapshot(cx)) .into_iter() .map(|selection| { let uuid = match version { @@ -21131,7 +21199,7 @@ impl Editor { return; }; - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); let multi_buffer = self.buffer.read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let mut new_selections_by_buffer = HashMap::default(); @@ -21255,7 +21323,7 @@ impl Editor { } } None => { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&self.display_snapshot(cx)); let multi_buffer = self.buffer.read(cx); for selection in selections { for (snapshot, range, _, anchor) in multi_buffer @@ -21393,7 +21461,9 @@ impl Editor { range: Range, cx: &mut App, ) -> Vec> { - let selections = self.selections.all::(cx); + let selections = self + .selections + .all::(&self.display_snapshot(cx)); let newest_selection = selections .iter() .max_by_key(|selection| selection.id) @@ -21556,7 +21626,10 @@ impl Editor { cx: &mut Context, ) { self.request_autoscroll(Autoscroll::newest(), cx); - let position = self.selections.newest_display(cx).start; + let position = self + .selections + .newest_display(&self.display_snapshot(cx)) + .start; mouse_context_menu::deploy_context_menu(self, None, position, window, cx); } @@ -21576,7 +21649,9 @@ impl Editor { return; } if let Some(relative_utf16_range) = relative_utf16_range { - let selections = self.selections.all::(cx); + let selections = self + .selections + .all::(&self.display_snapshot(cx)); self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { let new_ranges = selections.into_iter().map(|range| { let start = OffsetUtf16( @@ -21716,7 +21791,7 @@ impl Editor { } let transaction = self.transact(window, cx, |this, window, cx| { - let selections = this.selections.all::(cx); + let selections = this.selections.all::(&this.display_snapshot(cx)); let edits = selections .iter() .map(|selection| (selection.end..selection.end, pending.clone())); @@ -21735,7 +21810,7 @@ impl Editor { let snapshot = self.snapshot(window, cx); let ranges = self .selections - .all::(cx) + .all::(&snapshot.display_snapshot) .into_iter() .map(|selection| { snapshot.buffer_snapshot().anchor_after(selection.end) @@ -23867,7 +23942,9 @@ impl EntityInputHandler for Editor { return None; } - let selection = self.selections.newest::(cx); + let selection = self + .selections + .newest::(&self.display_snapshot(cx)); let range = selection.range(); Some(UTF16Selection { @@ -23910,7 +23987,7 @@ impl EntityInputHandler for Editor { let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| { let newest_selection_id = this.selections.newest_anchor().id; this.selections - .all::(cx) + .all::(&this.display_snapshot(cx)) .iter() .zip(ranges_to_replace.iter()) .find_map(|(selection, range)| { @@ -23985,7 +24062,7 @@ impl EntityInputHandler for Editor { let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| { let newest_selection_id = this.selections.newest_anchor().id; this.selections - .all::(cx) + .all::(&this.display_snapshot(cx)) .iter() .zip(ranges_to_replace.iter()) .find_map(|(selection, range)| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2c8dc9e3548fa5edd2cc3020f1a314e961bd71a3..239dc2969196d400a84824eabc0ca7e300851a35 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -219,7 +219,10 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { editor.insert("cd", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cd56"); - assert_eq!(editor.selections.ranges(cx), vec![4..4]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![4..4] + ); editor.start_transaction_at(now, window, cx); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -228,7 +231,10 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { editor.insert("e", window, cx); editor.end_transaction_at(now, cx); assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![5..5]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![5..5] + ); now += group_interval + Duration::from_millis(1); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { @@ -244,30 +250,45 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { }); assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selections.ranges(cx), vec![3..3]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![3..3] + ); // Last transaction happened past the group interval in a different editor. // Undo it individually and don't restore selections. editor.undo(&Undo, window, cx); assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![2..2]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![2..2] + ); // First two transactions happened within the group interval in this editor. // Undo them together and restore selections. editor.undo(&Undo, window, cx); editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op. assert_eq!(editor.text(cx), "123456"); - assert_eq!(editor.selections.ranges(cx), vec![0..0]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![0..0] + ); // Redo the first two transactions together. editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "12cde6"); - assert_eq!(editor.selections.ranges(cx), vec![5..5]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![5..5] + ); // Redo the last transaction on its own. editor.redo(&Redo, window, cx); assert_eq!(editor.text(cx), "ab2cde6"); - assert_eq!(editor.selections.ranges(cx), vec![6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + vec![6..6] + ); // Test empty transactions. editor.start_transaction_at(now, window, cx); @@ -770,10 +791,14 @@ fn test_clone(cx: &mut TestAppContext) { ); assert_set_eq!( cloned_editor - .update(cx, |editor, _, cx| editor.selections.ranges::(cx)) + .update(cx, |editor, _, cx| editor + .selections + .ranges::(&editor.display_snapshot(cx))) .unwrap(), editor - .update(cx, |editor, _, cx| editor.selections.ranges(cx)) + .update(cx, |editor, _, cx| editor + .selections + .ranges(&editor.display_snapshot(cx))) .unwrap() ); assert_set_eq!( @@ -3161,7 +3186,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { ); }); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), &[ Point::new(1, 2)..Point::new(1, 2), Point::new(2, 2)..Point::new(2, 2), @@ -3183,7 +3208,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { // The selections are moved after the inserted newlines assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), &[ Point::new(2, 0)..Point::new(2, 0), Point::new(4, 0)..Point::new(4, 0), @@ -3673,13 +3698,19 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx); assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent()); }); - assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + &[2..2, 7..7, 12..12], + ); editor.insert("Z", window, cx); assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)"); // The selections are moved after the inserted characters - assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + &[3..3, 9..9, 15..15], + ); }); } @@ -4439,7 +4470,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { let buffer = buffer.read(cx).as_singleton().unwrap(); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), &[Point::new(0, 0)..Point::new(0, 0)] ); @@ -4447,7 +4480,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), &[Point::new(0, 3)..Point::new(0, 3)] ); @@ -4458,7 +4493,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), &[Point::new(0, 11)..Point::new(0, 11)] ); @@ -4466,7 +4503,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { editor.undo(&Undo, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), &[Point::new(0, 5)..Point::new(2, 2)] ); @@ -4477,7 +4516,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [Point::new(2, 3)..Point::new(2, 3)] ); @@ -4485,7 +4526,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [Point::new(2, 3)..Point::new(2, 3)] ); @@ -4493,7 +4536,9 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { editor.join_lines(&JoinLines, window, cx); assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [Point::new(2, 3)..Point::new(2, 3)] ); @@ -4550,7 +4595,9 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [ Point::new(0, 7)..Point::new(0, 7), Point::new(1, 3)..Point::new(1, 3) @@ -5908,15 +5955,24 @@ fn test_transpose(cx: &mut TestAppContext) { }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bac"); - assert_eq!(editor.selections.ranges(cx), [2..2]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [2..2] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bca"); - assert_eq!(editor.selections.ranges(cx), [3..3]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [3..3] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bac"); - assert_eq!(editor.selections.ranges(cx), [3..3]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [3..3] + ); editor }); @@ -5929,22 +5985,34 @@ fn test_transpose(cx: &mut TestAppContext) { }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acb\nde"); - assert_eq!(editor.selections.ranges(cx), [3..3]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [3..3] + ); editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([4..4]) }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [5..5]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [5..5] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbde\n"); - assert_eq!(editor.selections.ranges(cx), [6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [6..6] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [6..6] + ); editor }); @@ -5957,23 +6025,38 @@ fn test_transpose(cx: &mut TestAppContext) { }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bacd\ne"); - assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [2..2, 3..3, 5..5] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [3..3, 4..4, 6..6] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcda\ne"); - assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [4..4, 6..6] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [4..4, 6..6] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "bcaed\n"); - assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [5..5, 6..6] + ); editor }); @@ -5986,15 +6069,24 @@ fn test_transpose(cx: &mut TestAppContext) { }); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [8..8]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [8..8] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀✋🍐"); - assert_eq!(editor.selections.ranges(cx), [11..11]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [11..11] + ); editor.transpose(&Default::default(), window, cx); assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [11..11]); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + [11..11] + ); editor }); @@ -9540,7 +9632,7 @@ async fn test_autoindent(cx: &mut TestAppContext) { editor.newline(&Newline, window, cx); assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), &[ Point::new(1, 4)..Point::new(1, 4), Point::new(3, 4)..Point::new(3, 4), @@ -9616,7 +9708,7 @@ async fn test_autoindent_disabled(cx: &mut TestAppContext) { ) ); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), &[ Point::new(1, 0)..Point::new(1, 0), Point::new(3, 0)..Point::new(3, 0), @@ -10255,7 +10347,9 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) { // Precondition: different languages are active at different locations. cx.update_editor(|editor, window, cx| { let snapshot = editor.snapshot(window, cx); - let cursors = editor.selections.ranges::(cx); + let cursors = editor + .selections + .ranges::(&editor.display_snapshot(cx)); let languages = cursors .iter() .map(|c| snapshot.language_at(c.start).unwrap().name()) @@ -10700,7 +10794,9 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) { .unindent() ); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [ Point::new(0, 4)..Point::new(0, 4), Point::new(1, 4)..Point::new(1, 4), @@ -10720,7 +10816,9 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) { .unindent() ); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [ Point::new(0, 2)..Point::new(0, 2), Point::new(1, 2)..Point::new(1, 2), @@ -10739,7 +10837,9 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) { .unindent() ); assert_eq!( - editor.selections.ranges::(cx), + editor + .selections + .ranges::(&editor.display_snapshot(cx)), [ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), @@ -10945,7 +11045,12 @@ async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) { fn assert(editor: &mut Editor, cx: &mut Context, marked_text: &str) { let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false); assert_eq!(editor.text(cx), expected_text); - assert_eq!(editor.selections.ranges::(cx), selection_ranges); + assert_eq!( + editor + .selections + .ranges::(&editor.display_snapshot(cx)), + selection_ranges + ); } assert( @@ -10976,7 +11081,7 @@ async fn test_snippets(cx: &mut TestAppContext) { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); let insertion_ranges = editor .selections - .all(cx) + .all(&editor.display_snapshot(cx)) .iter() .map(|s| s.range()) .collect::>(); @@ -11056,7 +11161,7 @@ async fn test_snippet_indentation(cx: &mut TestAppContext) { .unwrap(); let insertion_ranges = editor .selections - .all(cx) + .all(&editor.display_snapshot(cx)) .iter() .map(|s| s.range()) .collect::>(); @@ -15945,7 +16050,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { editor.handle_input("X", window, cx); assert_eq!(editor.text(cx), "Xaaaa\nXbbbb"); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), @@ -15959,7 +16064,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { editor.backspace(&Default::default(), window, cx); assert_eq!(editor.text(cx), "Xa\nbbb"); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [Point::new(1, 0)..Point::new(1, 0)] ); @@ -15969,7 +16074,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { editor.backspace(&Default::default(), window, cx); assert_eq!(editor.text(cx), "X\nbb"); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [Point::new(0, 1)..Point::new(0, 1)] ); }); @@ -16027,7 +16132,10 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { false, ); assert_eq!(editor.text(cx), expected_text); - assert_eq!(editor.selections.ranges(cx), expected_selections); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + expected_selections + ); editor.newline(&Newline, window, cx); let (expected_text, expected_selections) = marked_text_ranges( @@ -16044,7 +16152,10 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { false, ); assert_eq!(editor.text(cx), expected_text); - assert_eq!(editor.selections.ranges(cx), expected_selections); + assert_eq!( + editor.selections.ranges(&editor.display_snapshot(cx)), + expected_selections + ); }); } @@ -16085,7 +16196,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { cx, ); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [ Point::new(1, 3)..Point::new(1, 3), Point::new(2, 1)..Point::new(2, 1), @@ -16098,7 +16209,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { _ = editor.update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [ Point::new(1, 3)..Point::new(1, 3), Point::new(2, 1)..Point::new(2, 1), @@ -16112,7 +16223,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { _ = editor.update(cx, |editor, window, cx| { // Removing an excerpt causes the first selection to become degenerate. assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [ Point::new(0, 0)..Point::new(0, 0), Point::new(0, 1)..Point::new(0, 1) @@ -16123,7 +16234,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { // location. editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [ Point::new(0, 1)..Point::new(0, 1), Point::new(0, 3)..Point::new(0, 3) @@ -16167,7 +16278,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { cx, ); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [Point::new(1, 3)..Point::new(1, 3)] ); editor @@ -16178,14 +16289,14 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { }); _ = editor.update(cx, |editor, window, cx| { assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [Point::new(0, 0)..Point::new(0, 0)] ); // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), [Point::new(0, 3)..Point::new(0, 3)] ); assert!(editor.selections.pending_anchor().is_some()); @@ -16435,7 +16546,10 @@ async fn test_following(cx: &mut TestAppContext) { .await .unwrap(); _ = follower.update(cx, |follower, _, cx| { - assert_eq!(follower.selections.ranges(cx), vec![1..1]); + assert_eq!( + follower.selections.ranges(&follower.display_snapshot(cx)), + vec![1..1] + ); }); assert!(*is_still_following.borrow()); assert_eq!(*follower_edit_event_count.borrow(), 0); @@ -16488,7 +16602,10 @@ async fn test_following(cx: &mut TestAppContext) { .unwrap(); _ = follower.update(cx, |follower, _, cx| { assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0)); - assert_eq!(follower.selections.ranges(cx), vec![0..0]); + assert_eq!( + follower.selections.ranges(&follower.display_snapshot(cx)), + vec![0..0] + ); }); assert!(*is_still_following.borrow()); @@ -16512,7 +16629,10 @@ async fn test_following(cx: &mut TestAppContext) { .await .unwrap(); _ = follower.update(cx, |follower, _, cx| { - assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]); + assert_eq!( + follower.selections.ranges(&follower.display_snapshot(cx)), + vec![0..0, 1..1] + ); }); assert!(*is_still_following.borrow()); @@ -16533,7 +16653,10 @@ async fn test_following(cx: &mut TestAppContext) { .await .unwrap(); _ = follower.update(cx, |follower, _, cx| { - assert_eq!(follower.selections.ranges(cx), vec![0..2]); + assert_eq!( + follower.selections.ranges(&follower.display_snapshot(cx)), + vec![0..2] + ); }); // Scrolling locally breaks the follow @@ -22668,11 +22791,11 @@ fn add_log_breakpoint_at_cursor( .first() .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone()))) .unwrap_or_else(|| { - let cursor_position: Point = editor.selections.newest(cx).head(); + let snapshot = editor.snapshot(window, cx); + let cursor_position: Point = + editor.selections.newest(&snapshot.display_snapshot).head(); - let breakpoint_position = editor - .snapshot(window, cx) - .display_snapshot + let breakpoint_position = snapshot .buffer_snapshot() .anchor_before(Point::new(cursor_position.row, 0)); @@ -23619,7 +23742,7 @@ println!("5"); assert_eq!( editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|s| s.range()) .collect::>(), @@ -23662,7 +23785,7 @@ println!("5"); assert_eq!( editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|s| s.range()) .collect::>(), @@ -23788,7 +23911,7 @@ println!("5"); assert_eq!( editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|s| s.range()) .collect::>(), @@ -23814,7 +23937,7 @@ println!("5"); assert_eq!( editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|s| s.range()) .collect::>(), @@ -25211,7 +25334,7 @@ fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Cont let (text, ranges) = marked_text_ranges(marked_text, true); assert_eq!(editor.text(cx), text); assert_eq!( - editor.selections.ranges(cx), + editor.selections.ranges(&editor.display_snapshot(cx)), ranges, "Assert selections are {}", marked_text diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f71110ed95d13eba4577e53cf148e8d7efbc20c1..2dcb9996c37d54ad352795b39a0b28ece2827759 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1377,7 +1377,7 @@ impl EditorElement { editor_with_selections.update(cx, |editor, cx| { if editor.show_local_selections { let mut layouts = Vec::new(); - let newest = editor.selections.newest(cx); + let newest = editor.selections.newest(&editor.display_snapshot(cx)); for selection in local_selections.iter().cloned() { let is_empty = selection.start == selection.end; let is_newest = selection == newest; @@ -3195,7 +3195,9 @@ impl EditorElement { let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| { let newest_selection_head = newest_selection_head.unwrap_or_else(|| { - let newest = editor.selections.newest::(cx); + let newest = editor + .selections + .newest::(&editor.display_snapshot(cx)); SelectionLayout::new( newest, editor.selections.line_mode(), @@ -8793,7 +8795,8 @@ impl Element for EditorElement { .editor_with_selections(cx) .map(|editor| { editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all::(cx); + let all_selections = + editor.selections.all::(&snapshot.display_snapshot); let selected_buffer_ids = if editor.buffer_kind(cx) == ItemBufferKind::Singleton { Vec::new() @@ -8815,10 +8818,12 @@ impl Element for EditorElement { selected_buffer_ids }; - let mut selections = editor - .selections - .disjoint_in_range(start_anchor..end_anchor, cx); - selections.extend(editor.selections.pending(cx)); + let mut selections = editor.selections.disjoint_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + ); + selections + .extend(editor.selections.pending(&snapshot.display_snapshot)); (selections, selected_buffer_ids) }) diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 22b57bd80579c61405cf46b5e84d1fa128a38ffb..7c392d27531472a413ce4d32d09cce4eb722e462 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -69,7 +69,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Option> { - let selection = self.selections.newest::(cx); + let selection = self.selections.newest::(&self.display_snapshot(cx)); let cursor_row = MultiBufferRow(selection.head().row); let state = &mut self.active_indent_guides_state; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index de47f66183ee972e77a54e436bf25b94cef66e36..708efbbe979dd153dbafde265e56bc2ddd725f76 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -594,7 +594,7 @@ impl Item for Editor { cx: &mut Context, ) -> bool { if let Ok(data) = data.downcast::() { - let newest_selection = self.selections.newest::(cx); + let newest_selection = self.selections.newest::(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx).read(cx); let offset = if buffer.can_resolve(&data.cursor_anchor) { data.cursor_anchor.to_point(&buffer) @@ -1539,13 +1539,13 @@ impl SearchableItem for Editor { fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String { let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor; let snapshot = self.snapshot(window, cx); - let snapshot = snapshot.buffer_snapshot(); - let selection = self.selections.newest_adjusted(cx); + let selection = self.selections.newest_adjusted(&snapshot.display_snapshot); + let buffer_snapshot = snapshot.buffer_snapshot(); match setting { SeedQuerySetting::Never => String::new(), SeedQuerySetting::Selection | SeedQuerySetting::Always if !selection.is_empty() => { - let text: String = snapshot + let text: String = buffer_snapshot .text_for_range(selection.start..selection.end) .collect(); if text.contains('\n') { @@ -1556,10 +1556,10 @@ impl SearchableItem for Editor { } SeedQuerySetting::Selection => String::new(), SeedQuerySetting::Always => { - let (range, kind) = - snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion)); + let (range, kind) = buffer_snapshot + .surrounding_word(selection.start, Some(CharScopeContext::Completion)); if kind == Some(CharKind::Word) { - let text: String = snapshot.text_for_range(range).collect(); + let text: String = buffer_snapshot.text_for_range(range).collect(); if !text.trim().is_empty() { return text; } diff --git a/crates/editor/src/linked_editing_ranges.rs b/crates/editor/src/linked_editing_ranges.rs index a26e24e1cbca4f06e94cde4898e7f9d2a80da8e1..c883ec14fb4c50a11fb4dfba1031baebf4637f11 100644 --- a/crates/editor/src/linked_editing_ranges.rs +++ b/crates/editor/src/linked_editing_ranges.rs @@ -59,7 +59,7 @@ pub(super) fn refresh_linked_ranges( let mut applicable_selections = Vec::new(); editor .update(cx, |editor, cx| { - let selections = editor.selections.all::(cx); + let selections = editor.selections.all::(&editor.display_snapshot(cx)); let snapshot = editor.buffer.read(cx).snapshot(cx); let buffer = editor.buffer.read(cx); for selection in selections { diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 3d8bbb36103f2e82ce421c9ba83dea0bd6396780..cef691dec483c8a9ae978499689db69b14c5dffe 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -154,7 +154,7 @@ pub fn deploy_context_menu( return; } - let display_map = editor.selections.display_map(cx); + let display_map = editor.display_snapshot(cx); let source_anchor = display_map.display_point_to_anchor(point, text::Bias::Right); let context_menu = if let Some(custom) = editor.custom_context_menu.take() { let menu = custom(editor, point, window, cx); @@ -169,8 +169,8 @@ pub fn deploy_context_menu( return; }; - let display_map = editor.selections.display_map(cx); let snapshot = editor.snapshot(window, cx); + let display_map = editor.display_snapshot(cx); let buffer = snapshot.buffer_snapshot(); let anchor = buffer.anchor_before(point.to_point(&display_map)); if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) { @@ -185,7 +185,7 @@ pub fn deploy_context_menu( let has_reveal_target = editor.target_file(cx).is_some(); let has_selections = editor .selections - .all::(cx) + .all::(&display_map) .into_iter() .any(|s| !s.is_empty()); let has_git_repo = buffer diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 1d98cb537ab8cc9dcf7aac23e6c43f6c1a26ff0a..3b2ed55df724485ee72e6afbc02c7111817869fb 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -72,7 +72,12 @@ impl Editor { cx: &mut Context, ) { let scroll_margin_rows = self.vertical_scroll_margin() as u32; - let new_screen_top = self.selections.newest_display(cx).head().row().0; + let new_screen_top = self + .selections + .newest_display(&self.display_snapshot(cx)) + .head() + .row() + .0; let new_screen_top = new_screen_top.saturating_sub(scroll_margin_rows); self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx); } @@ -86,7 +91,12 @@ impl Editor { let Some(visible_rows) = self.visible_line_count().map(|count| count as u32) else { return; }; - let new_screen_top = self.selections.newest_display(cx).head().row().0; + let new_screen_top = self + .selections + .newest_display(&self.display_snapshot(cx)) + .head() + .row() + .0; let new_screen_top = new_screen_top.saturating_sub(visible_rows / 2); self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx); } @@ -101,7 +111,12 @@ impl Editor { let Some(visible_rows) = self.visible_line_count().map(|count| count as u32) else { return; }; - let new_screen_top = self.selections.newest_display(cx).head().row().0; + let new_screen_top = self + .selections + .newest_display(&self.display_snapshot(cx)) + .head() + .row() + .0; let new_screen_top = new_screen_top.saturating_sub(visible_rows.saturating_sub(scroll_margin_rows)); self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx); diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 9130e3cbf879d1b38461a34470b79cc5a50a3cac..28fd9442193bbec663d3f72eaa805214375dd8ca 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -148,7 +148,7 @@ impl Editor { target_top = first_highlighted_row.as_f64(); target_bottom = target_top + 1.; } else { - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); target_top = selections .first() @@ -293,7 +293,7 @@ impl Editor { let scroll_width = ScrollOffset::from(scroll_width); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all::(&display_map); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let mut target_left; diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 9ca2d1cb4ee116fb77bcae41fcb9d6a435629cff..ab0e78595310da43b803cd53b9177dec53a37d81 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -110,7 +110,7 @@ impl SelectionsCollection { if self.pending.is_none() { self.disjoint_anchors_arc() } else { - let all_offset_selections = self.all::(cx); + let all_offset_selections = self.all::(&self.display_map(cx)); let buffer = self.buffer(cx); all_offset_selections .into_iter() @@ -129,25 +129,23 @@ impl SelectionsCollection { pub fn pending>( &self, - cx: &mut App, + snapshot: &DisplaySnapshot, ) -> Option> { - let map = self.display_map(cx); - - resolve_selections(self.pending_anchor(), &map).next() + resolve_selections(self.pending_anchor(), &snapshot).next() } pub(crate) fn pending_mode(&self) -> Option { self.pending.as_ref().map(|pending| pending.mode.clone()) } - pub fn all<'a, D>(&self, cx: &mut App) -> Vec> + pub fn all<'a, D>(&self, snapshot: &DisplaySnapshot) -> Vec> where D: 'a + TextDimension + Ord + Sub, { - let map = self.display_map(cx); let disjoint_anchors = &self.disjoint; - let mut disjoint = resolve_selections::(disjoint_anchors.iter(), &map).peekable(); - let mut pending_opt = self.pending::(cx); + let mut disjoint = + resolve_selections::(disjoint_anchors.iter(), &snapshot).peekable(); + let mut pending_opt = self.pending::(&snapshot); iter::from_fn(move || { if let Some(pending) = pending_opt.as_mut() { while let Some(next_selection) = disjoint.peek() { @@ -175,12 +173,11 @@ impl SelectionsCollection { } /// Returns all of the selections, adjusted to take into account the selection line_mode - pub fn all_adjusted(&self, cx: &mut App) -> Vec> { - let mut selections = self.all::(cx); + pub fn all_adjusted(&self, snapshot: &DisplaySnapshot) -> Vec> { + let mut selections = self.all::(&snapshot); if self.line_mode { - let map = self.display_map(cx); for selection in &mut selections { - let new_range = map.expand_to_line(selection.range()); + let new_range = snapshot.expand_to_line(selection.range()); selection.start = new_range.start; selection.end = new_range.end; } @@ -210,11 +207,10 @@ impl SelectionsCollection { } /// Returns the newest selection, adjusted to take into account the selection line_mode - pub fn newest_adjusted(&self, cx: &mut App) -> Selection { - let mut selection = self.newest::(cx); + pub fn newest_adjusted(&self, snapshot: &DisplaySnapshot) -> Selection { + let mut selection = self.newest::(&snapshot); if self.line_mode { - let map = self.display_map(cx); - let new_range = map.expand_to_line(selection.range()); + let new_range = snapshot.expand_to_line(selection.range()); selection.start = new_range.start; selection.end = new_range.end; } @@ -223,53 +219,55 @@ impl SelectionsCollection { pub fn all_adjusted_display( &self, - cx: &mut App, - ) -> (DisplaySnapshot, Vec>) { + display_map: &DisplaySnapshot, + ) -> Vec> { if self.line_mode { - let selections = self.all::(cx); - let map = self.display_map(cx); + let selections = self.all::(&display_map); let result = selections .into_iter() .map(|mut selection| { - let new_range = map.expand_to_line(selection.range()); + let new_range = display_map.expand_to_line(selection.range()); selection.start = new_range.start; selection.end = new_range.end; - selection.map(|point| point.to_display_point(&map)) + selection.map(|point| point.to_display_point(&display_map)) }) .collect(); - (map, result) + result } else { - self.all_display(cx) + self.all_display(display_map) } } - pub fn disjoint_in_range<'a, D>(&self, range: Range, cx: &mut App) -> Vec> + pub fn disjoint_in_range<'a, D>( + &self, + range: Range, + snapshot: &DisplaySnapshot, + ) -> Vec> where D: 'a + TextDimension + Ord + Sub + std::fmt::Debug, { - let map = self.display_map(cx); let start_ix = match self .disjoint - .binary_search_by(|probe| probe.end.cmp(&range.start, map.buffer_snapshot())) + .binary_search_by(|probe| probe.end.cmp(&range.start, snapshot.buffer_snapshot())) { Ok(ix) | Err(ix) => ix, }; let end_ix = match self .disjoint - .binary_search_by(|probe| probe.start.cmp(&range.end, map.buffer_snapshot())) + .binary_search_by(|probe| probe.start.cmp(&range.end, snapshot.buffer_snapshot())) { Ok(ix) => ix + 1, Err(ix) => ix, }; - resolve_selections(&self.disjoint[start_ix..end_ix], &map).collect() + resolve_selections(&self.disjoint[start_ix..end_ix], snapshot).collect() } - pub fn all_display(&self, cx: &mut App) -> (DisplaySnapshot, Vec>) { - let map = self.display_map(cx); + pub fn all_display(&self, snapshot: &DisplaySnapshot) -> Vec> { let disjoint_anchors = &self.disjoint; - let mut disjoint = resolve_selections_display(disjoint_anchors.iter(), &map).peekable(); - let mut pending_opt = resolve_selections_display(self.pending_anchor(), &map).next(); - let selections = iter::from_fn(move || { + let mut disjoint = + resolve_selections_display(disjoint_anchors.iter(), &snapshot).peekable(); + let mut pending_opt = resolve_selections_display(self.pending_anchor(), &snapshot).next(); + iter::from_fn(move || { if let Some(pending) = pending_opt.as_mut() { while let Some(next_selection) = disjoint.peek() { if pending.start <= next_selection.end && pending.end >= next_selection.start { @@ -292,8 +290,7 @@ impl SelectionsCollection { disjoint.next() } }) - .collect(); - (map, selections) + .collect() } pub fn newest_anchor(&self) -> &Selection { @@ -306,19 +303,15 @@ impl SelectionsCollection { pub fn newest>( &self, - cx: &mut App, + snapshot: &DisplaySnapshot, ) -> Selection { - let map = self.display_map(cx); - - resolve_selections([self.newest_anchor()], &map) + resolve_selections([self.newest_anchor()], &snapshot) .next() .unwrap() } - pub fn newest_display(&self, cx: &mut App) -> Selection { - let map = self.display_map(cx); - - resolve_selections_display([self.newest_anchor()], &map) + pub fn newest_display(&self, snapshot: &DisplaySnapshot) -> Selection { + resolve_selections_display([self.newest_anchor()], &snapshot) .next() .unwrap() } @@ -333,11 +326,9 @@ impl SelectionsCollection { pub fn oldest>( &self, - cx: &mut App, + snapshot: &DisplaySnapshot, ) -> Selection { - let map = self.display_map(cx); - - resolve_selections([self.oldest_anchor()], &map) + resolve_selections([self.oldest_anchor()], &snapshot) .next() .unwrap() } @@ -349,12 +340,18 @@ impl SelectionsCollection { .unwrap_or_else(|| self.disjoint.first().cloned().unwrap()) } - pub fn first>(&self, cx: &mut App) -> Selection { - self.all(cx).first().unwrap().clone() + pub fn first>( + &self, + snapshot: &DisplaySnapshot, + ) -> Selection { + self.all(snapshot).first().unwrap().clone() } - pub fn last>(&self, cx: &mut App) -> Selection { - self.all(cx).last().unwrap().clone() + pub fn last>( + &self, + snapshot: &DisplaySnapshot, + ) -> Selection { + self.all(snapshot).last().unwrap().clone() } /// Returns a list of (potentially backwards!) ranges representing the selections. @@ -362,9 +359,9 @@ impl SelectionsCollection { #[cfg(any(test, feature = "test-support"))] pub fn ranges>( &self, - cx: &mut App, + snapshot: &DisplaySnapshot, ) -> Vec> { - self.all::(cx) + self.all::(snapshot) .iter() .map(|s| { if s.reversed { @@ -596,7 +593,8 @@ impl<'a> MutableSelectionsCollection<'a> { where T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub + std::marker::Copy, { - let mut selections = self.collection.all(self.cx); + let display_map = self.display_map(); + let mut selections = self.collection.all(&display_map); let mut start = range.start.to_offset(&self.buffer()); let mut end = range.end.to_offset(&self.buffer()); let reversed = if start > end { @@ -790,7 +788,7 @@ impl<'a> MutableSelectionsCollection<'a> { ) { let mut changed = false; let display_map = self.display_map(); - let (_, selections) = self.collection.all_display(self.cx); + let selections = self.collection.all_display(&display_map); let selections = selections .into_iter() .map(|selection| { @@ -814,9 +812,10 @@ impl<'a> MutableSelectionsCollection<'a> { ) { let mut changed = false; let snapshot = self.buffer().clone(); + let display_map = self.display_map(); let selections = self .collection - .all::(self.cx) + .all::(&display_map) .into_iter() .map(|selection| { let mut moved_selection = selection.clone(); diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 150044391a397cc2c35ffc8a85311c1470668ab1..6abd3e48880a59f3ce74511013bcd048ad5a2a51 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -82,7 +82,7 @@ impl Editor { if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) { return false; } - let newest_selection = self.selections.newest::(cx); + let newest_selection = self.selections.newest::(&self.display_snapshot(cx)); let head = newest_selection.head(); if !newest_selection.is_empty() && head != newest_selection.tail() { diff --git a/crates/editor/src/tasks.rs b/crates/editor/src/tasks.rs index d27e4564057ae9b0827ddec98bb3cfaeaf455211..e39880ddc1f575a7b12f40c5496c75c1f473c6e9 100644 --- a/crates/editor/src/tasks.rs +++ b/crates/editor/src/tasks.rs @@ -14,7 +14,7 @@ impl Editor { return Task::ready(None); }; let (selection, buffer, editor_snapshot) = { - let selection = self.selections.newest_adjusted(cx); + let selection = self.selections.newest_adjusted(&self.display_snapshot(cx)); let Some((buffer, _)) = self .buffer() .read(cx) diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index a3a8d81c64a709b65d8d7a894e338800cdeb71c5..9d1003e8c08b3d725ffa13b90eb0ee405520d8cd 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -108,7 +108,7 @@ pub fn assert_text_with_selections( assert_eq!(editor.text(cx), unmarked_text, "text doesn't match"); let actual = generate_marked_text( &editor.text(cx), - &editor.selections.ranges(cx), + &editor.selections.ranges(&editor.display_snapshot(cx)), marked_text.contains("«"), ); assert_eq!(actual, marked_text, "Selections don't match"); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 601eb9512cdef472ce0a5d660309d671c339ebe9..c6779d1e564deb57233dd9e4719ca87f8d6a2da1 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -265,7 +265,10 @@ impl EditorTestContext { pub fn pixel_position_for(&mut self, display_point: DisplayPoint) -> Point { self.update_editor(|editor, window, cx| { - let newest_point = editor.selections.newest_display(cx).head(); + let newest_point = editor + .selections + .newest_display(&editor.display_snapshot(cx)) + .head(); let pixel_position = editor.pixel_position_of_newest_cursor.unwrap(); let line_height = editor .style() @@ -590,7 +593,7 @@ impl EditorTestContext { fn editor_selections(&mut self) -> Vec> { self.editor .update(&mut self.cx, |editor, cx| { - editor.selections.all::(cx) + editor.selections.all::(&editor.display_snapshot(cx)) }) .into_iter() .map(|s| { @@ -688,9 +691,12 @@ pub fn assert_state_with_diff( expected_diff_text: &str, ) { let (snapshot, selections) = editor.update_in(cx, |editor, window, cx| { + let snapshot = editor.snapshot(window, cx); ( - editor.snapshot(window, cx).buffer_snapshot().clone(), - editor.selections.ranges::(cx), + snapshot.buffer_snapshot().clone(), + editor + .selections + .ranges::(&snapshot.display_snapshot), ) }); diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index b59d59e23953bb71af8909eb149943e8ff607357..50cba6ce5fd8c6af0fcbbc10855ff92caa532f22 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -490,7 +490,7 @@ async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { cx.executor().advance_clock(Duration::from_secs(2)); editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); + let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx)); assert_eq!( all_selections.len(), 1, @@ -565,7 +565,7 @@ async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { cx.executor().advance_clock(Duration::from_secs(2)); editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all_adjusted(cx); + let all_selections = editor.selections.all_adjusted(&editor.display_snapshot(cx)); assert_eq!( all_selections.len(), 1, diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 8f7dac4e4049a65dbd630966cea249664d22ba61..fd8cd3597377a6de78b3153ccc430afe81b1127e 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -49,7 +49,7 @@ impl TextDiffView { let selection_data = source_editor.update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); let source_buffer = multibuffer.as_singleton()?; - let selections = editor.selections.all::(cx); + let selections = editor.selections.all::(&editor.display_snapshot(cx)); let buffer_snapshot = source_buffer.read(cx); let first_selection = selections.first()?; let max_point = buffer_snapshot.max_point(); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index f9dd0178922b1a479caade3953d2eb0c0e75c83d..9b0fb6d8c16b0e44b1bbfd1464f44bb7e88b0cde 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -74,7 +74,9 @@ impl GoToLine { ) -> Self { let (user_caret, last_line, scroll_position) = active_editor.update(cx, |editor, cx| { let user_caret = UserCaretPosition::at_selection_end( - &editor.selections.last::(cx), + &editor + .selections + .last::(&editor.display_snapshot(cx)), &editor.buffer().read(cx).snapshot(cx), ); @@ -739,7 +741,7 @@ mod tests { let selections = editor.update(cx, |editor, cx| { editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|s| s.start..s.end) .collect::>() diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index 1c24bfdcf44c09a1729065835debd4ef5fbb2252..e834dd6aec003930d68ed745f67aff50b2c8f66b 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -229,8 +229,11 @@ impl LspLogView { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); let last_offset = editor.buffer().read(cx).len(cx); - let newest_cursor_is_at_end = - editor.selections.newest::(cx).start >= last_offset; + let newest_cursor_is_at_end = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .start + >= last_offset; editor.edit( vec![ (last_offset..last_offset, text.as_str()), diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index c1c4b604269ae8d731e39b541e05fbed139692cc..464d518c2e9c697d292d7bffda7ee7bae68dd254 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -252,7 +252,10 @@ impl SyntaxTreeView { .editor .update(cx, |editor, cx| editor.snapshot(window, cx)); let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| { - let selection_range = editor.selections.last::(cx).range(); + let selection_range = editor + .selections + .last::(&editor.display_snapshot(cx)) + .range(); let multi_buffer = editor.buffer().read(cx); let (buffer, range, excerpt_id) = snapshot .buffer_snapshot() diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index d20ed40b7928186e2caf564be5ff66b0bd04f0d1..f62ff0874df8079f44868dfeaa1ad2fd0348e474 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -278,8 +278,12 @@ impl MarkdownPreviewView { this.parse_markdown_from_active_editor(true, window, cx); } EditorEvent::SelectionsChanged { .. } => { - let selection_range = editor - .update(cx, |editor, cx| editor.selections.last::(cx).range()); + let selection_range = editor.update(cx, |editor, cx| { + editor + .selections + .last::(&editor.display_snapshot(cx)) + .range() + }); this.selected_block = this.get_block_index_under_cursor(selection_range); this.list_state.scroll_to_reveal_item(this.selected_block); cx.notify(); diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index ac74d6284f4fe2fe62bcad7be447b142255056b4..9e49fabb474d765aa79703ef55c1c98842bee209 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -245,7 +245,10 @@ impl PickerDelegate for OutlineViewDelegate { let (buffer, cursor_offset) = self.active_editor.update(cx, |editor, cx| { let buffer = editor.buffer().read(cx).snapshot(cx); - let cursor_offset = editor.selections.newest::(cx).head(); + let cursor_offset = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head(); (buffer, cursor_offset) }); selected_index = self @@ -673,7 +676,7 @@ mod tests { let selections = editor.update(cx, |editor, cx| { editor .selections - .all::(cx) + .all::(&editor.display_snapshot(cx)) .into_iter() .map(|s| s.start..s.end) .collect::>() diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 847edef1c697e8e008ec8f1010e99fb87362284e..4a4990b40a5f3f7ad2f182e007593e62a8bcd015 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -3099,7 +3099,10 @@ impl OutlinePanel { cx: &mut Context, ) -> Option { let selection = editor.update(cx, |editor, cx| { - editor.selections.newest::(cx).head() + editor + .selections + .newest::(&editor.display_snapshot(cx)) + .head() }); let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); let multi_buffer = editor.read(cx).buffer(); @@ -6957,13 +6960,13 @@ outline: struct OutlineEntryExcerpt fn selected_row_text(editor: &Entity, cx: &mut App) -> String { editor.update(cx, |editor, cx| { - let selections = editor.selections.all::(cx); - assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions"); - let selection = selections.first().unwrap(); - let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); - let line_start = language::Point::new(selection.start.row, 0); - let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right); - multi_buffer_snapshot.text_for_range(line_start..line_end).collect::().trim().to_owned() + let selections = editor.selections.all::(&editor.display_snapshot(cx)); + assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions"); + let selection = selections.first().unwrap(); + let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let line_start = language::Point::new(selection.start.row, 0); + let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right); + multi_buffer_snapshot.text_for_range(line_start..line_end).collect::().trim().to_owned() }) } diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 3f1e1e1b3c3fd909f667ebf9dc8e717b0d116c78..890041728988bab2914ad7f00ae11637cd9291eb 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -657,7 +657,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { let confirm = panel.update_in(cx, |panel, window, cx| { panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(cx); + let file_name_selections = editor.selections.all::(&editor.display_snapshot(cx)); assert_eq!( file_name_selections.len(), 1, @@ -731,7 +731,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { panel.update_in(cx, |panel, window, cx| { panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(cx); + let file_name_selections = editor.selections.all::(&editor.display_snapshot(cx)); assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); let file_name_selection = &file_name_selections[0]; assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); @@ -1214,7 +1214,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) { panel.update_in(cx, |panel, window, cx| { panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(cx); + let file_name_selections = editor.selections.all::(&editor.display_snapshot(cx)); assert_eq!( file_name_selections.len(), 1, diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs index b4c928c33e021229caaa68e11b7cdd7228ed934d..a47d680e9bfe7a82cee25db360a59223e89df93e 100644 --- a/crates/repl/src/repl_editor.rs +++ b/crates/repl/src/repl_editor.rs @@ -85,7 +85,11 @@ pub fn run( let editor = editor.upgrade().context("editor was dropped")?; let selected_range = editor - .update(cx, |editor, cx| editor.selections.newest_adjusted(cx)) + .update(cx, |editor, cx| { + editor + .selections + .newest_adjusted(&editor.display_snapshot(cx)) + }) .range(); let multibuffer = editor.read(cx).buffer().clone(); let Some(buffer) = multibuffer.read(cx).as_singleton() else { @@ -473,7 +477,9 @@ fn language_supported(language: &Arc, cx: &mut App) -> bool { fn get_language(editor: WeakEntity, cx: &mut App) -> Option> { editor .update(cx, |editor, cx| { - let selection = editor.selections.newest::(cx); + let selection = editor + .selections + .newest::(&editor.display_snapshot(cx)); let buffer = editor.buffer().read(cx).snapshot(cx); buffer.language_at(selection.head()).cloned() }) diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index c92ce4720e8ccd0454a83409d76789334192745f..a921d182e6ebd0ef96ef0b8d1cce75ed6d532d96 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -50,7 +50,8 @@ impl Vim { pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context) { let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| { - let (map, selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_adjusted_display(&display_map); let buffer = editor.buffer().clone(); let pop_state = editor @@ -59,7 +60,7 @@ impl Vim { .map(|previous| { previous.len() == selections.len() && previous.iter().enumerate().all(|(ix, p)| { - p.to_display_point(&map).row() == selections[ix].head().row() + p.to_display_point(&display_map).row() == selections[ix].head().row() }) }) .unwrap_or(false); @@ -68,11 +69,11 @@ impl Vim { .into_iter() .map(|s| { let point = if vim.mode == Mode::Insert { - movement::saturating_left(&map, s.head()) + movement::saturating_left(&display_map, s.head()) } else { s.head() }; - map.display_point_to_anchor(point, Bias::Left) + display_map.display_point_to_anchor(point, Bias::Left) }) .collect::>(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index f5a170055d3604f023c4c31fa5a413a8e52cbc66..9dc4ec999a47e6a0e8ab802761cab474ef81499b 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -606,7 +606,9 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { let result = vim.update_editor(cx, |vim, editor, cx| { let snapshot = editor.snapshot(window, cx); let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?; - let current = editor.selections.newest::(cx); + let current = editor + .selections + .newest::(&editor.display_snapshot(cx)); let target = snapshot .buffer_snapshot() .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left); @@ -1903,7 +1905,9 @@ impl OnMatchingLines { }); window.dispatch_action(action, cx); cx.defer_in(window, move |editor, window, cx| { - let newest = editor.selections.newest::(cx); + let newest = editor + .selections + .newest::(&editor.display_snapshot(cx)); editor.change_selections( SelectionEffects::no_scroll(), window, @@ -2000,7 +2004,9 @@ impl Vim { }; let command = self.update_editor(cx, |_, editor, cx| { let snapshot = editor.snapshot(window, cx); - let start = editor.selections.newest_display(cx); + let start = editor + .selections + .newest_display(&editor.display_snapshot(cx)); let text_layout_details = editor.text_layout_details(window); let (mut range, _) = motion .range( @@ -2047,7 +2053,9 @@ impl Vim { }; let command = self.update_editor(cx, |_, editor, cx| { let snapshot = editor.snapshot(window, cx); - let start = editor.selections.newest_display(cx); + let start = editor + .selections + .newest_display(&editor.display_snapshot(cx)); let range = object .range(&snapshot, start.clone(), around, None) .unwrap_or(start.range()); @@ -2156,7 +2164,11 @@ impl ShellExec { Point::new(range.start.0, 0) ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right) } else { - let mut end = editor.selections.newest::(cx).range().end; + let mut end = editor + .selections + .newest::(&editor.display_snapshot(cx)) + .range() + .end; end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right); needs_newline_prefix = end == snapshot.max_point(); end..end diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index ed7abaa11c1a6b95e436b019de1605792c82fc9d..6788a186fb45222f7b09fe756862e6cb337c6d90 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -345,7 +345,7 @@ impl Vim { self.update_editor(cx, |vim, editor, cx| { let has_selection = editor .selections - .all_adjusted(cx) + .all_adjusted(&editor.display_snapshot(cx)) .iter() .any(|selection| !selection.is_empty()); @@ -478,19 +478,20 @@ impl Vim { pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context) { self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { - let (map, selections) = editor.selections.all_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_display(&display_map); // Store selection info for positioning after edit let selection_info: Vec<_> = selections .iter() .map(|selection| { let range = selection.range(); - let start_offset = range.start.to_offset(&map, Bias::Left); - let end_offset = range.end.to_offset(&map, Bias::Left); + let start_offset = range.start.to_offset(&display_map, Bias::Left); + let end_offset = range.end.to_offset(&display_map, Bias::Left); let was_empty = range.is_empty(); let was_reversed = selection.reversed; ( - map.buffer_snapshot().anchor_before(start_offset), + display_map.buffer_snapshot().anchor_before(start_offset), end_offset - start_offset, was_empty, was_reversed, @@ -504,11 +505,11 @@ impl Vim { // For empty selections, extend to replace one character if range.is_empty() { - range.end = movement::saturating_right(&map, range.start); + range.end = movement::saturating_right(&display_map, range.start); } - let byte_range = range.start.to_offset(&map, Bias::Left) - ..range.end.to_offset(&map, Bias::Left); + let byte_range = range.start.to_offset(&display_map, Bias::Left) + ..range.end.to_offset(&display_map, Bias::Left); if !byte_range.is_empty() { let replacement_text = text.repeat(byte_range.len()); @@ -568,7 +569,7 @@ impl Vim { self.update_editor(cx, |_, editor, cx| { editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = editor.selections.all::(cx); + let mut selections = editor.selections.all::(&display_map); let max_point = display_map.buffer_snapshot().max_point(); let buffer_snapshot = &display_map.buffer_snapshot(); @@ -606,7 +607,9 @@ impl Vim { cx: &mut Context, ) { self.update_editor(cx, |_, editor, cx| { - let newest = editor.selections.newest::(cx); + let newest = editor + .selections + .newest::(&editor.display_snapshot(cx)); editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest])); }); } @@ -633,7 +636,10 @@ impl Vim { if yank { vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); } - let selections = editor.selections.all::(cx).into_iter(); + let selections = editor + .selections + .all::(&editor.display_snapshot(cx)) + .into_iter(); let edits = selections.map(|selection| (selection.start..selection.end, "")); editor.edit(edits, cx); }); diff --git a/crates/vim/src/helix/duplicate.rs b/crates/vim/src/helix/duplicate.rs index 91e53f13e9962db30ad34b91a3c29e2a530626d8..1b1f10b00b6a7381f22c6ec3be674dc2c085eff6 100644 --- a/crates/vim/src/helix/duplicate.rs +++ b/crates/vim/src/helix/duplicate.rs @@ -56,7 +56,8 @@ impl Vim { let times = times.unwrap_or(1); self.update_editor(cx, |_, editor, cx| { let mut selections = Vec::new(); - let (map, mut original_selections) = editor.selections.all_display(cx); + let map = editor.display_snapshot(cx); + let mut original_selections = editor.selections.all_display(&map); // The order matters, because it is recorded when the selections are added. if above { original_selections.reverse(); diff --git a/crates/vim/src/helix/paste.rs b/crates/vim/src/helix/paste.rs index 9b6b6e454ac1e8d3a47009fcd85db0d2da00261e..62d8c6caef99050cffa17a2e608a924aa97c3e99 100644 --- a/crates/vim/src/helix/paste.rs +++ b/crates/vim/src/helix/paste.rs @@ -44,7 +44,8 @@ impl Vim { return; }; - let (display_map, current_selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let current_selections = editor.selections.all_adjusted_display(&display_map); // The clipboard can have multiple selections, and there can // be multiple selections. Helix zips them together, so the first diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 5b9fef402a7b4fee9ae1d8722cb2cf22f3c2fdb9..98d542dbc4d3651b5307959fba01bf7320983cc9 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -84,7 +84,7 @@ impl Vim { self.update_editor(cx, |_, editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); let mut edits = Vec::new(); - for selection in editor.selections.all::(cx) { + for selection in editor.selections.all::(&editor.display_snapshot(cx)) { let point = selection.head(); let new_row = match direction { Direction::Next => point.row + 1, diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2757a927aaa8ad7d565e56ccbda4042e5f3c56b3..f80f9be38edbb7fafb0864437c8de2bda4740154 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -657,7 +657,7 @@ impl Vim { self.switch_mode(Mode::Insert, false, window, cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(cx); + let selections = editor.selections.all::(&editor.display_snapshot(cx)); let snapshot = editor.buffer().read(cx).snapshot(cx); let selection_start_rows: BTreeSet = selections @@ -699,7 +699,7 @@ impl Vim { self.update_editor(cx, |_, editor, cx| { let text_layout_details = editor.text_layout_details(window); editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(cx); + let selections = editor.selections.all::(&editor.display_snapshot(cx)); let snapshot = editor.buffer().read(cx).snapshot(cx); let selection_end_rows: BTreeSet = selections @@ -745,7 +745,7 @@ impl Vim { Vim::take_forced_motion(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, _, cx| { - let selections = editor.selections.all::(cx); + let selections = editor.selections.all::(&editor.display_snapshot(cx)); let selection_start_rows: BTreeSet = selections .into_iter() @@ -774,9 +774,10 @@ impl Vim { Vim::take_forced_motion(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all::(&display_map); let snapshot = editor.buffer().read(cx).snapshot(cx); - let (_map, display_selections) = editor.selections.all_display(cx); + let display_selections = editor.selections.all_display(&display_map); let original_positions = display_selections .iter() .map(|s| (s.id, s.head())) @@ -937,13 +938,14 @@ impl Vim { self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); - let (map, display_selections) = editor.selections.all_display(cx); + let display_map = editor.display_snapshot(cx); + let display_selections = editor.selections.all_display(&display_map); - let mut edits = Vec::new(); + let mut edits = Vec::with_capacity(display_selections.len()); for selection in &display_selections { let mut range = selection.range(); for _ in 0..count { - let new_point = movement::saturating_right(&map, range.end); + let new_point = movement::saturating_right(&display_map, range.end); if range.end == new_point { return; } @@ -951,8 +953,8 @@ impl Vim { } edits.push(( - range.start.to_offset(&map, Bias::Left) - ..range.end.to_offset(&map, Bias::Left), + range.start.to_offset(&display_map, Bias::Left) + ..range.end.to_offset(&display_map, Bias::Left), text.repeat(if is_return_char { 0 } else { count }), )); } @@ -976,16 +978,16 @@ impl Vim { pub fn save_selection_starts( &self, editor: &Editor, - cx: &mut Context, ) -> HashMap { - let (map, selections) = editor.selections.all_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_display(&display_map); selections .iter() .map(|selection| { ( selection.id, - map.display_point_to_anchor(selection.start, Bias::Right), + display_map.display_point_to_anchor(selection.start, Bias::Right), ) }) .collect::>() diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index 11d040850d341155bf428ebc337cc9e3f4cc42c3..0ee132a44d20723970fecbbef4cef13ff31e310c 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -199,7 +199,7 @@ impl Vim { let mut ranges = Vec::new(); let mut cursor_positions = Vec::new(); let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(cx) { + for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) { match vim.mode { Mode::Visual | Mode::VisualLine => { ranges.push(selection.start..selection.end); diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 34ac4aab1f11c547ed1335e1a9da12fe52be9b08..4b27b4dfaf911c72458c9f412d5d0d2ba4cd70b8 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -58,7 +58,7 @@ impl Vim { let mut new_anchors = Vec::new(); let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(cx) { + for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) { if !selection.is_empty() && (vim.mode != Mode::VisualBlock || new_anchors.is_empty()) { diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index ea9aafe1315d3d89afe9d258f4e736717ffe789f..3bb040511fdd7fa53dd97198ae02b492b0e7359d 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -50,16 +50,19 @@ impl Vim { let mut reversed = vec![]; self.update_editor(cx, |vim, editor, cx| { - let (map, selections) = editor.selections.all_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_display(&display_map); for selection in selections { - let end = movement::saturating_left(&map, selection.end); + let end = movement::saturating_left(&display_map, selection.end); ends.push( - map.buffer_snapshot() - .anchor_before(end.to_offset(&map, Bias::Left)), + display_map + .buffer_snapshot() + .anchor_before(end.to_offset(&display_map, Bias::Left)), ); starts.push( - map.buffer_snapshot() - .anchor_before(selection.start.to_offset(&map, Bias::Left)), + display_map + .buffer_snapshot() + .anchor_before(selection.start.to_offset(&display_map, Bias::Left)), ); reversed.push(selection.reversed) } @@ -301,19 +304,21 @@ impl Vim { name = "'"; } if matches!(name, "{" | "}" | "(" | ")") { - let (map, selections) = editor.selections.all_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_display(&display_map); let anchors = selections .into_iter() .map(|selection| { let point = match name { - "{" => movement::start_of_paragraph(&map, selection.head(), 1), - "}" => movement::end_of_paragraph(&map, selection.head(), 1), - "(" => motion::sentence_backwards(&map, selection.head(), 1), - ")" => motion::sentence_forwards(&map, selection.head(), 1), + "{" => movement::start_of_paragraph(&display_map, selection.head(), 1), + "}" => movement::end_of_paragraph(&display_map, selection.head(), 1), + "(" => motion::sentence_backwards(&display_map, selection.head(), 1), + ")" => motion::sentence_forwards(&display_map, selection.head(), 1), _ => unreachable!(), }; - map.buffer_snapshot() - .anchor_before(point.to_offset(&map, Bias::Left)) + display_map + .buffer_snapshot() + .anchor_before(point.to_offset(&display_map, Bias::Left)) }) .collect::>(); return Some(Mark::Local(anchors)); diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 2a45695928ec3fe87a8f26c5161bb1f095186c53..74a28322d13b6ab0f563e6953f6b1edbfea66740 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -56,7 +56,8 @@ impl Vim { vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx); } - let (display_map, current_selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let current_selections = editor.selections.all_adjusted_display(&display_map); // unlike zed, if you have a multi-cursor selection from vim block mode, // pasting it will paste it on subsequent lines, even if you don't yet @@ -173,7 +174,7 @@ impl Vim { original_indent_columns.push(original_indent_column); } - let cursor_offset = editor.selections.last::(cx).head(); + let cursor_offset = editor.selections.last::(&display_map).head(); if editor .buffer() .read(cx) diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index edb3d7f2157aec8d23faee5fa1a069a10974360f..ff884e3b7393b39b86114338fe2af11e384e1fa0 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -363,7 +363,10 @@ mod test { point(0., 3.0) ); assert_eq!( - editor.selections.newest(cx).range(), + editor + .selections + .newest(&editor.display_snapshot(cx)) + .range(), Point::new(6, 0)..Point::new(6, 0) ) }); @@ -380,7 +383,10 @@ mod test { point(0., 3.0) ); assert_eq!( - editor.selections.newest(cx).range(), + editor + .selections + .newest(&editor.display_snapshot(cx)) + .range(), Point::new(0, 0)..Point::new(6, 1) ) }); diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 889d48717068b0561fd21614dc9fb5d0581754dc..df8d7b4879e21491ed808de1dad78cfebc5b12ec 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -94,7 +94,10 @@ impl Vim { MotionKind::Exclusive }; vim.copy_selections_content(editor, kind, window, cx); - let selections = editor.selections.all::(cx).into_iter(); + let selections = editor + .selections + .all::(&editor.display_snapshot(cx)) + .into_iter(); let edits = selections.map(|selection| (selection.start..selection.end, "")); editor.edit(edits, cx); }); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index fe8180ffff37de51a019b394fd5742278b9355e2..d5a45fca544d61735f62a8f46e849db2c009847f 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -106,7 +106,7 @@ impl Vim { true, editor .selections - .all_adjusted(cx) + .all_adjusted(&editor.display_snapshot(cx)) .iter() .map(|s| s.range()) .collect(), @@ -128,7 +128,7 @@ impl Vim { false, editor .selections - .all_adjusted(cx) + .all_adjusted(&editor.display_snapshot(cx)) .iter() .map(|s| s.range()) .collect(), diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 40fe4f213e205569129775a2e495ec2b3bee14b6..c9a9fbdb9ee3428ce80c934a686a73a63ddee714 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -53,7 +53,7 @@ impl Vim { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let map = editor.snapshot(window, cx); - let display_selections = editor.selections.all::(cx); + let display_selections = editor.selections.all::(&map.display_snapshot); // Handles all string that require manipulation, including inserts and replaces let edits = display_selections @@ -98,7 +98,7 @@ impl Vim { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); let map = editor.snapshot(window, cx); - let selections = editor.selections.all::(cx); + let selections = editor.selections.all::(&map.display_snapshot); let mut new_selections = vec![]; let edits: Vec<(Range, String)> = selections .into_iter() @@ -150,7 +150,9 @@ impl Vim { self.stop_recording(cx); self.update_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); - let mut selection = editor.selections.newest_display(cx); + let mut selection = editor + .selections + .newest_display(&editor.display_snapshot(cx)); let snapshot = editor.snapshot(window, cx); object.expand_selection(&snapshot, &mut selection, around, None); let start = snapshot @@ -196,7 +198,9 @@ impl Vim { self.update_editor(cx, |vim, editor, cx| { editor.set_clip_at_line_ends(false, cx); let text_layout_details = editor.text_layout_details(window); - let mut selection = editor.selections.newest_display(cx); + let mut selection = editor + .selections + .newest_display(&editor.display_snapshot(cx)); let snapshot = editor.snapshot(window, cx); motion.expand_selection( &snapshot, diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 98a2fe0b17fb27bc7201fb82a904b0d6c4ade1cc..959edff63dd50fa549edcbae1bea213224b923af 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -863,7 +863,9 @@ impl VimGlobals { } } '%' => editor.and_then(|editor| { - let selection = editor.selections.newest::(cx); + let selection = editor + .selections + .newest::(&editor.display_snapshot(cx)); if let Some((_, buffer, _)) = editor .buffer() .read(cx) diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index e1b46f56a9e8b934e8c8e55d144b8eb325352375..bc817e2d4871a0be07e8c100b332f5630dcec711 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -45,7 +45,8 @@ impl Vim { }, }; let surround = pair.end != surround_alias((*text).as_ref()); - let (display_map, display_selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let display_selections = editor.selections.all_adjusted_display(&display_map); let mut edits = Vec::new(); let mut anchors = Vec::new(); @@ -144,7 +145,8 @@ impl Vim { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); - let (display_map, display_selections) = editor.selections.all_display(cx); + let display_map = editor.display_snapshot(cx); + let display_selections = editor.selections.all_display(&display_map); let mut edits = Vec::new(); let mut anchors = Vec::new(); @@ -256,7 +258,8 @@ impl Vim { let preserve_space = will_replace_pair.start == will_replace_pair.end || !opening; - let (display_map, selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_adjusted_display(&display_map); let mut edits = Vec::new(); let mut anchors = Vec::new(); @@ -382,7 +385,8 @@ impl Vim { self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { editor.set_clip_at_line_ends(false, cx); - let (display_map, selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_adjusted_display(&display_map); let mut anchors = Vec::new(); for selection in &selections { @@ -500,7 +504,8 @@ impl Vim { let mut min_range_size = usize::MAX; let _ = self.editor.update(cx, |editor, cx| { - let (display_map, selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_adjusted_display(&display_map); // Even if there's multiple cursors, we'll simply rely on // the first one to understand what bracket pair to map to. // I believe we could, if worth it, go one step above and diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 297ef6447368bdc35ed3935b25dcd687f0e9f252..93b610877a163ba0f3035e8a0483f531a3246e6c 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -2295,7 +2295,10 @@ async fn test_clipping_on_mode_change(cx: &mut gpui::TestAppContext) { let mut pixel_position = cx.update_editor(|editor, window, cx| { let snapshot = editor.snapshot(window, cx); - let current_head = editor.selections.newest_display(cx).end; + let current_head = editor + .selections + .newest_display(&snapshot.display_snapshot) + .end; editor.last_bounds().unwrap().origin + editor .display_to_pixel_point(current_head, &snapshot, window) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 1d8aed62936711cdd048ee1817d2d2aad475f628..7481d176109907baccf6e742d0b3f3614014dcac 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1359,7 +1359,10 @@ impl Vim { return; }; let newest_selection_empty = editor.update(cx, |editor, cx| { - editor.selections.newest::(cx).is_empty() + editor + .selections + .newest::(&editor.display_snapshot(cx)) + .is_empty() }); let editor = editor.read(cx); let editor_mode = editor.mode(); @@ -1455,9 +1458,11 @@ impl Vim { cx: &mut Context, ) -> Option { self.update_editor(cx, |_, editor, cx| { - let selection = editor.selections.newest::(cx); + let snapshot = &editor.snapshot(window, cx); + let selection = editor + .selections + .newest::(&snapshot.display_snapshot); - let snapshot = editor.snapshot(window, cx); let snapshot = snapshot.buffer_snapshot(); let (range, kind) = snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion)); @@ -1484,9 +1489,11 @@ impl Vim { let selections = self.editor().map(|editor| { editor.update(cx, |editor, cx| { + let snapshot = editor.display_snapshot(cx); + ( - editor.selections.oldest::(cx), - editor.selections.newest::(cx), + editor.selections.oldest::(&snapshot), + editor.selections.newest::(&snapshot), ) }) }); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index fcbfd11bb62b9c6cf1e4df54f7521b4ba4810f69..bce49eb7d4a3c21b00a8076f0474d9da591cc993 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -747,7 +747,8 @@ impl Vim { self.stop_recording(cx); self.update_editor(cx, |_, editor, cx| { editor.transact(window, cx, |editor, window, cx| { - let (display_map, selections) = editor.selections.all_adjusted_display(cx); + let display_map = editor.display_snapshot(cx); + let selections = editor.selections.all_adjusted_display(&display_map); // Selections are biased right at the start. So we need to store // anchors that are biased left so that we can restore the selections @@ -858,7 +859,9 @@ impl Vim { }); } self.update_editor(cx, |_, editor, cx| { - let latest = editor.selections.newest::(cx); + let latest = editor + .selections + .newest::(&editor.display_snapshot(cx)); start_selection = latest.start; end_selection = latest.end; }); @@ -879,7 +882,9 @@ impl Vim { return; } self.update_editor(cx, |_, editor, cx| { - let latest = editor.selections.newest::(cx); + let latest = editor + .selections + .newest::(&editor.display_snapshot(cx)); if vim_is_normal { start_selection = latest.start; end_selection = latest.end; diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index 82eb82de1e2807346eb3ade2ced8a7946413f0a4..5210bb718c0663d2c256f865f0fcabf41bd5708f 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -54,7 +54,8 @@ impl QuickActionBar { .count() .ne(&0) .then(|| { - let latest = this.selections.newest_display(cx); + let snapshot = this.display_snapshot(cx); + let latest = this.selections.newest_display(&snapshot); !latest.is_empty() }) .unwrap_or_default() From 60285459e8374705f4ebef12ea906cf275ac76c7 Mon Sep 17 00:00:00 2001 From: Seivan Date: Fri, 17 Oct 2025 22:26:10 +0200 Subject: [PATCH 009/202] gpui: Update link to Ownership and data flow section (#40457) Fixed broken link to `Ownership and data flow section`. --- crates/gpui/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/README.md b/crates/gpui/README.md index 4b2ba8818b7e983a52730c8399e8afaab79d5e5e..2c411f76cd4782904f5e704c446a6f0e76f7d9ab 100644 --- a/crates/gpui/README.md +++ b/crates/gpui/README.md @@ -11,7 +11,7 @@ GPUI is still in active development as we work on the Zed code editor, and is st gpui = { version = "*" } ``` - - [Ownership and data flow](_ownership_and_data_flow) + - [Ownership and data flow](src/_ownership_and_data_flow.rs) Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example. From a660a39ae882ec3d6413c9cb0bdb47e6a8cab762 Mon Sep 17 00:00:00 2001 From: Seeni <20085943+seeni-dev@users.noreply.github.com> Date: Sat, 18 Oct 2025 02:32:53 +0530 Subject: [PATCH 010/202] docs: Update cpp.md to indicate GDB version requirements (#40027) Release Notes: - N/A --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Marshall Bowers --- docs/src/languages/cpp.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md index 90e1934c16b5ff62b1eb8611c06af4c04cfa879d..36cdc7a9580d2de41a6eb7063d694d54c7caffa4 100644 --- a/docs/src/languages/cpp.md +++ b/docs/src/languages/cpp.md @@ -137,6 +137,7 @@ You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build - [CodeLLDB configuration documentation](https://github.com/vadimcn/codelldb/blob/master/MANUAL.md#starting-a-new-debug-session) - [GDB configuration documentation](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Debugger-Adapter-Protocol.html) + - GDB needs to be at least v14.1 ### Build and Debug Binary From 22fd91d4909ec051cdf415c9450084df4039aa0f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 18 Oct 2025 00:04:08 +0300 Subject: [PATCH 011/202] Re-register buffers on server stop (#40504) Follow-up of https://github.com/zed-industries/zed/pull/40388 Release Notes: - N/A --- crates/editor/src/editor.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 66e5b4df9d035e85114d177ba0456fba9d2f3d10..cc02740900770e4bee42ad49361f65b7dda98568 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1883,8 +1883,14 @@ impl Editor { project::Event::RefreshInlayHints => { editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); } - project::Event::LanguageServerAdded(..) - | project::Event::LanguageServerRemoved(..) => { + project::Event::LanguageServerRemoved(..) => { + if editor.tasks_update_task.is_none() { + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + } + editor.registered_buffers.clear(); + editor.register_visible_buffers(cx); + } + project::Event::LanguageServerAdded(..) => { if editor.tasks_update_task.is_none() { editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } @@ -22102,8 +22108,7 @@ impl Editor { } fn register_visible_buffers(&mut self, cx: &mut Context) { - // Singletons are registered on editor creation. - if self.ignore_lsp_data() || self.buffer().read(cx).is_singleton() { + if self.ignore_lsp_data() { return; } for (_, (visible_buffer, _, _)) in self.visible_excerpts(None, cx) { From 4dd463f8ac2b05014102bab1783f589cbb309948 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 17 Oct 2025 14:57:03 -0700 Subject: [PATCH 012/202] Fix repo path to project path conversion in git panel (#40535) Closes https://github.com/zed-industries/zed/issues/40422 Closes https://github.com/zed-industries/zed/issues/40379 Closes https://github.com/zed-industries/zed/issues/40307 Release Notes: - Fixed an issue where the project diff view did not work for multi-repo projects on Windows when using WSL or SSH remoting --- crates/project/src/git_store.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index c58f7217a9bedab02f62337bb44d87aa5b7bbade..461b15ef5f84a22f2a666674e12d27f0ef341d99 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2910,6 +2910,13 @@ impl RepositorySnapshot { Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style) } + fn repo_path_to_abs_path(&self, repo_path: &RepoPath) -> PathBuf { + self.path_style + .join(&self.work_directory_abs_path, repo_path.as_std_path()) + .unwrap() + .into() + } + #[inline] fn abs_path_to_repo_path_inner( work_directory_abs_path: &Path, @@ -3349,10 +3356,7 @@ impl Repository { pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option { let git_store = self.git_store.upgrade()?; let worktree_store = git_store.read(cx).worktree_store.read(cx); - let abs_path = self - .snapshot - .work_directory_abs_path - .join(path.as_std_path()); + let abs_path = self.snapshot.repo_path_to_abs_path(path); let abs_path = SanitizedPath::new(&abs_path); let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?; Some(ProjectPath { From 438c890816203f9402981022b80e15b0731ffd86 Mon Sep 17 00:00:00 2001 From: Nia Date: Sat, 18 Oct 2025 00:11:09 +0200 Subject: [PATCH 013/202] perf: Add on search + fixups (#40537) Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + crates/search/Cargo.toml | 1 + crates/search/src/buffer_search.rs | 12 ++++++++++++ crates/search/src/project_search.rs | 11 +++++++++++ tooling/perf/src/implementation.rs | 9 ++++++--- tooling/perf/src/main.rs | 2 +- 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e85c8adaad744c4e7940764c5d6710c8f8344352..cca144a2bfdd90989013eae1d296c5fb6cf6e11c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14867,6 +14867,7 @@ dependencies = [ "ui", "unindent", "util", + "util_macros", "workspace", "zed_actions", ] diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 3f06b4228714e67b87d83e69b1afeb2a8cb6a155..e6bea2e4dd634ca915242f8d86fc31e22bb61c95 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -39,6 +39,7 @@ smol.workspace = true theme.workspace = true ui.workspace = true util.workspace = true +util_macros.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 84be844532931eaf8c0ce5ced5bfc56c14a62dd0..923e30e0b6878ad32fd65d210101a5a62fd38687 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1524,6 +1524,7 @@ mod tests { use settings::{SearchSettingsContent, SettingsStore}; use smol::stream::StreamExt as _; use unindent::Unindent as _; + use util_macros::perf; fn init_globals(cx: &mut TestAppContext) { cx.update(|cx| { @@ -1580,6 +1581,7 @@ mod tests { (editor.unwrap(), search_bar, cx) } + #[perf] #[gpui::test] async fn test_search_simple(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); @@ -1860,6 +1862,7 @@ mod tests { .collect::>() } + #[perf] #[gpui::test] async fn test_search_option_handling(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); @@ -1920,6 +1923,7 @@ mod tests { }); } + #[perf] #[gpui::test] async fn test_search_select_all_matches(cx: &mut TestAppContext) { init_globals(cx); @@ -2128,6 +2132,7 @@ mod tests { .unwrap(); } + #[perf] #[gpui::test] async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) { init_globals(cx); @@ -2213,6 +2218,7 @@ mod tests { }); } + #[perf] #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { let (_editor, search_bar, cx) = init_test(cx); @@ -2362,6 +2368,7 @@ mod tests { }); } + #[perf] #[gpui::test] async fn test_replace_simple(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); @@ -2529,6 +2536,7 @@ mod tests { ); } + #[perf] #[gpui::test] async fn test_replace_special_characters(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); @@ -2592,6 +2600,7 @@ mod tests { .await; } + #[perf] #[gpui::test] async fn test_find_matches_in_selections_singleton_buffer_multiple_selections( cx: &mut TestAppContext, @@ -2658,6 +2667,7 @@ mod tests { }); } + #[perf] #[gpui::test] async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections( cx: &mut TestAppContext, @@ -2744,6 +2754,7 @@ mod tests { }); } + #[perf] #[gpui::test] async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) { let (editor, search_bar, cx) = init_test(cx); @@ -2779,6 +2790,7 @@ mod tests { }); } + #[perf] #[gpui::test] async fn test_search_options_changes(cx: &mut TestAppContext) { let (_editor, search_bar, cx) = init_test(cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1373c7d8454c9e31681d8365f167026f666cb429..c292d05df75ad329c608d456aaf07a9d1d3af044 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2359,8 +2359,10 @@ pub mod tests { use serde_json::json; use settings::SettingsStore; use util::{path, paths::PathStyle, rel_path::rel_path}; + use util_macros::perf; use workspace::DeploySearch; + #[perf] #[gpui::test] async fn test_project_search(cx: &mut TestAppContext) { init_test(cx); @@ -2498,6 +2500,7 @@ pub mod tests { .unwrap(); } + #[perf] #[gpui::test] async fn test_deploy_project_search_focus(cx: &mut TestAppContext) { init_test(cx); @@ -2738,6 +2741,7 @@ pub mod tests { }).unwrap(); } + #[perf] #[gpui::test] async fn test_filters_consider_toggle_state(cx: &mut TestAppContext) { init_test(cx); @@ -2858,6 +2862,7 @@ pub mod tests { .unwrap(); } + #[perf] #[gpui::test] async fn test_new_project_search_focus(cx: &mut TestAppContext) { init_test(cx); @@ -3153,6 +3158,7 @@ pub mod tests { });}).unwrap(); } + #[perf] #[gpui::test] async fn test_new_project_search_in_directory(cx: &mut TestAppContext) { init_test(cx); @@ -3279,6 +3285,7 @@ pub mod tests { .unwrap(); } + #[perf] #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { init_test(cx); @@ -3609,6 +3616,7 @@ pub mod tests { .unwrap(); } + #[perf] #[gpui::test] async fn test_search_query_history_with_multiple_views(cx: &mut TestAppContext) { init_test(cx); @@ -3832,6 +3840,7 @@ pub mod tests { assert_eq!(active_query(&search_view_1, cx), ""); } + #[perf] #[gpui::test] async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) { init_test(cx); @@ -3991,6 +4000,7 @@ pub mod tests { .unwrap(); } + #[perf] #[gpui::test] async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) { init_test(cx); @@ -4071,6 +4081,7 @@ pub mod tests { .expect("unable to update search view"); } + #[perf] #[gpui::test] async fn test_buffer_search_query_reused(cx: &mut TestAppContext) { init_test(cx); diff --git a/tooling/perf/src/implementation.rs b/tooling/perf/src/implementation.rs index 5c050cf5b136f5dfa58c419057b28d53cf529465..535f25a2b312133d5ef446a668b50f2bcdef7489 100644 --- a/tooling/perf/src/implementation.rs +++ b/tooling/perf/src/implementation.rs @@ -420,13 +420,16 @@ impl std::fmt::Display for PerfReport { for (cat, delta) in sorted.into_iter().rev() { const SIGN_POS: &str = "↑"; const SIGN_NEG: &str = "↓"; - const SIGN_NEUTRAL: &str = "±"; + const SIGN_NEUTRAL_POS: &str = "±↑"; + const SIGN_NEUTRAL_NEG: &str = "±↓"; let prettify = |time: f64| { let sign = if time > 0.05 { SIGN_POS - } else if time < 0.05 && time > -0.05 { - SIGN_NEUTRAL + } else if time > 0. { + SIGN_NEUTRAL_POS + } else if time > -0.05 { + SIGN_NEUTRAL_NEG } else { SIGN_NEG }; diff --git a/tooling/perf/src/main.rs b/tooling/perf/src/main.rs index c5a251a3e3920248278813236509435dadb86525..1e6ddedf11e2c5f265d3d4dd93785afbf7f565d2 100644 --- a/tooling/perf/src/main.rs +++ b/tooling/perf/src/main.rs @@ -228,8 +228,8 @@ fn compare_profiles(args: &[String]) { a.strip_prefix("--save=") .expect("FATAL: save param formatted incorrectly"), ); + ident_idx = 1; } - ident_idx = 1; }); let ident_new = args .get(ident_idx) From d7e193caf36a3913d74ff26de93be1e3e752545c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 17 Oct 2025 15:11:36 -0700 Subject: [PATCH 014/202] Show telemetry for adding all items (#40541) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/workspace/src/pane.rs | 9 +++++++++ crates/workspace/src/workspace.rs | 4 ---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ebb55f4a75669d5596c0d2ddb554b0a83c12062a..0784f30739be9ef6bf6c65f38e2f7e52c73390e8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -954,6 +954,11 @@ impl Pane { if allow_preview { pane.set_preview_item_id(Some(new_item.item_id()), cx); } + + if let Some(text) = new_item.telemetry_event_text(cx) { + telemetry::event!(text); + } + pane.add_item_inner( new_item, true, @@ -1170,6 +1175,10 @@ impl Pane { window: &mut Window, cx: &mut Context, ) { + if let Some(text) = item.telemetry_event_text(cx) { + telemetry::event!(text); + } + self.add_item_inner( item, activate_pane, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e7942a3d86e33fcab4cb1ecb0a9c11d248fd90f9..bbb9ee767196c062707efcc2618670cf09da4e87 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3294,10 +3294,6 @@ impl Workspace { window: &mut Window, cx: &mut App, ) { - if let Some(text) = item.telemetry_event_text(cx) { - telemetry::event!(text); - } - pane.update(cx, |pane, cx| { pane.add_item( item, From 30c4434b702f066abb392b0115e55f98dca9e830 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 17 Oct 2025 15:28:43 -0700 Subject: [PATCH 015/202] Ignore flaky ignored_dirs_events test (#40546) Release Notes: - N/A --- crates/project/src/project_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 626a2793d78a578931094a39b2ecb2f6c3df3489..24612d974d43fa2d8b9ad7bf188c7e3b51726f25 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -8846,7 +8846,7 @@ async fn test_file_status(cx: &mut gpui::TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] +#[ignore] async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { init_test(cx); cx.executor().allow_parking(); From 1e69e5d84411d406efb0b4d88d524765cb6af1b2 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 17 Oct 2025 18:32:52 -0400 Subject: [PATCH 016/202] Set the minimum log level to `info` for the remote server (#40543) `env_logger` defaults to only showing error-level logs, but we show info-level logs and above for the main Zed process, so I think it makes sense for the remote server to behave the same way. Release Notes: - N/A --- crates/remote_server/src/unix.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index ce2cb95d2f30fe2117746d70acfee2c84c74fedb..3cfb73adbb82af2d83182400d725d859ebad29f8 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -103,7 +103,9 @@ fn init_logging_server(log_file_path: PathBuf) -> Result>> { buffer: Vec::new(), }); - env_logger::Builder::from_default_env() + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .parse_default_env() .target(env_logger::Target::Pipe(target)) .format(|buf, record| { let mut log_record = LogRecord::new(record); From 63e719fadf45755b623eb96c8796eaf195f51b37 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 17 Oct 2025 16:33:08 -0600 Subject: [PATCH 017/202] Disallow rename/copy/delete on unshared files (#40540) Release Notes: - Disallow rename/delete/copy on unshared files Co-Authored-By: Cole --- crates/project/src/worktree_store.rs | 33 ++++++++++++++++++++++- crates/project_panel/src/project_panel.rs | 8 +++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 5da8fe2d0070e77c2294465bde4423a13f8ec9ec..670b405ed33757117ec62bfbbb4c947f79e5026a 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -5,7 +5,7 @@ use std::{ sync::{Arc, atomic::AtomicUsize}, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context as _, Result, anyhow, bail}; use collections::{HashMap, HashSet}; use fs::{Fs, copy_recursive}; use futures::{ @@ -1203,6 +1203,16 @@ impl WorktreeStore { RelPath::from_proto(&envelope.payload.new_path)?, ); let (scan_id, entry) = this.update(&mut cx, |this, cx| { + let Some((_, project_id)) = this.downstream_client else { + bail!("no downstream client") + }; + let Some(entry) = this.entry_for_id(entry_id, cx) else { + bail!("no such entry"); + }; + if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID { + bail!("entry is private") + } + let new_worktree = this .worktree_for_id(new_worktree_id, cx) .context("no such worktree")?; @@ -1226,6 +1236,15 @@ impl WorktreeStore { ) -> Result { let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); let worktree = this.update(&mut cx, |this, cx| { + let Some((_, project_id)) = this.downstream_client else { + bail!("no downstream client") + }; + let Some(entry) = this.entry_for_id(entry_id, cx) else { + bail!("no entry") + }; + if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID { + bail!("entry is private") + } this.worktree_for_entry(entry_id, cx) .context("worktree not found") })??; @@ -1246,6 +1265,18 @@ impl WorktreeStore { let worktree = this .worktree_for_entry(entry_id, cx) .context("no such worktree")?; + + let Some((_, project_id)) = this.downstream_client else { + bail!("no downstream client") + }; + let entry = worktree + .read(cx) + .entry_for_id(entry_id) + .ok_or_else(|| anyhow!("missing entry"))?; + if entry.is_private && project_id != REMOTE_SERVER_PROJECT_ID { + bail!("entry is private") + } + let scan_id = worktree.read(cx).scan_id(); anyhow::Ok(( scan_id, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 17c09dcb06e6f665eb589fc4b61b5bc73a0c2982..0160d4d151d5bd102e72e195127fd6a16b63ce15 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -64,7 +64,7 @@ use workspace::{ DraggedSelection, OpenInTerminal, OpenOptions, OpenVisible, PreviewTabsSettings, SelectedEntry, SplitDirection, Workspace, dock::{DockPosition, Panel, PanelEvent}, - notifications::{DetachAndPromptErr, NotifyTaskExt}, + notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt}, }; use worktree::CreatedEntry; use zed_actions::workspace::OpenWithSystem; @@ -2677,12 +2677,14 @@ impl ProjectPanel { for task in paste_tasks { match task { PasteTask::Rename(task) => { - if let Some(CreatedEntry::Included(entry)) = task.await.log_err() { + if let Some(CreatedEntry::Included(entry)) = + task.await.notify_async_err(cx) + { last_succeed = Some(entry); } } PasteTask::Copy(task) => { - if let Some(Some(entry)) = task.await.log_err() { + if let Some(Some(entry)) = task.await.notify_async_err(cx) { last_succeed = Some(entry); } } From 287314f415742c6861dfff5425072e34e7a578a7 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Sat, 18 Oct 2025 00:43:44 +0200 Subject: [PATCH 018/202] markdown_preview: Improve the link decoration logic (#39905) Closes #39838 Refs: * https://github.com/zed-industries/zed/pull/39149#issuecomment-3383015060 # How After digging a bit more to find out why raw links are not colored in Markdown renderer I have found a simpler approach to applying color decoration, which also fixed the lack of colors on raw links mentioned in issue and comment above. Release Notes: - Improved decoration logic for links in Markdown # Preview Screenshot 2025-10-09 at 23 39 09 --- crates/markdown_preview/src/markdown_elements.rs | 10 ++-------- crates/markdown_preview/src/markdown_renderer.rs | 10 +++++++++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index 827c11f0453817b00431a1f32db8c645aced4e86..d0bde48889143b8ab9f66d9dc2839ebabf7d3541 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -1,5 +1,5 @@ use gpui::{ - DefiniteLength, FontStyle, FontWeight, HighlightStyle, Hsla, SharedString, StrikethroughStyle, + DefiniteLength, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle, px, }; use language::HighlightId; @@ -175,11 +175,7 @@ pub enum MarkdownHighlight { impl MarkdownHighlight { /// Converts this [`MarkdownHighlight`] to a [`HighlightStyle`]. - pub fn to_highlight_style( - &self, - theme: &theme::SyntaxTheme, - link_color: Hsla, - ) -> Option { + pub fn to_highlight_style(&self, theme: &theme::SyntaxTheme) -> Option { match self { MarkdownHighlight::Style(style) => { let mut highlight = HighlightStyle::default(); @@ -209,10 +205,8 @@ impl MarkdownHighlight { if style.link { highlight.underline = Some(UnderlineStyle { thickness: px(1.), - color: Some(link_color), ..Default::default() }); - highlight.color = Some(link_color); } Some(highlight) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 489ce532f0060d29436d008413be1391044ab3e3..6f794b1358a1869779b01f6af3069bf8be735e7e 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -692,7 +692,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) let highlights = gpui::combine_highlights( parsed.highlights.iter().filter_map(|(range, highlight)| { highlight - .to_highlight_style(&syntax_theme, link_color) + .to_highlight_style(&syntax_theme) .map(|style| (range.clone(), style)) }), parsed.regions.iter().zip(&parsed.region_ranges).filter_map( @@ -705,6 +705,14 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) ..Default::default() }, )) + } else if region.link.is_some() { + Some(( + range.clone(), + HighlightStyle { + color: Some(link_color), + ..Default::default() + }, + )) } else { None } From 9984614f3d55f623e5422772be0a0b749515fa9e Mon Sep 17 00:00:00 2001 From: David <124755024+44David@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:45:44 -0600 Subject: [PATCH 019/202] settings_ui: Fix misplaced comma in autoclose setting description (#40519) Release Notes: - Fixed misplaced comma in the autoclose description from: "when you type (, Zed will ...)" to "when you type, (Zed will ...)" --------- Co-authored-by: Danilo Leal --- assets/settings/default.json | 4 ++-- crates/settings/src/settings_content/language.rs | 4 ++-- crates/settings_ui/src/page_data.rs | 4 ++-- docs/src/configuring-zed.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index d4a23586d82fac46a2ec78ef5cfeb53de2e589ce..3fb8ea88b3f43fdb341cd68dc02e6b98aeb1bdf0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -311,11 +311,11 @@ "use_on_type_format": true, // Whether to automatically add matching closing characters when typing // opening parenthesis, bracket, brace, single or double quote characters. - // For example, when you type (, Zed will add a closing ) at the correct position. + // For example, when you type '(', Zed will add a closing ) at the correct position. "use_autoclose": true, // Whether to automatically surround selected text when typing opening parenthesis, // bracket, brace, single or double quote characters. - // For example, when you select text and type (, Zed will surround the text with (). + // For example, when you select text and type '(', Zed will surround the text with (). "use_auto_surround": true, // Whether indentation should be adjusted based on the context whilst typing. "auto_indent": true, diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index 5624147369a19283f03190696f51f1393e410ed8..a5dbd682d2ca4943e6230789acad96c5d7e2a742 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -296,12 +296,12 @@ pub struct LanguageSettingsContent { /// Inlay hint related settings. pub inlay_hints: Option, /// Whether to automatically type closing characters for you. For example, - /// when you type (, Zed will automatically add a closing ) at the correct position. + /// when you type '(', Zed will automatically add a closing ')' at the correct position. /// /// Default: true pub use_autoclose: Option, /// Whether to automatically surround text with characters for you. For example, - /// when you select text and type (, Zed will automatically surround text with (). + /// when you select text and type '(', Zed will automatically surround text with (). /// /// Default: true pub use_auto_surround: Option, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 26447445f99145afc1f9103726e9e99d87e8aca0..6683f42daee9832f493878dc3c8721ff3bc9d268 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -5430,7 +5430,7 @@ fn language_settings_data() -> Vec { SettingsPageItem::SectionHeader("Autoclose"), SettingsPageItem::SettingItem(SettingItem { title: "Use Autoclose", - description: "Whether to automatically type closing characters for you. For example, when you type (, Zed will automatically add a closing ) at the correct position", + description: "Whether to automatically type closing characters for you. For example, when you type '(', Zed will automatically add a closing ')' at the correct position", field: Box::new(SettingField { pick: |settings_content| { language_settings_field(settings_content, |language| { @@ -5448,7 +5448,7 @@ fn language_settings_data() -> Vec { }), SettingsPageItem::SettingItem(SettingItem { title: "Use Auto Surround", - description: "Whether to automatically surround text with characters for you. For example, when you select text and type (, Zed will automatically surround text with ()", + description: "Whether to automatically surround text with characters for you. For example, when you select text and type '(', Zed will automatically surround text with ()", field: Box::new(SettingField { pick: |settings_content| { language_settings_field(settings_content, |language| { diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 14c2f724f0367c42e8e0346c9251d27b92bf7a0a..4fb65d14118d637d7123998e53da9aa40dc7a84c 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3326,7 +3326,7 @@ Positive integer values ## Use Auto Surround -- Description: Whether to automatically surround selected text when typing opening parenthesis, bracket, brace, single or double quote characters. For example, when you select text and type (, Zed will surround the text with (). +- Description: Whether to automatically surround selected text when typing opening parenthesis, bracket, brace, single or double quote characters. For example, when you select text and type '(', Zed will surround the text with (). - Setting: `use_auto_surround` - Default: `true` From 47c6ae7b1f38d4ed0f76157dec1ce922e1a15b5d Mon Sep 17 00:00:00 2001 From: Delvin <64721581+delvin02@users.noreply.github.com> Date: Sat, 18 Oct 2025 09:42:05 +1030 Subject: [PATCH 020/202] settings_ui: Fix stepper buttons to Inactive Opacity to 0.1 increment adjustments (#40477) Closes #40279 Release Notes: - Fix stepper buttons (+/-) to the Inactive Opacity setting for 0.1 increment adjustments on settings UI --------- Co-authored-by: Danilo Leal --- .../settings/src/settings_content/editor.rs | 25 ++++++++++++++++++ .../src/settings_content/workspace.rs | 5 ++-- crates/settings/src/vscode_import.rs | 2 +- crates/settings_ui/src/settings_ui.rs | 1 + crates/ui_input/src/number_field.rs | 26 ++++++++++++++++++- crates/workspace/src/pane_group.rs | 2 +- crates/workspace/src/workspace_settings.rs | 4 +-- 7 files changed, 58 insertions(+), 7 deletions(-) diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index 171cb874fcdea23bd51ffa453b30c6d96587fe6a..7bc447346c8a20e53640928a6ba0ca28e10d92e7 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -803,3 +803,28 @@ impl Display for MinimumContrast { write!(f, "{:.1}", self.0) } } + +/// Opacity of the inactive panes. 0 means transparent, 1 means opaque. +/// +/// Valid range: 0.0 to 1.0 +/// Default: 1.0 +#[derive( + Clone, + Copy, + Debug, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + PartialOrd, + derive_more::FromStr, +)] +#[serde(transparent)] +pub struct InactiveOpacity(pub f32); + +impl Display for InactiveOpacity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.1}", self.0) + } +} diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 21b25d6d3c47fb76e4de638519a145b5d916e555..7ebb468f79bf195fb9d97b8d52dd6e728d8c8f99 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use settings_macros::MergeFrom; -use crate::{DockPosition, DockSide, ScrollbarSettingsContent, ShowIndentGuides}; +use crate::{DockPosition, DockSide, InactiveOpacity, ScrollbarSettingsContent, ShowIndentGuides}; #[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] @@ -256,7 +256,8 @@ pub struct ActivePaneModifiers { /// Values are clamped to the [0.0, 1.0] range. /// /// Default: `1.0` - pub inactive_opacity: Option, + #[schemars(range(min = 0.0, max = 1.0))] + pub inactive_opacity: Option, } #[derive( diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index cc55613c63ef7d21b5f4830b0f5c6496ac1930f2..e1cce43f6227bb26a36be871c31cf1143aab5c70 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -843,7 +843,7 @@ impl VsCodeSettings { { Some(ActivePaneModifiers { border_size: None, - inactive_opacity: Some(opacity), + inactive_opacity: Some(InactiveOpacity(opacity)), }) } else { None diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 2cd18d0a3da34f5fb6f42b84f14972d1eb9e0503..7856164a12a34643b9a223023a8ab27922c68906 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -418,6 +418,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) + .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) diff --git a/crates/ui_input/src/number_field.rs b/crates/ui_input/src/number_field.rs index 37ef4db53b2620e217618413f95a92f892fe4993..b72566947771be6411ce879a48092f508909b18f 100644 --- a/crates/ui_input/src/number_field.rs +++ b/crates/ui_input/src/number_field.rs @@ -8,7 +8,7 @@ use std::{ use editor::{Editor, EditorStyle}; use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers}; -use settings::{CodeFade, MinimumContrast}; +use settings::{CodeFade, InactiveOpacity, MinimumContrast}; use ui::prelude::*; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -79,6 +79,30 @@ impl NumberFieldType for settings::CodeFade { } } +impl NumberFieldType for settings::InactiveOpacity { + fn default_step() -> Self { + InactiveOpacity(0.10) + } + fn large_step() -> Self { + InactiveOpacity(0.20) + } + fn small_step() -> Self { + InactiveOpacity(0.05) + } + fn min_value() -> Self { + InactiveOpacity(0.0) + } + fn max_value() -> Self { + InactiveOpacity(1.0) + } + fn saturating_add(self, rhs: Self) -> Self { + InactiveOpacity((self.0 + rhs.0).min(Self::max_value().0)) + } + fn saturating_sub(self, rhs: Self) -> Self { + InactiveOpacity((self.0 - rhs.0).max(Self::min_value().0)) + } +} + impl NumberFieldType for settings::MinimumContrast { fn default_step() -> Self { MinimumContrast(1.0) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 88ec56de6fa2e890d11afbda58cc3a69ab1d6a60..36898b127bdd749a9c1867a97bd72dfd6f4e15ea 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1306,7 +1306,7 @@ mod element { let overlay_opacity = WorkspaceSettings::get(None, cx) .active_pane_modifiers .inactive_opacity - .map(|val| val.clamp(0.0, 1.0)) + .map(|val| val.0.clamp(0.0, 1.0)) .and_then(|val| (val <= 1.).then_some(val)); let mut overlay_background = cx.theme().colors().editor_background; diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index ffa6767427d150d59b7f9f66575e3e385cca9564..6ed2aeb5cb85ae5728f683b3c3d7dfbdf86f0c6a 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -4,11 +4,11 @@ use crate::DockPosition; use collections::HashMap; use serde::Deserialize; pub use settings::AutosaveSetting; -use settings::Settings; pub use settings::{ BottomDockLayout, PaneSplitDirectionHorizontal, PaneSplitDirectionVertical, RestoreOnStartupBehavior, }; +use settings::{InactiveOpacity, Settings}; pub struct WorkspaceSettings { pub active_pane_modifiers: ActivePanelModifiers, @@ -50,7 +50,7 @@ pub struct ActivePanelModifiers { /// /// Default: `1.0` // TODO: make this not an option, it is never None - pub inactive_opacity: Option, + pub inactive_opacity: Option, } #[derive(Deserialize)] From 3d6722be9abf94af5680a3323b69a452154c55b2 Mon Sep 17 00:00:00 2001 From: joel <62033695+jvb0@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:27:45 -0700 Subject: [PATCH 021/202] Fix Right Alt key not working in keybindings on Windows (#40536) ### Problem On Windows, the right Alt key was not working in keybindings (e.g., `Ctrl+Right Alt+B`), while the left Alt key worked correctly. This was due to overly aggressive AltGr detection that treated any `right Alt + left Ctrl` combination as AltGr, even on US keyboards where AltGr doesn't exist. ### Root Cause Windows internally represents AltGr (Alt Graph) as `right Alt + left Ctrl` pressed simultaneously. The previous implementation always excluded this combination from being treated as regular modifier keys to support international keyboards. However, this broke keybindings using right Alt on US/UK keyboards where users expect right Alt to behave identically to left Alt. ### Solution Implemented keyboard layout-aware AltGr detection: 1. Added `uses_altgr()` method to `WindowsKeyboardLayout` that checks if the current keyboard layout is known to use AltGr (German, French, Spanish, Polish, etc.) 2. Modified `current_modifiers()` to only apply AltGr special handling when the keyboard layout actually uses it 3. Added explicit checking for both `VK_LMENU` and `VK_RMENU` instead of relying solely on the generic `VK_MENU` ### Behavior - **US/UK keyboards**: Right Alt now works identically to left Alt in keybindings. `Ctrl+Right Alt+B` triggers the same action as `Ctrl+Left Alt+B` - **International keyboards** (German, French, Spanish, etc.): AltGr continues to work correctly for typing special characters and doesn't trigger keybindings - **All keyboards**: Both Alt keys are detected symmetrically, matching the behavior of left/right Windows keys ### Testing Manually tested on Windows with US keyboard layout: - `Ctrl+Left Alt+B` triggers keybinding - `Ctrl+Right Alt+B` triggers keybinding - Both Alt keys work independently in keybindings Release Notes: - Fixed Right Alt key not working in keybindings on Windows --- crates/gpui/src/platform/windows/events.rs | 20 +++++++++--- crates/gpui/src/platform/windows/keyboard.rs | 32 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index ee2bdf9abf175eb6f3fec58878aef0fbc5896a00..9c10dcec4bb629bfbc78b76e74db099ed605d8be 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -1307,10 +1307,10 @@ where F: FnOnce(Keystroke) -> PlatformInput, { let virtual_key = VIRTUAL_KEY(wparam.loword()); - let mut modifiers = current_modifiers(); + let modifiers = current_modifiers(); match virtual_key { - VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => { + VK_SHIFT | VK_CONTROL | VK_MENU | VK_LMENU | VK_RMENU | VK_LWIN | VK_RWIN => { if state .last_reported_modifiers .is_some_and(|prev_modifiers| prev_modifiers == modifiers) @@ -1460,13 +1460,25 @@ fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool { unsafe { GetKeyState(vkey.0 as i32) < 0 } } +fn keyboard_uses_altgr() -> bool { + use crate::platform::windows::keyboard::WindowsKeyboardLayout; + WindowsKeyboardLayout::new() + .map(|layout| layout.uses_altgr()) + .unwrap_or(false) +} + #[inline] pub(crate) fn current_modifiers() -> Modifiers { - let altgr = is_virtual_key_pressed(VK_RMENU) && is_virtual_key_pressed(VK_LCONTROL); + let lmenu_pressed = is_virtual_key_pressed(VK_LMENU); + let rmenu_pressed = is_virtual_key_pressed(VK_RMENU); + let lcontrol_pressed = is_virtual_key_pressed(VK_LCONTROL); + + // Only treat right Alt + left Ctrl as AltGr on keyboards that actually use it + let altgr = keyboard_uses_altgr() && rmenu_pressed && lcontrol_pressed; Modifiers { control: is_virtual_key_pressed(VK_CONTROL) && !altgr, - alt: is_virtual_key_pressed(VK_MENU) && !altgr, + alt: (lmenu_pressed || rmenu_pressed) && !altgr, shift: is_virtual_key_pressed(VK_SHIFT), platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN), function: false, diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 259ebaebff794d4ed7203420c8c66188998c5fa4..7a8478d5910d35fb98a913ed799f2fa1447e9a65 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -110,6 +110,38 @@ impl WindowsKeyboardLayout { name: "unknown".to_string(), } } + + pub(crate) fn uses_altgr(&self) -> bool { + // Check if this is a known AltGr layout by examining the layout ID + // The layout ID is a hex string like "00000409" (US) or "00000407" (German) + // Extract the language ID (last 4 bytes) + let id_bytes = self.id.as_bytes(); + if id_bytes.len() >= 4 { + let lang_id = &id_bytes[id_bytes.len() - 4..]; + // List of keyboard layouts that use AltGr (non-exhaustive) + matches!( + lang_id, + b"0407" | // German + b"040C" | // French + b"040A" | // Spanish + b"0415" | // Polish + b"0413" | // Dutch + b"0816" | // Portuguese + b"041D" | // Swedish + b"0414" | // Norwegian + b"040B" | // Finnish + b"041F" | // Turkish + b"0419" | // Russian + b"0405" | // Czech + b"040E" | // Hungarian + b"0424" | // Slovenian + b"041B" | // Slovak + b"0418" // Romanian + ) + } else { + false + } + } } impl WindowsKeyboardMapper { From e702df21a4dea701dcf33657680a8c191683f10a Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Fri, 17 Oct 2025 18:33:00 -0700 Subject: [PATCH 022/202] Fix Rust macro_invocation injections (#40534) Closes #40317 Release Notes: - N/A --- crates/languages/src/rust/injections.scm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/rust/injections.scm b/crates/languages/src/rust/injections.scm index e5921501bc613e8adae652e41b4b621b932281d1..91c092b353b615c5dff1f7189af816c9205cbf21 100644 --- a/crates/languages/src/rust/injections.scm +++ b/crates/languages/src/rust/injections.scm @@ -2,7 +2,7 @@ (#set! injection.language "comment")) (macro_invocation - macro: (identifier) @_macro_name + macro: [(identifier) (scoped_identifier)] @_macro_name (#not-any-of? @_macro_name "view" "html") (token_tree) @injection.content (#set! injection.language "rust")) @@ -11,7 +11,7 @@ ; it wants to inject inside of rust, instead of modifying the rust ; injections to support leptos injections (macro_invocation - macro: (identifier) @_macro_name + macro: [(identifier) (scoped_identifier)] @_macro_name (#any-of? @_macro_name "view" "html") (token_tree) @injection.content (#set! injection.language "rstml") From 219ae05d1f37a628d0dcd11296497510b2079430 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 18 Oct 2025 01:09:27 -0400 Subject: [PATCH 023/202] Add a doc on crafting release notes (#40557) Release Notes: - N/A --- docs/src/SUMMARY.md | 1 + docs/src/development/release-notes.md | 29 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 docs/src/development/release-notes.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index eb542497238780d93974cbc2627ce3466e23049b..086f8394d639994b808c4e390653f65bf9978d7a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -160,4 +160,5 @@ - [Using Debuggers](./development/debuggers.md) - [Glossary](./development/glossary.md) - [Release Process](./development/releases.md) +- [Release Notes](./development/release-notes.md) - [Debugging Crashes](./development/debugging-crashes.md) diff --git a/docs/src/development/release-notes.md b/docs/src/development/release-notes.md new file mode 100644 index 0000000000000000000000000000000000000000..5005fc32d36bafb57754e45423b45fc8b7bf64d9 --- /dev/null +++ b/docs/src/development/release-notes.md @@ -0,0 +1,29 @@ +# Release Notes + +Whenever you open a pull request, the body is automatically populated based on this [pull request template](https://github.com/zed-industries/zed/blob/main/.github/pull_request_template.md). + +```md +... + +Release Notes: + +- N/A _or_ Added/Fixed/Improved ... +``` + +On Wednesdays, we run a [`get-preview-channel-changes`](https://github.com/zed-industries/zed/blob/main/script/get-preview-channel-changes) script that scrapes `Release Notes` lines from pull requests landing in preview, as documented in our [Release](https://zed.dev/docs/development/releases) docs. + +The script outputs everything below the `Release Notes` line, including additional data such as the pull request author (if not a Zed team member) and a link to the pull request. +If you use `N/A`, the script skips your pull request entirely. + +## Guidelines for crafting your `Release Notes` line(s) + +- A `Release Notes` line should only be written if the user can see or feel the difference in Zed. +- A `Release Notes` line should be written such that a Zed user can understand what the change is. + Don't assume a user knows technical editor developer lingo; phrase your change in language they understand as a user of a text editor. +- If you want to include technical details about your pull request for other team members to see, do so above the `Release Notes` line. +- Changes to docs should be labeled as `N/A`. +- If your pull request adds/changes a setting or a keybinding, always mention that setting or keybinding. + Don't make the user dig into docs or the pull request to find this information (although it should be included in docs as well). +- For pull requests that are reverts: + - If the item being reverted **has already been shipped**, include a `Release Notes` line explaining why we reverted, as this is a breaking change. + - If the item being reverted **hasn't been shipped**, edit the original PR's `Release Notes` line to be `N/A`; otherwise, it will be included and the compiler of the release notes may not know to skip it, leading to a potentially-awkward situation where we are stating we shipped something we actually didn't. From 35664461c604d131ee254beb8611df57ff15a40a Mon Sep 17 00:00:00 2001 From: Bedis Nbiba Date: Sat, 18 Oct 2025 11:08:00 +0100 Subject: [PATCH 024/202] docs: Add `deno.jsonc` to JSON LSP settings (#40563) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- docs/src/languages/deno.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/languages/deno.md b/docs/src/languages/deno.md index a5aea4c07c047fde40d9537899bf3e35aeecd87c..a4192257765d6aa131232ff8a80a3af452a38d57 100644 --- a/docs/src/languages/deno.md +++ b/docs/src/languages/deno.md @@ -69,7 +69,8 @@ To get completions for `deno.json` or `package.json` you can add the following t "schemas": [ { "fileMatch": [ - "deno.json" + "deno.json", + "deno.jsonc" ], "url": "https://raw.githubusercontent.com/denoland/deno/refs/heads/main/cli/schemas/config-file.v1.json" }, From 89be2635d4c89b7c1c1ead4578e9bd3e00a76726 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sat, 18 Oct 2025 05:44:41 -0700 Subject: [PATCH 025/202] project_panel: Fix double-click on blank area to create a new file (#40503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regressed in https://github.com/zed-industries/zed/pull/38008 Release Notes: - Fixed an issue where double-clicking empty space in the project panel wouldn’t create a new file. --- crates/project_panel/src/project_panel.rs | 51 +++++++++++++---------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0160d4d151d5bd102e72e195127fd6a16b63ce15..d9decd954b8f15abd3d5126b3e8f475013a9b895 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5463,28 +5463,6 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) - .on_click(cx.listener(|this, event: &gpui::ClickEvent, window, cx| { - if event.click_count() > 1 - && let Some(entry_id) = this.state.last_worktree_root_id - { - let project = this.project.read(cx); - - let worktree_id = if let Some(worktree) = - project.worktree_for_entry(entry_id, cx) - { - worktree.read(cx).id() - } else { - return; - }; - - this.state.selection = Some(SelectedEntry { - worktree_id, - entry_id, - }); - - this.new_file(&NewFile, window, cx); - } - })) }) .when(project.is_local(), |el| { el.on_action(cx.listener(Self::reveal_in_finder)) @@ -5819,7 +5797,34 @@ impl Render for ProjectPanel { ); } }), - ), + ) + .when(!project.is_read_only(cx), |el| { + el.on_click(cx.listener( + |this, event: &gpui::ClickEvent, window, cx| { + if event.click_count() > 1 + && let Some(entry_id) = + this.state.last_worktree_root_id + { + let project = this.project.read(cx); + + let worktree_id = if let Some(worktree) = + project.worktree_for_entry(entry_id, cx) + { + worktree.read(cx).id() + } else { + return; + }; + + this.state.selection = Some(SelectedEntry { + worktree_id, + entry_id, + }); + + this.new_file(&NewFile, window, cx); + } + }, + )) + }), ) .size_full(), ) From 41994452f2726be1912fd1d7df7de26ffdce479d Mon Sep 17 00:00:00 2001 From: Andrew Farkas <6060305+HactarCE@users.noreply.github.com> Date: Sat, 18 Oct 2025 10:57:59 -0400 Subject: [PATCH 026/202] Fix `extension` keymap context for single file worktree (#40425) Closes #40353 Release Notes: - Fixed `extension` in keymap context being empty for single file worktree Co-authored-by: Cole Miller --- crates/editor/src/editor.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cc02740900770e4bee42ad49361f65b7dda98568..2ff8b585c3f228f70246947bed8c2e6da221bd37 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2523,12 +2523,15 @@ impl Editor { } if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() { - if let Some(extension) = singleton_buffer - .read(cx) - .file() - .and_then(|file| file.path().extension()) - { - key_context.set("extension", extension.to_string()); + if let Some(extension) = singleton_buffer.read(cx).file().and_then(|file| { + Some( + file.full_path(cx) + .extension()? + .to_string_lossy() + .into_owned(), + ) + }) { + key_context.set("extension", extension); } } else { key_context.add("multibuffer"); From 8d48f9cdae65116145535d730ca129b4ed880819 Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Sat, 18 Oct 2025 23:04:40 +0800 Subject: [PATCH 027/202] gpui: Simplify tab group lookup logic in SystemWindowTabController (#40466) Refactor the find_tab_group method to use the question mark operator for cleaner error handling, replacing the explicit if-else pattern with a more concise chained approach. Release Notes: - N/A Signed-off-by: Xiaobo Liu --- crates/gpui/src/app.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 07ff04e32abc19dbe681ab6214d06469fe7917ff..789652b331cd64f0d5400830fa19e5e50fc05b50 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -344,13 +344,9 @@ impl SystemWindowTabController { let tab_group = self .tab_groups .iter() - .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group)); + .find_map(|(group, tabs)| tabs.iter().find(|tab| tab.id == id).map(|_| *group))?; - if let Some(tab_group) = tab_group { - self.tab_groups.get(&tab_group) - } else { - None - } + self.tab_groups.get(&tab_group) } /// Initialize the visibility of the system window tab controller. From 02b15f006207454a11d44c57e536251f36594073 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 18 Oct 2025 19:53:19 +0300 Subject: [PATCH 028/202] Add Windows path into custom theme docs (#40599) Closes https://github.com/zed-industries/zed/issues/40584 Closes https://github.com/zed-industries/zed/issues/40057 Release Notes: - N/A --- docs/src/themes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/themes.md b/docs/src/themes.md index c0706cbf31e39f057c47dec55365113d8ce6009c..438301dc13fe04b8b75ba0df348cdf499c49c329 100644 --- a/docs/src/themes.md +++ b/docs/src/themes.md @@ -58,7 +58,7 @@ To see a list of available theme attributes look at the JSON file for your theme ## Local Themes -Store new themes locally by placing them in the `~/.config/zed/themes` directory. +Store new themes locally by placing them in the `~/.config/zed/themes` directory (macOS and Linux) or `%USERPROFILE%\AppData\Roaming\Zed\themes\` (Windows). For example, to create a new theme called `my-cool-theme`, create a file called `my-cool-theme.json` in that directory. It will be available in the theme selector the next time Zed loads. From 3aee14378dc9d2fe3387cbb550c55cbb7363a859 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 18 Oct 2025 18:52:11 -0400 Subject: [PATCH 029/202] python: Only enable basedpyright and ruff by default (#40604) Though we ship with `basedpyright`, `ruff` and a few other laps for python, we run them all at once. Release Notes: - Only enable `basedpyright` and `ruff` by default when opening Python files. If you prefer one of the other. --- assets/settings/default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3fb8ea88b3f43fdb341cd68dc02e6b98aeb1bdf0..5ffd78c022530c8cd9801305f87d1378ad21d52d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1776,7 +1776,8 @@ "name": "ruff" } }, - "debuggers": ["Debugpy"] + "debuggers": ["Debugpy"], + "language_servers": ["basedpyright", "ruff", "!ty", "!pyrefly", "!pyright", "!pylsp", "..."] }, "Ruby": { "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] From 6d975984eed02e87c6d202351a1fd4adf583603f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 18 Oct 2025 21:47:17 -0400 Subject: [PATCH 030/202] Register rules files as Markdown (#40614) Release Notes: - `.rules`, `.cursorrules`, `.windsurfrules`, and `.clinerules` are now syntax highlighted as Markdown files. --- assets/settings/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5ffd78c022530c8cd9801305f87d1378ad21d52d..7433a49ff0fd107fcf02bb691da05319865a56ee 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1559,6 +1559,7 @@ // "file_types": { "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json", "tsconfig*.json"], + "Markdown": [".rules", ".cursorrules", ".windsurfrules", ".clinerules"], "Shell Script": [".env.*"] }, // Settings for which version of Node.js and NPM to use when installing From 197d24437809a7d5bbd214f79e11b850367e35e2 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Sun, 19 Oct 2025 07:00:28 +0200 Subject: [PATCH 031/202] image_viewer: Use buffer font in breadcrumbs (#40601) # Why Spotted that image path in editor breadcrumb uses regular (UI) font in comparison to paths of any other code-related files. Screenshot 2025-10-18 at 19 32 55 # How Use buffer font for image path in Image Viewer breadcrumbs. Release Notes: - Aligned appearance of path displayed by Image Viewer breadcrumbs with other panes. # Preview ### Before Screenshot 2025-10-18 at 19 26 17 ### After Screenshot 2025-10-18 at 19 24 17 --- crates/image_viewer/src/image_viewer.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 47a20ad58b7618193e29aecce194b2e85f11cf74..ff68de39e87c7246e0aa07e96a4d0d7e3c2ad523 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -13,7 +13,7 @@ use language::{DiskState, File as _}; use persistence::IMAGE_VIEWER; use project::{ImageItem, Project, ProjectPath, image_store::ImageItemEvent}; use settings::Settings; -use theme::Theme; +use theme::{Theme, ThemeSettings}; use ui::prelude::*; use util::paths::PathExt; use workspace::{ @@ -162,10 +162,12 @@ impl Item for ImageView { fn breadcrumbs(&self, _theme: &Theme, cx: &App) -> Option> { let text = breadcrumbs_text_for_image(self.project.read(cx), self.image_item.read(cx), cx); + let settings = ThemeSettings::get_global(cx); + Some(vec![BreadcrumbText { text, highlights: None, - font: None, + font: Some(settings.buffer_font.clone()), }]) } From 57387812dc24f4ea8a701a86f9b0a659c5d2a4bd Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Sun, 19 Oct 2025 15:26:41 +0100 Subject: [PATCH 032/202] terminal_ui: Terminal failed to spawn UI (#40246) Co-authored-by: Piotr piotr@zed.dev Co-authored-by: Lukas lukas@zed.dev Co-authored-by: Lukas Wirth Co-authored-by: Gaauwe Rombouts Co-authored-by: Danilo Leal --- crates/terminal_view/src/terminal_panel.rs | 219 +++++++++++++++++---- 1 file changed, 181 insertions(+), 38 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index bd718d1432cfeb178355663bc657fd53fc37d57b..3f0ea0e274edce225d8fd4754043b49d76bf05b4 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -22,8 +22,8 @@ use settings::{Settings, TerminalDockPosition}; use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId}; use terminal::{Terminal, terminal_settings::TerminalSettings}; use ui::{ - ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable, Tooltip, - prelude::*, + ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable, + Tooltip, prelude::*, }; use util::{ResultExt, TryFutureExt}; use workspace::{ @@ -35,7 +35,6 @@ use workspace::{ dock::{DockPosition, Panel, PanelEvent, PanelHandle}, item::SerializableItem, move_active_item, move_item, pane, - ui::IconName, }; use anyhow::{Result, anyhow}; @@ -813,6 +812,7 @@ impl TerminalPanel { cx: &mut Context, ) -> Task>> { let workspace = self.workspace.clone(); + cx.spawn_in(window, async move |terminal_panel, cx| { if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { anyhow::bail!("terminal not yet supported for collaborative projects"); @@ -824,43 +824,59 @@ impl TerminalPanel { let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; let terminal = project .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? - .await?; - let result = workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - })); + .await; - match reveal_strategy { - RevealStrategy::Always => { - workspace.focus_panel::(window, cx); - } - RevealStrategy::NoFocus => { - workspace.open_panel::(window, cx); - } - RevealStrategy::Never => {} - } + match terminal { + Ok(terminal) => { + let result = workspace.update_in(cx, |workspace, window, cx| { + let terminal_view = Box::new(cx.new(|cx| { + TerminalView::new( + terminal.clone(), + workspace.weak_handle(), + workspace.database_id(), + workspace.project().downgrade(), + window, + cx, + ) + })); - pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); - pane.add_item(terminal_view, true, focus, None, window, cx); - }); + match reveal_strategy { + RevealStrategy::Always => { + workspace.focus_panel::(window, cx); + } + RevealStrategy::NoFocus => { + workspace.open_panel::(window, cx); + } + RevealStrategy::Never => {} + } - Ok(terminal.downgrade()) - })?; - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.pending_terminals_to_add = - terminal_panel.pending_terminals_to_add.saturating_sub(1); - terminal_panel.serialize(cx) - })?; - result + pane.update(cx, |pane, cx| { + let focus = pane.has_focus(window, cx) + || matches!(reveal_strategy, RevealStrategy::Always); + pane.add_item(terminal_view, true, focus, None, window, cx); + }); + + Ok(terminal.downgrade()) + })?; + terminal_panel.update(cx, |terminal_panel, cx| { + terminal_panel.pending_terminals_to_add = + terminal_panel.pending_terminals_to_add.saturating_sub(1); + terminal_panel.serialize(cx) + })?; + result + } + Err(error) => { + pane.update_in(cx, |pane, window, cx| { + let focus = pane.has_focus(window, cx); + let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal { + error: error.to_string(), + focus_handle: cx.focus_handle(), + }); + pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx); + })?; + Err(error) + } + } }) } @@ -1288,6 +1304,82 @@ fn add_paths_to_terminal( } } +struct FailedToSpawnTerminal { + error: String, + focus_handle: FocusHandle, +} + +impl Focusable for FailedToSpawnTerminal { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for FailedToSpawnTerminal { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let popover_menu = PopoverMenu::new("settings-popover") + .trigger( + IconButton::new("icon-button-popover", IconName::ChevronDown) + .icon_size(IconSize::XSmall), + ) + .menu(move |window, cx| { + Some(ContextMenu::build(window, cx, |context_menu, _, _| { + context_menu + .action("Open Settings", zed_actions::OpenSettings.boxed_clone()) + .action( + "Edit settings.json", + zed_actions::OpenSettingsFile.boxed_clone(), + ) + })) + }) + .anchor(Corner::TopRight) + .offset(gpui::Point { + x: px(0.0), + y: px(2.0), + }); + + v_flex() + .track_focus(&self.focus_handle) + .size_full() + .p_4() + .items_center() + .justify_center() + .bg(cx.theme().colors().editor_background) + .child( + v_flex() + .max_w_112() + .items_center() + .justify_center() + .text_center() + .child(Label::new("Failed to spawn terminal")) + .child( + Label::new(self.error.to_string()) + .size(LabelSize::Small) + .color(Color::Muted) + .mb_4(), + ) + .child(SplitButton::new( + ButtonLike::new("open-settings-ui") + .child(Label::new("Edit Settings").size(LabelSize::Small)) + .on_click(|_, window, cx| { + window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx); + }), + popover_menu.into_any_element(), + )), + ) + } +} + +impl EventEmitter<()> for FailedToSpawnTerminal {} + +impl workspace::Item for FailedToSpawnTerminal { + type Event = (); + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + SharedString::new_static("Failed to spawn terminal") + } +} + impl EventEmitter for TerminalPanel {} impl Render for TerminalPanel { @@ -1657,7 +1749,7 @@ impl Render for InlineAssistTabBarButton { #[cfg(test)] mod tests { use super::*; - use gpui::TestAppContext; + use gpui::{TestAppContext, UpdateGlobal as _}; use pretty_assertions::assert_eq; use project::FakeFs; use settings::SettingsStore; @@ -1775,6 +1867,57 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) { + init_test(cx); + + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.terminal.get_or_insert_default().project.shell = + Some(settings::Shell::Program("asdf".to_owned())); + }); + }); + }); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + + let (window_handle, terminal_panel) = workspace + .update(cx, |workspace, window, cx| { + let window_handle = window.window_handle(); + let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); + (window_handle, terminal_panel) + }) + .unwrap(); + + window_handle + .update(cx, |_, window, cx| { + terminal_panel.update(cx, |terminal_panel, cx| { + terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx) + }) + }) + .unwrap() + .await + .unwrap_err(); + + window_handle + .update(cx, |_, _, cx| { + terminal_panel.update(cx, |terminal_panel, cx| { + assert!( + terminal_panel + .active_pane + .read(cx) + .items() + .any(|item| item.downcast::().is_some()), + "should spawn `FailedToSpawnTerminal` pane" + ); + }) + }) + .unwrap(); + } + pub fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let store = SettingsStore::test(cx); From 5f13ce6d7d948b94cd0aec3a8ccd3aed01cc4ffe Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Sun, 19 Oct 2025 16:30:01 +0200 Subject: [PATCH 033/202] docs: Update section about installing extensions locally (#40636) Release Notes: - N/A --- docs/src/extensions/developing-extensions.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index b624077a7ef99c4ebf4ccd9e9c66382388392b60..5fdff6ff0810f6075d43c91af5a5f22e1d154d21 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -23,11 +23,7 @@ From the extensions page, click the `Install Dev Extension` button (or the {#act If you need to troubleshoot, you can check the Zed.log ({#action zed::OpenLog}) for additional output. For debug output, close and relaunch zed with the `zed --foreground` from the command line which show more verbose INFO level logging. -If you already have a published extension with the same name installed, your dev extension will override it. - -After installing, the `Extensions` page will indicate that the upstream extension is "Overridden by dev extension". - -Pre-installed extensions with the same name have to be uninstalled before installing the dev extension. See [#31106](https://github.com/zed-industries/zed/issues/31106) for more. +If you already have the published version of the extension installed, the published version will be uninstalled prior to the installation of the dev extension. After successful installation, the `Extensions` page will indicate that the upstream extension is "Overridden by dev extension". ## Directory Structure of a Zed Extension From 8f3f7232fb4471dfc91a796873c4e937a3fd15ec Mon Sep 17 00:00:00 2001 From: Xiaobo Liu Date: Sun, 19 Oct 2025 22:52:16 +0800 Subject: [PATCH 034/202] gpui: Add exit in tab title update loop (#40628) Release Notes: - N/A --------- Signed-off-by: Xiaobo Liu --- crates/gpui/src/app.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 789652b331cd64f0d5400830fa19e5e50fc05b50..a5f53c2dad1019ad5ccd3427696a4ceb964c1e61 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -411,7 +411,8 @@ impl SystemWindowTabController { for windows in controller.tab_groups.values_mut() { for tab in windows.iter_mut() { if tab.id == id { - tab.title = title.clone(); + tab.title = title; + return; } } } From 1b43a632dc488a62c0ccc87316b118468f701bb0 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 19 Oct 2025 17:25:13 +0200 Subject: [PATCH 035/202] fs: Fix `RealFs::open_handle` implementation for directories on windows (#40639) Release Notes: - Fixed worktree names not updating when renaming the root folder on windows --- crates/fs/src/fs.rs | 9 ++++++++- crates/worktree/src/worktree.rs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9907d0dcbde489b4f4de57133baf299ad0be14a1..9fe9e53eaacc864ad66248131813e305fd5bc172 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -558,7 +558,14 @@ impl Fs for RealFs { } async fn open_handle(&self, path: &Path) -> Result> { - Ok(Arc::new(std::fs::File::open(path)?)) + let mut options = std::fs::OpenOptions::new(); + options.read(true); + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + options.custom_flags(windows::Win32::Storage::FileSystem::FILE_FLAG_BACKUP_SEMANTICS.0); + } + Ok(Arc::new(options.open(path)?)) } async fn load(&self, path: &Path) -> Result { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 3fa271a37ac014e046b4e5a98ae780d45a66743c..447dc0eeb5fe69aa8a936a5850e788d315a69bac 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -236,7 +236,7 @@ pub struct LocalSnapshot { /// All of the git repositories in the worktree, indexed by the project entry /// id of their parent directory. git_repositories: TreeMap, - /// The file handle of the worktree root. `None` if the worktree is a directory. + /// The file handle of the worktree root /// (so we can find it after it's been moved) root_file_handle: Option>, executor: BackgroundExecutor, @@ -3830,7 +3830,7 @@ impl BackgroundScanner { .unbounded_send(ScanState::RootUpdated { new_path }) .ok(); } else { - log::warn!("root path could not be canonicalized: {}", err); + log::warn!("root path could not be canonicalized: {:#}", err); } return; } From 4507110981cb0b261945cc904d4d41cf82adee2b Mon Sep 17 00:00:00 2001 From: Bennet Fenner Date: Sun, 19 Oct 2025 17:52:28 +0200 Subject: [PATCH 036/202] settings: Remove unused `stream_edits` setting in `agent` (#40640) This setting is unused (we always stream edits) Release Notes: - N/A --- assets/settings/default.json | 2 -- crates/agent_settings/src/agent_settings.rs | 2 -- crates/eval/runner_settings.json | 1 - crates/settings/src/settings_content/agent.rs | 4 ---- 4 files changed, 9 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 7433a49ff0fd107fcf02bb691da05319865a56ee..df1ec6fa3a997bae3367b6af7a00262191960983 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -884,8 +884,6 @@ // Note: This setting has no effect on external agents that support permission modes, such as Claude Code. // You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests. "always_allow_tool_actions": false, - // When enabled, the agent will stream edits. - "stream_edits": false, // When enabled, agent edits will be displayed in single-file editors for review "single_file_review": true, // When enabled, show voting thumbs for feedback on agent edits. diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 988340318c9f6a68d7b36010eecf0957df145236..c573f2688159619474051e1f7cfefb957f7154a8 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -41,7 +41,6 @@ pub struct AgentSettings { pub always_allow_tool_actions: bool, pub notify_when_agent_waiting: NotifyWhenAgentWaiting, pub play_sound_when_agent_done: bool, - pub stream_edits: bool, pub single_file_review: bool, pub model_parameters: Vec, pub preferred_completion_mode: CompletionMode, @@ -174,7 +173,6 @@ impl Settings for AgentSettings { always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), - stream_edits: agent.stream_edits.unwrap(), single_file_review: agent.single_file_review.unwrap(), model_parameters: agent.model_parameters, preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(), diff --git a/crates/eval/runner_settings.json b/crates/eval/runner_settings.json index 91f193d7b3359bdc9ca5a2255f0fb51c4484f344..53d853023c75e78f19c78f797b5751ff79bf1e44 100644 --- a/crates/eval/runner_settings.json +++ b/crates/eval/runner_settings.json @@ -1,7 +1,6 @@ { "assistant": { "always_allow_tool_actions": true, - "stream_edits": true, "version": "2" } } diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs index 80326c84688697ab4dcbc83d9013cf3acab26d2b..9d1cd6fed88deb89848a24c890237805b5a8128c 100644 --- a/crates/settings/src/settings_content/agent.rs +++ b/crates/settings/src/settings_content/agent.rs @@ -68,10 +68,6 @@ pub struct AgentSettingsContent { /// /// Default: false pub play_sound_when_agent_done: Option, - /// Whether to stream edits from the agent as they are received. - /// - /// Default: false - pub stream_edits: Option, /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. /// /// Default: true From f2b966b1392d7820441687eacf696c959602c534 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Sun, 19 Oct 2025 11:31:43 -0700 Subject: [PATCH 037/202] remote: Use SFTP over SCP for uploading files and directories (#40510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #37322 Uses SFTP if available, otherwise falls back to SCP for uploading files and directories to remote. This fixes an issue on older macOS versions where outdated SCP can throw an ambiguous target error. Release Notes: - Fixed an issue where extensions wouldn’t work when SSHing into a remote from older macOS versions. --- crates/remote/src/transport/ssh.rs | 167 ++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 51 deletions(-) diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index d547f18e3307e008fb6db6fe2ecdf9480a73f334..e119e3a2edbd166990076820bf8056821555fde8 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -170,35 +170,44 @@ impl RemoteConnection for SshRemoteConnection { dest_path: RemotePathBuf, cx: &App, ) -> Task> { - let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command, false) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg("-C") - .arg("-r") - .arg(&src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path - )) - .output(); + let dest_path_str = dest_path.to_string(); + let src_path_display = src_path.display().to_string(); + + let mut sftp_command = self.build_sftp_command(); + let mut scp_command = + self.build_scp_command(&src_path, &dest_path_str, Some(&["-C", "-r"])); cx.background_spawn(async move { - let output = output.await?; + if Self::is_sftp_available().await { + log::debug!("using SFTP for directory upload"); + let mut child = sftp_command.spawn()?; + if let Some(mut stdin) = child.stdin.take() { + use futures::AsyncWriteExt; + let sftp_batch = format!("put -r {} {}\n", src_path.display(), dest_path_str); + stdin.write_all(sftp_batch.as_bytes()).await?; + drop(stdin); + } + + let output = child.output().await?; + anyhow::ensure!( + output.status.success(), + "failed to upload directory via SFTP {} -> {}: {}", + src_path_display, + dest_path_str, + String::from_utf8_lossy(&output.stderr) + ); + + return Ok(()); + } + + log::debug!("using SCP for directory upload"); + let output = scp_command.output().await?; anyhow::ensure!( output.status.success(), - "failed to upload directory {} -> {}: {}", - src_path.display(), - dest_path, + "failed to upload directory via SCP {} -> {}: {}", + src_path_display, + dest_path_str, String::from_utf8_lossy(&output.stderr) ); @@ -643,36 +652,92 @@ impl SshRemoteConnection { Ok(()) } - async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + fn build_scp_command( + &self, + src_path: &Path, + dest_path_str: &str, + args: Option<&[&str]>, + ) -> process::Command { let mut command = util::command::new_smol_command("scp"); - let output = self - .socket - .ssh_options(&mut command, false) - .args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ) - .arg(src_path) - .arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path.display(self.path_style()) - )) - .output() - .await?; + self.socket.ssh_options(&mut command, false).args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), + ); + if let Some(args) = args { + command.args(args); + } + command.arg(src_path).arg(format!( + "{}:{}", + self.socket.connection_options.scp_url(), + dest_path_str + )); + command + } - anyhow::ensure!( - output.status.success(), - "failed to upload file {} -> {}: {}", - src_path.display(), - dest_path.display(self.path_style()), - String::from_utf8_lossy(&output.stderr) + fn build_sftp_command(&self) -> process::Command { + let mut command = util::command::new_smol_command("sftp"); + self.socket.ssh_options(&mut command, false).args( + self.socket + .connection_options + .port + .map(|port| vec!["-P".to_string(), port.to_string()]) + .unwrap_or_default(), ); - Ok(()) + command.arg("-b").arg("-"); + command.arg(self.socket.connection_options.scp_url()); + command.stdin(Stdio::piped()); + command + } + + async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> { + log::debug!("uploading file {:?} to {:?}", src_path, dest_path); + + let dest_path_str = dest_path.display(self.path_style()); + + if Self::is_sftp_available().await { + log::debug!("using SFTP for file upload"); + let mut command = self.build_sftp_command(); + let sftp_batch = format!("put {} {}\n", src_path.display(), dest_path_str); + + let mut child = command.spawn()?; + if let Some(mut stdin) = child.stdin.take() { + use futures::AsyncWriteExt; + stdin.write_all(sftp_batch.as_bytes()).await?; + drop(stdin); + } + + let output = child.output().await?; + anyhow::ensure!( + output.status.success(), + "failed to upload file via SFTP {} -> {}: {}", + src_path.display(), + dest_path_str, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + } else { + log::debug!("using SCP for file upload"); + let mut command = self.build_scp_command(src_path, &dest_path_str, None); + let output = command.output().await?; + + anyhow::ensure!( + output.status.success(), + "failed to upload file via SCP {} -> {}: {}", + src_path.display(), + dest_path_str, + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) + } + } + + async fn is_sftp_available() -> bool { + which::which("sftp").is_ok() } } From fd4682c8d1a3af412002e2e66a5e3137635c5e86 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 19 Oct 2025 15:29:09 -0400 Subject: [PATCH 038/202] Remove Windows beta issue template (#40650) Release Notes: - N/A --- .../ISSUE_TEMPLATE/06_bug_windows_beta.yml | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/06_bug_windows_beta.yml diff --git a/.github/ISSUE_TEMPLATE/06_bug_windows_beta.yml b/.github/ISSUE_TEMPLATE/06_bug_windows_beta.yml deleted file mode 100644 index b2b2a0f9dfcd5ddaa0dda41650864b053c5bb933..0000000000000000000000000000000000000000 --- a/.github/ISSUE_TEMPLATE/06_bug_windows_beta.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Bug Report (Windows Beta) -description: Zed Windows Beta Related Bugs -type: "Bug" -labels: ["windows"] -title: "Windows Beta: " -body: - - type: textarea - attributes: - label: Summary - description: Describe the bug with a one-line summary, and provide detailed reproduction steps - value: | - - SUMMARY_SENTENCE_HERE - - ### Description - - Steps to trigger the problem: - 1. - 2. - 3. - - **Expected Behavior**: - **Actual Behavior**: - - validations: - required: true - - type: textarea - id: environment - attributes: - label: Zed Version and System Specs - description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"' - placeholder: | - Output of "zed: copy system specs into clipboard" - validations: - required: true From 59b87d5c71a51d2b981ba86f694212a748b33120 Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Mon, 20 Oct 2025 04:21:21 +0200 Subject: [PATCH 039/202] git_ui: When no changes, disable stage/unstage toolbar buttons (#39909) # Why While working on recent PR I have spotted that "Stage" and "Unstage" buttons in "Uncommited Changes" toolbar are always active, even when there is no changes made locally. Screenshot 2025-10-10 at 00 49 06 # How Re-use already existing button states for managing the disabled state of "Uncommited Changes" toolbar buttons when changeset is empty. Release Notes: - Added disabled state for "Uncommited Changes" toolbar buttons when there are no changes present # Preview Screenshot 2025-10-10 at 08 40 14 --- crates/git_ui/src/project_diff.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index e0eda7d8bef40ae622eb03b2df981d28d61de5d0..5cd2e5e3b07bb8bf503cada7f0ca2f8dd0388a4e 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -947,6 +947,11 @@ impl Render for ProjectDiffToolbar { &StageAndNext, &focus_handle, )) + .disabled( + !button_states.prev_next + && !button_states.stage_all + && !button_states.unstage_all, + ) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&StageAndNext, window, cx) })), @@ -958,6 +963,11 @@ impl Render for ProjectDiffToolbar { &UnstageAndNext, &focus_handle, )) + .disabled( + !button_states.prev_next + && !button_states.stage_all + && !button_states.unstage_all, + ) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&UnstageAndNext, window, cx) })), From 1d3bf9789e64c2ca97b91d6bc31341d2b5f94ec1 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Sun, 19 Oct 2025 21:34:55 -0700 Subject: [PATCH 040/202] Add DelayMs type for settings (#40659) Closes #40610 Release Notes: - N/A --- crates/diagnostics/src/diagnostics_tests.rs | 2 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/editor_settings.rs | 8 +- crates/editor/src/element.rs | 10 +- crates/editor/src/hover_popover.rs | 6 +- crates/project/src/project_settings.rs | 12 +- crates/settings/src/settings_content.rs | 30 ++++ .../settings/src/settings_content/editor.rs | 20 ++- .../settings/src/settings_content/project.rs | 9 +- crates/settings/src/settings_content/theme.rs | 6 + .../src/settings_content/workspace.rs | 6 +- crates/settings/src/vscode_import.rs | 5 +- crates/settings_ui/src/settings_ui.rs | 1 + crates/ui_input/src/number_field.rs | 129 +++++------------- crates/workspace/src/item.rs | 2 +- crates/workspace/src/workspace.rs | 5 +- 16 files changed, 130 insertions(+), 125 deletions(-) diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 2d86361df003ceab114d1e4cd3adabbbfbf9b497..74a235697834b120dd1b0dbb55aae03fe950be64 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1341,7 +1341,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) range: Some(range), })) }); - let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay + 1); + let delay = cx.update(|_, cx| EditorSettings::get_global(cx).hover_popover_delay.0 + 1); cx.background_executor .advance_clock(Duration::from_millis(delay)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2ff8b585c3f228f70246947bed8c2e6da221bd37..9baa1c892e8dc7e0f62cdc2c0e7abbed82ae9cdd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6772,7 +6772,7 @@ impl Editor { if let Some(state) = &mut self.inline_blame_popover { state.hide_task.take(); } else { - let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; + let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0; let blame_entry = blame_entry.clone(); let show_task = cx.spawn(async move |editor, cx| { if !ignore_timeout { @@ -6863,7 +6863,7 @@ impl Editor { return None; } - let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce; + let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce.0; self.document_highlights_task = Some(cx.spawn(async move |this, cx| { cx.background_executor() .timer(Duration::from_millis(debounce)) diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 9ecbbff97612d391e56271f19331160ef08ba534..dc67ab3ed6c8cfdbe88809e32d615789c01eef60 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -5,7 +5,7 @@ use language::CursorShape; use project::project_settings::DiagnosticSeverity; use settings::Settings; pub use settings::{ - CurrentLineHighlight, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, + CurrentLineHighlight, DelayMs, DisplayIn, DocumentColorsRenderMode, DoubleClickInMultibuffer, GoToDefinitionFallback, HideMouseMode, MinimapThumb, MinimapThumbBorder, MultiCursorModifier, ScrollBeyondLastLine, ScrollbarDiagnostics, SeedQuerySetting, ShowMinimap, SnippetSortOrder, }; @@ -20,9 +20,9 @@ pub struct EditorSettings { pub current_line_highlight: CurrentLineHighlight, pub selection_highlight: bool, pub rounded_selection: bool, - pub lsp_highlight_debounce: u64, + pub lsp_highlight_debounce: DelayMs, pub hover_popover_enabled: bool, - pub hover_popover_delay: u64, + pub hover_popover_delay: DelayMs, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub minimap: Minimap, @@ -147,7 +147,7 @@ pub struct DragAndDropSelection { /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. /// /// Default: 300 - pub delay: u64, + pub delay: DelayMs, } /// Default options for buffer and project search items. diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2dcb9996c37d54ad352795b39a0b28ece2827759..52e73bd01c1712371805610f2aff2de6d7aa2d4b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1070,7 +1070,10 @@ impl EditorElement { ref mouse_down_time, } => { let drag_and_drop_delay = Duration::from_millis( - EditorSettings::get_global(cx).drag_and_drop_selection.delay, + EditorSettings::get_global(cx) + .drag_and_drop_selection + .delay + .0, ); if mouse_down_time.elapsed() >= drag_and_drop_delay { let drop_cursor = Selection { @@ -6172,7 +6175,10 @@ impl EditorElement { } = &editor.selection_drag_state { let drag_and_drop_delay = Duration::from_millis( - EditorSettings::get_global(cx).drag_and_drop_selection.delay, + EditorSettings::get_global(cx) + .drag_and_drop_selection + .delay + .0, ); if mouse_down_time.elapsed() >= drag_and_drop_delay { window.set_cursor_style( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 2b0ebf52805493d008c8b69ec4ff86991d6c743d..9db04363b27959d8f8b81539da4ba65c75fbeb02 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -154,7 +154,7 @@ pub fn hover_at_inlay( hide_hover(editor, cx); } - let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; + let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0; let task = cx.spawn_in(window, async move |this, cx| { async move { @@ -275,7 +275,7 @@ fn show_hover( return None; } - let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; + let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay.0; let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All; let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics { Some(group.group_id) @@ -1004,7 +1004,7 @@ mod tests { use text::Bias; fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 { - cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay }) + cx.read(|cx: &App| -> u64 { EditorSettings::get_global(cx).hover_popover_delay.0 }) } impl InfoPopover { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 788cd0212a094154e6c4e3b3eb0d379ecadaf11c..676fac507252646a0650be87dc7a22689a1e70d0 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -331,7 +331,7 @@ pub struct InlineBlameSettings { /// after a delay once the cursor stops moving. /// /// Default: 0 - pub delay_ms: std::time::Duration, + pub delay_ms: settings::DelayMs, /// The amount of padding between the end of the source line and the start /// of the inline blame in units of columns. /// @@ -357,8 +357,8 @@ pub struct BlameSettings { impl GitSettings { pub fn inline_blame_delay(&self) -> Option { - if self.inline_blame.delay_ms.as_millis() > 0 { - Some(self.inline_blame.delay_ms) + if self.inline_blame.delay_ms.0 > 0 { + Some(Duration::from_millis(self.inline_blame.delay_ms.0)) } else { None } @@ -452,7 +452,7 @@ impl Settings for ProjectSettings { let inline = git.inline_blame.unwrap(); InlineBlameSettings { enabled: inline.enabled.unwrap(), - delay_ms: std::time::Duration::from_millis(inline.delay_ms.unwrap()), + delay_ms: inline.delay_ms.unwrap(), padding: inline.padding.unwrap(), min_column: inline.min_column.unwrap(), show_commit_summary: inline.show_commit_summary.unwrap(), @@ -504,11 +504,11 @@ impl Settings for ProjectSettings { include_warnings: diagnostics.include_warnings.unwrap(), lsp_pull_diagnostics: LspPullDiagnosticsSettings { enabled: lsp_pull_diagnostics.enabled.unwrap(), - debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap(), + debounce_ms: lsp_pull_diagnostics.debounce_ms.unwrap().0, }, inline: InlineDiagnosticsSettings { enabled: inline_diagnostics.enabled.unwrap(), - update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap(), + update_debounce_ms: inline_diagnostics.update_debounce_ms.unwrap().0, padding: inline_diagnostics.padding.unwrap(), min_column: inline_diagnostics.min_column.unwrap(), max_severity: inline_diagnostics.max_severity.map(Into::into), diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 92493557eeeb5f28c6a4d25fca3f9e38f8eef6bb..045bc21613141ca30f125ab25757a3b3a97307b0 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -985,3 +985,33 @@ impl merge_from::MergeFrom for SaturatingBool { self.0 |= other.0 } } + +#[derive( + Copy, + Clone, + Default, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + MergeFrom, + JsonSchema, + derive_more::FromStr, +)] +#[serde(transparent)] +pub struct DelayMs(pub u64); + +impl From for DelayMs { + fn from(n: u64) -> Self { + Self(n) + } +} + +impl std::fmt::Display for DelayMs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}ms", self.0) + } +} diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index 7bc447346c8a20e53640928a6ba0ca28e10d92e7..4b00cd24500999eb917bf2117fd17b557d2509ae 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use settings_macros::MergeFrom; -use crate::{DiagnosticSeverityContent, ShowScrollbar}; +use crate::{DelayMs, DiagnosticSeverityContent, ShowScrollbar}; #[skip_serializing_none] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] @@ -45,7 +45,7 @@ pub struct EditorSettingsContent { /// server based on the current cursor location. /// /// Default: 75 - pub lsp_highlight_debounce: Option, + pub lsp_highlight_debounce: Option, /// Whether to show the informational hover box when moving the mouse /// over symbols in the editor. /// @@ -54,7 +54,7 @@ pub struct EditorSettingsContent { /// Time to wait in milliseconds before showing the informational hover box. /// /// Default: 300 - pub hover_popover_delay: Option, + pub hover_popover_delay: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings @@ -722,7 +722,7 @@ pub struct DragAndDropSelectionContent { /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. /// /// Default: 300 - pub delay: Option, + pub delay: Option, } /// When to show the minimap in the editor. @@ -804,6 +804,12 @@ impl Display for MinimumContrast { } } +impl From for MinimumContrast { + fn from(x: f32) -> Self { + Self(x) + } +} + /// Opacity of the inactive panes. 0 means transparent, 1 means opaque. /// /// Valid range: 0.0 to 1.0 @@ -828,3 +834,9 @@ impl Display for InactiveOpacity { write!(f, "{:.1}", self.0) } } + +impl From for InactiveOpacity { + fn from(x: f32) -> Self { + Self(x) + } +} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index d421a7bb2aefb92f0c7cd1de1c89fe1fee95e3ec..a26a72e7aa32d513dd5afe3c7aa5078f3e3201a5 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -8,7 +8,8 @@ use settings_macros::MergeFrom; use util::serde::default_true; use crate::{ - AllLanguageSettingsContent, ExtendingVec, ProjectTerminalSettingsContent, SlashCommandSettings, + AllLanguageSettingsContent, DelayMs, ExtendingVec, ProjectTerminalSettingsContent, + SlashCommandSettings, }; #[skip_serializing_none] @@ -310,7 +311,7 @@ pub struct InlineBlameSettings { /// after a delay once the cursor stops moving. /// /// Default: 0 - pub delay_ms: Option, + pub delay_ms: Option, /// The amount of padding between the end of the source line and the start /// of the inline blame in units of columns. /// @@ -397,7 +398,7 @@ pub struct LspPullDiagnosticsSettingsContent { /// 0 turns the debounce off. /// /// Default: 50 - pub debounce_ms: Option, + pub debounce_ms: Option, } #[skip_serializing_none] @@ -413,7 +414,7 @@ pub struct InlineDiagnosticsSettingsContent { /// last editor event. /// /// Default: 150 - pub update_debounce_ms: Option, + pub update_debounce_ms: Option, /// The amount of padding between the end of the source line and the start /// of the inline diagnostic in units of columns. /// diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index 45640ae3ae37321498bb3dafd6d07b2ba1f2d92e..c988b98a4ef04c7ff5e9b20eaf9e2bebeccb88fb 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -112,6 +112,12 @@ impl Display for CodeFade { } } +impl From for CodeFade { + fn from(x: f32) -> Self { + Self(x) + } +} + fn default_font_features() -> Option { Some(FontFeatures::default()) } diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 7ebb468f79bf195fb9d97b8d52dd6e728d8c8f99..78b17eeb883dd83b16f45c0cabfc3be9a7840eae 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use settings_macros::MergeFrom; -use crate::{DockPosition, DockSide, InactiveOpacity, ScrollbarSettingsContent, ShowIndentGuides}; +use crate::{ + DelayMs, DockPosition, DockSide, InactiveOpacity, ScrollbarSettingsContent, ShowIndentGuides, +}; #[skip_serializing_none] #[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] @@ -386,7 +388,7 @@ pub enum AutosaveSetting { /// Disable autosave. Off, /// Save after inactivity period of `milliseconds`. - AfterDelay { milliseconds: u64 }, + AfterDelay { milliseconds: DelayMs }, /// Autosave when focus changes. OnFocusChange, /// Autosave when the active window changes. diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index e1cce43f6227bb26a36be871c31cf1143aab5c70..c07f75a9e3b96bea689f5d6dda22d0742800d321 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -259,7 +259,7 @@ impl VsCodeSettings { gutter: self.gutter_content(), hide_mouse: None, horizontal_scroll_margin: None, - hover_popover_delay: self.read_u64("editor.hover.delay"), + hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into), hover_popover_enabled: self.read_bool("editor.hover.enabled"), inline_code_actions: None, jupyter: None, @@ -791,7 +791,8 @@ impl VsCodeSettings { milliseconds: self .read_value("files.autoSaveDelay") .and_then(|v| v.as_u64()) - .unwrap_or(1000), + .unwrap_or(1000) + .into(), }), "onFocusChange" => Some(AutosaveSetting::OnFocusChange), "onWindowChange" => Some(AutosaveSetting::OnWindowChange), diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 7856164a12a34643b9a223023a8ab27922c68906..138d54fc4a539045b04ca1091ad8b55049fbf1d5 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -417,6 +417,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::>(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) + .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) diff --git a/crates/ui_input/src/number_field.rs b/crates/ui_input/src/number_field.rs index b72566947771be6411ce879a48092f508909b18f..3ae1d77c0400ea474864087bf3a4a5f4705a2e41 100644 --- a/crates/ui_input/src/number_field.rs +++ b/crates/ui_input/src/number_field.rs @@ -8,7 +8,7 @@ use std::{ use editor::{Editor, EditorStyle}; use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers}; -use settings::{CodeFade, InactiveOpacity, MinimumContrast}; +use settings::{CodeFade, DelayMs, InactiveOpacity, MinimumContrast}; use ui::prelude::*; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -31,102 +31,47 @@ pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr fn saturating_sub(self, rhs: Self) -> Self; } -impl NumberFieldType for gpui::FontWeight { - fn default_step() -> Self { - FontWeight(50.0) - } - fn large_step() -> Self { - FontWeight(100.0) - } - fn small_step() -> Self { - FontWeight(10.0) - } - fn min_value() -> Self { - gpui::FontWeight::THIN - } - fn max_value() -> Self { - gpui::FontWeight::BLACK - } - fn saturating_add(self, rhs: Self) -> Self { - FontWeight((self.0 + rhs.0).min(Self::max_value().0)) - } - fn saturating_sub(self, rhs: Self) -> Self { - FontWeight((self.0 - rhs.0).max(Self::min_value().0)) - } -} +macro_rules! impl_newtype_numeric_stepper { + ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { + impl NumberFieldType for $type { + fn default_step() -> Self { + $default.into() + } -impl NumberFieldType for settings::CodeFade { - fn default_step() -> Self { - CodeFade(0.10) - } - fn large_step() -> Self { - CodeFade(0.20) - } - fn small_step() -> Self { - CodeFade(0.05) - } - fn min_value() -> Self { - CodeFade(0.0) - } - fn max_value() -> Self { - CodeFade(0.9) - } - fn saturating_add(self, rhs: Self) -> Self { - CodeFade((self.0 + rhs.0).min(Self::max_value().0)) - } - fn saturating_sub(self, rhs: Self) -> Self { - CodeFade((self.0 - rhs.0).max(Self::min_value().0)) - } -} + fn large_step() -> Self { + $large.into() + } -impl NumberFieldType for settings::InactiveOpacity { - fn default_step() -> Self { - InactiveOpacity(0.10) - } - fn large_step() -> Self { - InactiveOpacity(0.20) - } - fn small_step() -> Self { - InactiveOpacity(0.05) - } - fn min_value() -> Self { - InactiveOpacity(0.0) - } - fn max_value() -> Self { - InactiveOpacity(1.0) - } - fn saturating_add(self, rhs: Self) -> Self { - InactiveOpacity((self.0 + rhs.0).min(Self::max_value().0)) - } - fn saturating_sub(self, rhs: Self) -> Self { - InactiveOpacity((self.0 - rhs.0).max(Self::min_value().0)) - } -} + fn small_step() -> Self { + $small.into() + } -impl NumberFieldType for settings::MinimumContrast { - fn default_step() -> Self { - MinimumContrast(1.0) - } - fn large_step() -> Self { - MinimumContrast(10.0) - } - fn small_step() -> Self { - MinimumContrast(0.5) - } - fn min_value() -> Self { - MinimumContrast(0.0) - } - fn max_value() -> Self { - MinimumContrast(106.0) - } - fn saturating_add(self, rhs: Self) -> Self { - MinimumContrast((self.0 + rhs.0).min(Self::max_value().0)) - } - fn saturating_sub(self, rhs: Self) -> Self { - MinimumContrast((self.0 - rhs.0).max(Self::min_value().0)) - } + fn min_value() -> Self { + $min.into() + } + + fn max_value() -> Self { + $max.into() + } + + fn saturating_add(self, rhs: Self) -> Self { + $type((self.0 + rhs.0).min(Self::max_value().0)) + } + + fn saturating_sub(self, rhs: Self) -> Self { + $type((self.0 - rhs.0).max(Self::min_value().0)) + } + } + }; } +#[rustfmt::skip] +impl_newtype_numeric_stepper!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK); +impl_newtype_numeric_stepper!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9); +impl_newtype_numeric_stepper!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0); +impl_newtype_numeric_stepper!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0); +impl_newtype_numeric_stepper!(DelayMs, 100, 500, 10, 0, 2000); + macro_rules! impl_numeric_stepper_int { ($type:ident) => { impl NumberFieldType for $type { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 1a6a09c38d0aea0c2df59947455628ec4a7ccd43..bc755d851036d10f04b76866b3d7b94673f2df84 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -811,7 +811,7 @@ impl ItemHandle for Entity { let autosave = item.workspace_settings(cx).autosave; if let AutosaveSetting::AfterDelay { milliseconds } = autosave { - let delay = Duration::from_millis(milliseconds); + let delay = Duration::from_millis(milliseconds.0); let item = item.clone(); pending_autosave.fire_new( delay, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bbb9ee767196c062707efcc2618670cf09da4e87..053b578ff9082bd933440a36abab47c9b84928bd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -8799,8 +8799,9 @@ mod tests { item.update(cx, |item, cx| { SettingsStore::update_global(cx, |settings, cx| { settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = - Some(AutosaveSetting::AfterDelay { milliseconds: 500 }); + settings.workspace.autosave = Some(AutosaveSetting::AfterDelay { + milliseconds: 500.into(), + }); }) }); item.is_dirty = true; From 92ff29fa7d98858011c584904d7a64f38a49b670 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 20 Oct 2025 01:02:31 -0700 Subject: [PATCH 041/202] Add Vue language server v3 support (#40651) Closes https://github.com/zed-extensions/vue/issues/48 Migration guide: https://github.com/vuejs/language-tools/discussions/5456 PR to remove tdsk: https://github.com/zed-extensions/vue/pull/61 Release Notes: - Added support for Vue language server version 3. Know more [here](https://github.com/vuejs/language-tools/releases/tag/v3.0.0). --------- Co-authored-by: MrSubidubi Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- assets/settings/default.json | 2 +- crates/project/src/lsp_store.rs | 2 + .../src/lsp_store/vue_language_server_ext.rs | 124 ++++++++++++++++++ 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 crates/project/src/lsp_store/vue_language_server_ext.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index df1ec6fa3a997bae3367b6af7a00262191960983..331c696bf890ed16762e7702d03257952a30d124 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1821,7 +1821,7 @@ "use_on_type_format": false }, "Vue.js": { - "language_servers": ["vue-language-server", "..."], + "language_servers": ["vue-language-server", "vtsls", "..."], "prettier": { "allowed": true } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index c33f922a62d6ce702748a92063c8c2a1a2095a56..bdce7c26fd4b40dd37fd9bf67a03490bdf3c7bc5 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -14,6 +14,7 @@ pub mod json_language_server_ext; pub mod log_store; pub mod lsp_ext_command; pub mod rust_analyzer_ext; +pub mod vue_language_server_ext; use crate::{ CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse, @@ -987,6 +988,7 @@ impl LocalLspStore { }) .detach(); + vue_language_server_ext::register_requests(this.clone(), language_server); json_language_server_ext::register_requests(this.clone(), language_server); rust_analyzer_ext::register_notifications(this.clone(), language_server); clangd_ext::register_notifications(this, language_server, adapter); diff --git a/crates/project/src/lsp_store/vue_language_server_ext.rs b/crates/project/src/lsp_store/vue_language_server_ext.rs new file mode 100644 index 0000000000000000000000000000000000000000..28249745403d2c6afe3532582ee92bb94de7dde7 --- /dev/null +++ b/crates/project/src/lsp_store/vue_language_server_ext.rs @@ -0,0 +1,124 @@ +use anyhow::Context as _; +use gpui::{AppContext, WeakEntity}; +use lsp::{LanguageServer, LanguageServerName}; +use serde_json::Value; + +use crate::LspStore; + +struct VueServerRequest; +struct TypescriptServerResponse; + +impl lsp::notification::Notification for VueServerRequest { + type Params = Vec<(u64, String, serde_json::Value)>; + + const METHOD: &'static str = "tsserver/request"; +} + +impl lsp::notification::Notification for TypescriptServerResponse { + type Params = Vec<(u64, serde_json::Value)>; + + const METHOD: &'static str = "tsserver/response"; +} + +const VUE_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vue-language-server"); +const VTSLS: LanguageServerName = LanguageServerName::new_static("vtsls"); +const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-language-server"); + +pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { + let language_server_name = language_server.name(); + if language_server_name == VUE_SERVER_NAME { + let vue_server_id = language_server.server_id(); + language_server + .on_notification::({ + move |params, cx| { + let lsp_store = lsp_store.clone(); + let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| { + this.language_server_for_id(vue_server_id) + }) else { + return; + }; + + let requests = params; + let target_server = match lsp_store.read_with(cx, |this, _| { + let language_server_id = this + .as_local() + .and_then(|local| { + local.language_server_ids.iter().find_map(|(seed, v)| { + [VTSLS, TS_LS].contains(&seed.name).then_some(v.id) + }) + }) + .context("Could not find language server")?; + + this.language_server_for_id(language_server_id) + .context("language server not found") + }) { + Ok(Ok(server)) => server, + other => { + log::warn!( + "vue-language-server forwarding skipped: {other:?}. \ + Returning null tsserver responses" + ); + if !requests.is_empty() { + let null_responses = requests + .into_iter() + .map(|(id, _, _)| (id, Value::Null)) + .collect::>(); + let _ = vue_server + .notify::(null_responses); + } + return; + } + }; + + let cx = cx.clone(); + for (request_id, command, payload) in requests.into_iter() { + let target_server = target_server.clone(); + let vue_server = vue_server.clone(); + cx.background_spawn(async move { + let response = target_server + .request::( + lsp::ExecuteCommandParams { + command: "typescript.tsserverRequest".to_owned(), + arguments: vec![Value::String(command), payload], + ..Default::default() + }, + ) + .await; + + let response_body = match response { + util::ConnectionResult::Result(Ok(result)) => match result { + Some(Value::Object(mut map)) => map + .remove("body") + .unwrap_or(Value::Object(map)), + Some(other) => other, + None => Value::Null, + }, + util::ConnectionResult::Result(Err(error)) => { + log::warn!( + "typescript.tsserverRequest failed: {error:?} for request {request_id}" + ); + Value::Null + } + other => { + log::warn!( + "typescript.tsserverRequest did not return a response: {other:?} for request {request_id}" + ); + Value::Null + } + }; + + if let Err(err) = vue_server + .notify::(vec![(request_id, response_body)]) + { + log::warn!( + "Failed to notify vue-language-server of tsserver response: {err:?}" + ); + } + }) + .detach(); + } + } + }) + .detach(); + } +} From e3297cdcaed2b5ca84702bb76d2893bee8e07155 Mon Sep 17 00:00:00 2001 From: Sylvain Brunerie Date: Mon, 20 Oct 2025 11:44:58 +0200 Subject: [PATCH 042/202] Add "Setting up Xdebug" section in PHP docs (#40470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page about PHP in the docs doesn’t explain how to use Xdebug. I had a lof of trouble setting it up the first time, then recently had another headache trying to get it to work again, because the value for `adapter` had changed from `PHP` to `Xdebug`. It’s likely that my example config isn’t perfect or has redundant stuff or whatever, feel free to amend it. I also took the liberty to set the Phpactor and Intelephense headings to level 3 because I felt like they were part of "Choosing a language server." Release Notes: - N/A --- docs/src/languages/php.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/src/languages/php.md b/docs/src/languages/php.md index 40c7f9a838e0435b952b25bb0072153ac2fcf4ec..b2b8dffcf1b973f769d2900c21385804fbb4394f 100644 --- a/docs/src/languages/php.md +++ b/docs/src/languages/php.md @@ -13,7 +13,7 @@ The PHP extension offers both `phpactor` and `intelephense` language server supp `phpactor` is enabled by default. -## Phpactor +### Phpactor The Zed PHP Extension can install `phpactor` automatically but requires `php` to be installed and available in your path: @@ -25,7 +25,7 @@ The Zed PHP Extension can install `phpactor` automatically but requires `php` to which php ``` -## Intelephense +### Intelephense [Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/). @@ -60,3 +60,35 @@ To use the premium features, you can place your [licence.txt file](https://intel Zed supports syntax highlighting for PHPDoc comments. - Tree-sitter: [claytonrcarter/tree-sitter-phpdoc](https://github.com/claytonrcarter/tree-sitter-phpdoc) + +## Setting up Xdebug + +Zed’s PHP extension provides a debug adapter for PHP and Xdebug. The adapter name is `Xdebug`. Here a couple ways you can use it: + +```json +[ + { + "label": "PHP: Listen to Xdebug", + "adapter": "Xdebug", + "request": "launch", + "initialize_args": { + "port": 9003 + } + }, + { + "label": "PHP: Debug this test", + "adapter": "Xdebug", + "request": "launch", + "program": "vendor/bin/phpunit", + "args": ["--filter", "$ZED_SYMBOL"] + } +] +``` + +In case you run into issues: + +- ensure that you have Xdebug installed for the version of PHP you’re running +- ensure that Xdebug is configured to run in `debug` mode +- ensure that Xdebug is actually starting a debugging session +- check that the host and port matches between Xdebug and Zed +- look at the diagnostics log by using the `xdebug_info()` function in the page you’re trying to debug From 36210e72af5e5c480775ddd83e9d31afec1cc52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Nison?= Date: Mon, 20 Oct 2025 11:46:34 +0200 Subject: [PATCH 043/202] Make the Yarn SDK path relative to the worktree (#40062) Let's say you run this: ``` cd ~/proj-a zed ~/proj-b ``` The `zed` process will execute with `current_dir() = ~/proj-a`, but a `worktree_root_path() = ~/proj-b`. The old detection was then checking if the Yarn SDK was installed in `proj-a` to decide whether to set the tsdk value or not. This was incorrect, as we should instead check for the SDK presence inside `proj-b`. Release Notes: - Fixed the Yarn SDK detection when the Zed pwd is different from the opened folder. --- crates/languages/src/vtsls.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index fbaab1341c3b5332887698f0e28397a15b9f158b..8cbb9f307f6f4222e0e9a65fe2a6954f97fc7f21 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -12,7 +12,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::{ResultExt, maybe, merge_json_value_into, rel_path::RelPath}; +use util::{ResultExt, maybe, merge_json_value_into}; fn typescript_server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] @@ -29,19 +29,19 @@ impl VtslsLspAdapter { const TYPESCRIPT_PACKAGE_NAME: &'static str = "typescript"; const TYPESCRIPT_TSDK_PATH: &'static str = "node_modules/typescript/lib"; + const TYPESCRIPT_YARN_TSDK_PATH: &'static str = ".yarn/sdks/typescript/lib"; pub fn new(node: NodeRuntime, fs: Arc) -> Self { VtslsLspAdapter { node, fs } } async fn tsdk_path(&self, adapter: &Arc) -> Option<&'static str> { - let is_yarn = adapter - .read_text_file(RelPath::unix(".yarn/sdks/typescript/lib/typescript.js").unwrap()) - .await - .is_ok(); + let yarn_sdk = adapter + .worktree_root_path() + .join(Self::TYPESCRIPT_YARN_TSDK_PATH); - let tsdk_path = if is_yarn { - ".yarn/sdks/typescript/lib" + let tsdk_path = if self.fs.is_dir(&yarn_sdk).await { + Self::TYPESCRIPT_YARN_TSDK_PATH } else { Self::TYPESCRIPT_TSDK_PATH }; From 94c28ba14afb812d748409ed93f930911b56d2f7 Mon Sep 17 00:00:00 2001 From: Daniel Wargh <55995125+ogdakke@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:22:07 +0300 Subject: [PATCH 044/202] Improve TS and JS symbol outline (#39797) Added more granular symbols for ts and js in outline panel. This is a bit closer to what vscode offers.
Screenshots of current vs new

New: image Current: image Current vscode (cursor): image

I have never touched scheme before, and pair-programmed this with ai, so please let me know if there's any glaring issues with the implementation. I just miss the outline panel in vscode very much, and would love to see this land. Happy to help with tsx/jsx as well if this is the direction you guys were thinking of taking the outline impl. Doesn't fully close https://github.com/zed-industries/zed/issues/20964 as there is no support for chained class method callbacks or `Class.prototype.method = ...` as mentioned in https://github.com/zed-industries/zed/issues/21243, but this is a step forward. Release Notes: - Improved typescript and javascript symbol outline panel --- crates/languages/src/javascript/outline.scm | 146 +++++++++++-- crates/languages/src/typescript.rs | 220 +++++++++++++++++++- crates/languages/src/typescript/outline.scm | 145 +++++++++++-- 3 files changed, 468 insertions(+), 43 deletions(-) diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index ca16c27a27be3e1e09ced16cd2eef7aa28345f9e..5f72103bc63bdfab73f7b858c01abe8d34317b22 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -31,38 +31,103 @@ (export_statement (lexical_declaration ["let" "const"] @context - ; Multiple names may be exported - @item is on the declarator to keep - ; ranges distinct. (variable_declarator - name: (_) @name) @item))) + name: (identifier) @name) @item))) + +; Exported array destructuring +(program + (export_statement + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (array_pattern + [ + (identifier) @name @item + (assignment_pattern left: (identifier) @name @item) + (rest_pattern (identifier) @name @item) + ]))))) + +; Exported object destructuring +(program + (export_statement + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (object_pattern + [(shorthand_property_identifier_pattern) @name @item + (pair_pattern + value: (identifier) @name @item) + (pair_pattern + value: (assignment_pattern left: (identifier) @name @item)) + (rest_pattern (identifier) @name @item)]))))) (program (lexical_declaration ["let" "const"] @context - ; Multiple names may be defined - @item is on the declarator to keep - ; ranges distinct. (variable_declarator - name: (_) @name) @item)) + name: (identifier) @name) @item)) + +; Top-level array destructuring +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (array_pattern + [ + (identifier) @name @item + (assignment_pattern left: (identifier) @name @item) + (rest_pattern (identifier) @name @item) + ])))) + +; Top-level object destructuring +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (object_pattern + [(shorthand_property_identifier_pattern) @name @item + (pair_pattern + value: (identifier) @name @item) + (pair_pattern + value: (assignment_pattern left: (identifier) @name @item)) + (rest_pattern (identifier) @name @item)])))) (class_declaration "class" @context name: (_) @name) @item -(method_definition - [ - "get" - "set" - "async" - "*" - "readonly" - "static" - (override_modifier) - (accessibility_modifier) - ]* @context - name: (_) @name - parameters: (formal_parameters - "(" @context - ")" @context)) @item +; Method definitions in classes (not in object literals) +(class_body + (method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item) + +; Object literal methods +(variable_declarator + value: (object + (method_definition + [ + "get" + "set" + "async" + "*" + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item)) (public_field_definition [ @@ -116,4 +181,43 @@ ) ) @item +; Object properties +(pair + key: [ + (property_identifier) @name + (string (string_fragment) @name) + (number) @name + (computed_property_name) @name + ]) @item + +; Nested variables in function bodies +(statement_block + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (identifier) @name) @item)) + +; Nested array destructuring in functions +(statement_block + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (array_pattern + [ + (identifier) @name @item + (assignment_pattern left: (identifier) @name @item) + (rest_pattern (identifier) @name @item) + ])))) + +; Nested object destructuring in functions +(statement_block + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (object_pattern + [(shorthand_property_identifier_pattern) @name @item + (pair_pattern value: (identifier) @name @item) + (pair_pattern value: (assignment_pattern left: (identifier) @name @item)) + (rest_pattern (identifier) @name @item)])))) + (comment) @annotation diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index a9a1104c8c6cfa2b6eaa7083d18316cee4978fc8..334fd4c4a717d2b0a9890611ff5cc21f3d898aeb 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -1110,7 +1110,7 @@ mod tests { let text = r#" function a() { - // local variables are omitted + // local variables are included let a1 = 1; // all functions are included async function a2() {} @@ -1133,6 +1133,7 @@ mod tests { .collect::>(), &[ ("function a()", 0), + ("let a1", 1), ("async function a2()", 1), ("let b", 0), ("function getB()", 0), @@ -1141,6 +1142,223 @@ mod tests { ); } + #[gpui::test] + async fn test_outline_with_destructuring(cx: &mut TestAppContext) { + let language = crate::language( + "typescript", + tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), + ); + + let text = r#" + // Top-level destructuring + const { a1, a2 } = a; + const [b1, b2] = b; + + // Defaults and rest + const [c1 = 1, , c2, ...rest1] = c; + const { d1, d2: e1, f1 = 2, g1: h1 = 3, ...rest2 } = d; + + function processData() { + // Nested object destructuring + const { c1, c2 } = c; + // Nested array destructuring + const [d1, d2, d3] = d; + // Destructuring with renaming + const { f1: g1 } = f; + // With defaults + const [x = 10, y] = xy; + } + + class DataHandler { + method() { + // Destructuring in class method + const { a1, a2 } = a; + const [b1, ...b2] = b; + } + } + "# + .unindent(); + + let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx)); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None)); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[ + ("const a1", 0), + ("const a2", 0), + ("const b1", 0), + ("const b2", 0), + ("const c1", 0), + ("const c2", 0), + ("const rest1", 0), + ("const d1", 0), + ("const e1", 0), + ("const h1", 0), + ("const rest2", 0), + ("function processData()", 0), + ("const c1", 1), + ("const c2", 1), + ("const d1", 1), + ("const d2", 1), + ("const d3", 1), + ("const g1", 1), + ("const x", 1), + ("const y", 1), + ("class DataHandler", 0), + ("method()", 1), + ("const a1", 2), + ("const a2", 2), + ("const b1", 2), + ("const b2", 2), + ] + ); + } + + #[gpui::test] + async fn test_outline_with_object_properties(cx: &mut TestAppContext) { + let language = crate::language( + "typescript", + tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), + ); + + let text = r#" + // Object with function properties + const o = { m() {}, async n() {}, g: function* () {}, h: () => {}, k: function () {} }; + + // Object with primitive properties + const p = { p1: 1, p2: "hello", p3: true }; + + // Nested objects + const q = { + r: { + // won't be included due to one-level depth limit + s: 1 + }, + t: 2 + }; + + function getData() { + const local = { x: 1, y: 2 }; + return local; + } + "# + .unindent(); + + let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx)); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None)); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[ + ("const o", 0), + ("m()", 1), + ("async n()", 1), + ("g", 1), + ("h", 1), + ("k", 1), + ("const p", 0), + ("p1", 1), + ("p2", 1), + ("p3", 1), + ("const q", 0), + ("r", 1), + ("s", 2), + ("t", 1), + ("function getData()", 0), + ("const local", 1), + ("x", 2), + ("y", 2), + ] + ); + } + + #[gpui::test] + async fn test_outline_with_computed_property_names(cx: &mut TestAppContext) { + let language = crate::language( + "typescript", + tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), + ); + + let text = r#" + // Symbols as object keys + const sym = Symbol("test"); + const obj1 = { + [sym]: 1, + [Symbol("inline")]: 2, + normalKey: 3 + }; + + // Enums as object keys + enum Color { Red, Blue, Green } + + const obj2 = { + [Color.Red]: "red value", + [Color.Blue]: "blue value", + regularProp: "normal" + }; + + // Mixed computed properties + const key = "dynamic"; + const obj3 = { + [key]: 1, + ["string" + "concat"]: 2, + [1 + 1]: 3, + static: 4 + }; + + // Nested objects with computed properties + const obj4 = { + [sym]: { + nested: 1 + }, + regular: { + [key]: 2 + } + }; + "# + .unindent(); + + let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx)); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None)); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[ + ("const sym", 0), + ("const obj1", 0), + ("[sym]", 1), + ("[Symbol(\"inline\")]", 1), + ("normalKey", 1), + ("enum Color", 0), + ("const obj2", 0), + ("[Color.Red]", 1), + ("[Color.Blue]", 1), + ("regularProp", 1), + ("const key", 0), + ("const obj3", 0), + ("[key]", 1), + ("[\"string\" + \"concat\"]", 1), + ("[1 + 1]", 1), + ("static", 1), + ("const obj4", 0), + ("[sym]", 1), + ("nested", 2), + ("regular", 1), + ("[key]", 2), + ] + ); + } + #[gpui::test] async fn test_generator_function_outline(cx: &mut TestAppContext) { let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()); diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index f4261b9697d376f517b717bc942387190e0b6dde..54d29007c7b7eb57c0bcaefc2c1e0ab75e4d9a6c 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -34,18 +34,64 @@ (export_statement (lexical_declaration ["let" "const"] @context - ; Multiple names may be exported - @item is on the declarator to keep - ; ranges distinct. (variable_declarator - name: (_) @name) @item)) + name: (identifier) @name) @item)) +; Exported array destructuring +(export_statement + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (array_pattern + [ + (identifier) @name @item + (assignment_pattern left: (identifier) @name @item) + (rest_pattern (identifier) @name @item) + ])))) + +; Exported object destructuring +(export_statement + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (object_pattern + [(shorthand_property_identifier_pattern) @name @item + (pair_pattern + value: (identifier) @name @item) + (pair_pattern + value: (assignment_pattern left: (identifier) @name @item)) + (rest_pattern (identifier) @name @item)])))) + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (identifier) @name) @item)) + +; Top-level array destructuring (program (lexical_declaration ["let" "const"] @context - ; Multiple names may be defined - @item is on the declarator to keep - ; ranges distinct. (variable_declarator - name: (_) @name) @item)) + name: (array_pattern + [ + (identifier) @name @item + (assignment_pattern left: (identifier) @name @item) + (rest_pattern (identifier) @name @item) + ])))) + +; Top-level object destructuring +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (object_pattern + [(shorthand_property_identifier_pattern) @name @item + (pair_pattern + value: (identifier) @name @item) + (pair_pattern + value: (assignment_pattern left: (identifier) @name @item)) + (rest_pattern (identifier) @name @item)])))) (class_declaration "class" @context @@ -56,21 +102,38 @@ "class" @context name: (_) @name) @item -(method_definition - [ - "get" - "set" - "async" - "*" - "readonly" - "static" - (override_modifier) - (accessibility_modifier) - ]* @context - name: (_) @name - parameters: (formal_parameters - "(" @context - ")" @context)) @item +; Method definitions in classes (not in object literals) +(class_body + (method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item) + +; Object literal methods +(variable_declarator + value: (object + (method_definition + [ + "get" + "set" + "async" + "*" + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item)) (public_field_definition [ @@ -124,4 +187,44 @@ ) ) @item +; Object properties +(pair + key: [ + (property_identifier) @name + (string (string_fragment) @name) + (number) @name + (computed_property_name) @name + ]) @item + + +; Nested variables in function bodies +(statement_block + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (identifier) @name) @item)) + +; Nested array destructuring in functions +(statement_block + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (array_pattern + [ + (identifier) @name @item + (assignment_pattern left: (identifier) @name @item) + (rest_pattern (identifier) @name @item) + ])))) + +; Nested object destructuring in functions +(statement_block + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (object_pattern + [(shorthand_property_identifier_pattern) @name @item + (pair_pattern value: (identifier) @name @item) + (pair_pattern value: (assignment_pattern left: (identifier) @name @item)) + (rest_pattern (identifier) @name @item)])))) + (comment) @annotation From 37e264ab99c5406269f7dd66572d28ed23c75be7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:30:06 +0200 Subject: [PATCH 045/202] fs: Reintroduce benchmarks crate (#40689) It was erroenously removed in #40216 Release Notes: - N/A --- Cargo.lock | 8 ++++++++ Cargo.toml | 1 + crates/fs_benchmarks/Cargo.toml | 12 ++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 crates/fs_benchmarks/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index cca144a2bfdd90989013eae1d296c5fb6cf6e11c..ea89f8f36231676bdb32c65d528d0c8f8cde5787 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6341,6 +6341,14 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "fs_benchmarks" +version = "0.1.0" +dependencies = [ + "fs", + "gpui", +] + [[package]] name = "fs_extra" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 4828de8895e62213f16db9118a6ae348aaa40a74..5f4a72a5a6f62b407dd7a796bf5eb79b3f441559 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ members = [ "crates/file_finder", "crates/file_icons", "crates/fs", + "crates/fs_benchmarks", "crates/fsevent", "crates/fuzzy", "crates/git", diff --git a/crates/fs_benchmarks/Cargo.toml b/crates/fs_benchmarks/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..f207a2db3b7354ca96347aaffb5c1915a514ef7c --- /dev/null +++ b/crates/fs_benchmarks/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fs_benchmarks" +version = "0.1.0" +publish.workspace = true +edition.workspace = true + +[dependencies] +fs.workspace = true +gpui = {workspace = true, features = ["windows-manifest"]} + +[lints] +workspace = true From 43a9368dff31882bb0dab3ad7660f05ea8953d2e Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 13:26:20 +0200 Subject: [PATCH 046/202] clock: Cleanup `ReplicaId`, `Lamport` and `Global` (#40600) - Notable change is the use of a newtype for `ReplicaId` - Fixes `WorktreeStore::create_remote_worktree` creating a remote worktree with the local replica id, though this is not currently used - Fixes observing the `Agent` (that is following the agent) causing global clocks to allocate 65535 elements - Shrinks the size of `Global` a bit. In a local or non-collab remote session it won't ever allocate still. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/acp_thread/src/diff.rs | 10 +- .../src/edit_agent/streaming_fuzzy_matcher.rs | 21 ++- .../src/assistant_context.rs | 16 +- .../src/assistant_context_tests.rs | 22 +-- crates/buffer_diff/src/buffer_diff.rs | 28 ++- crates/channel/src/channel_buffer.rs | 11 +- crates/client/src/user.rs | 2 +- crates/clock/src/clock.rs | 171 +++++++++++------- crates/collab/src/db/queries/buffers.rs | 22 ++- crates/collab/src/db/queries/projects.rs | 10 +- crates/collab/src/db/tests/buffer_tests.rs | 44 +++-- crates/editor/src/display_map/inlay_map.rs | 10 +- crates/editor/src/editor.rs | 6 +- crates/git_ui/src/commit_view.rs | 6 +- crates/language/src/buffer.rs | 34 ++-- crates/language/src/buffer_tests.rs | 69 ++++--- crates/language/src/proto.rs | 50 ++--- .../src/syntax_map/syntax_map_tests.rs | 12 +- crates/multi_buffer/src/multi_buffer.rs | 2 +- crates/multi_buffer/src/multi_buffer_tests.rs | 6 +- crates/project/src/buffer_store.rs | 14 +- crates/project/src/git_store/conflict_set.rs | 10 +- crates/project/src/lsp_store.rs | 5 +- crates/project/src/project.rs | 6 +- crates/project/src/project_tests.rs | 2 +- crates/project/src/worktree_store.rs | 2 +- crates/text/src/anchor.rs | 2 +- crates/text/src/operation_queue.rs | 16 +- crates/text/src/tests.rs | 58 +++--- crates/text/src/text.rs | 26 +-- crates/text/src/undo_map.rs | 12 +- crates/worktree/src/worktree.rs | 2 +- crates/zeta/src/zeta.rs | 4 +- 33 files changed, 427 insertions(+), 284 deletions(-) diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs index 15de12af27fe233bad4ad8ebb2893ffa5fbdd598..055b2f7fb86ffe9d7f12459b6b16405ce77815a0 100644 --- a/crates/acp_thread/src/diff.rs +++ b/crates/acp_thread/src/diff.rs @@ -236,21 +236,21 @@ impl PendingDiff { fn finalize(&self, cx: &mut Context) -> FinalizedDiff { let ranges = self.excerpt_ranges(cx); let base_text = self.base_text.clone(); - let language_registry = self.new_buffer.read(cx).language_registry(); + let new_buffer = self.new_buffer.read(cx); + let language_registry = new_buffer.language_registry(); - let path = self - .new_buffer - .read(cx) + let path = new_buffer .file() .map(|file| file.path().display(file.path_style(cx))) .unwrap_or("untitled".into()) .into(); + let replica_id = new_buffer.replica_id(); // Replace the buffer in the multibuffer with the snapshot let buffer = cx.new(|cx| { let language = self.new_buffer.read(cx).language().cloned(); let buffer = TextBuffer::new_normalized( - 0, + replica_id, cx.entity_id().as_non_zero_u64().into(), self.new_buffer.read(cx).line_ending(), self.new_buffer.read(cx).as_rope().clone(), diff --git a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs index 386b8204400a157b37b2f356829fa27df3abca92..904ec05a8c7565d5052cd546fc0bf6d723ffa375 100644 --- a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs +++ b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs @@ -308,12 +308,13 @@ mod tests { use indoc::indoc; use language::{BufferId, TextBuffer}; use rand::prelude::*; + use text::ReplicaId; use util::test::{generate_marked_text, marked_text_ranges}; #[test] fn test_empty_query() { let buffer = TextBuffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), "Hello world\nThis is a test\nFoo bar baz", ); @@ -327,7 +328,7 @@ mod tests { #[test] fn test_streaming_exact_match() { let buffer = TextBuffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), "Hello world\nThis is a test\nFoo bar baz", ); @@ -351,7 +352,7 @@ mod tests { #[test] fn test_streaming_fuzzy_match() { let buffer = TextBuffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), indoc! {" function foo(a, b) { @@ -385,7 +386,7 @@ mod tests { #[test] fn test_incremental_improvement() { let buffer = TextBuffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", ); @@ -410,7 +411,7 @@ mod tests { #[test] fn test_incomplete_lines_buffering() { let buffer = TextBuffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), indoc! {" The quick brown fox @@ -437,7 +438,7 @@ mod tests { #[test] fn test_multiline_fuzzy_match() { let buffer = TextBuffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), indoc! {r#" impl Display for User { @@ -691,7 +692,11 @@ mod tests { } "#}; - let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string()); + let buffer = TextBuffer::new( + ReplicaId::LOCAL, + BufferId::new(1).unwrap(), + text.to_string(), + ); let snapshot = buffer.snapshot(); let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); @@ -724,7 +729,7 @@ mod tests { #[track_caller] fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) { let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false); - let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.clone()); + let buffer = TextBuffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text.clone()); let snapshot = buffer.snapshot(); let mut matcher = StreamingFuzzyMatcher::new(snapshot); diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 6c06cc2c8ec7f845b1e6d49631a1bea6755a62d0..5a1fa707ff04ac3b0cd719c3d0a5e67dfeb3e625 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -486,7 +486,7 @@ pub enum ContextSummary { Error, } -#[derive(Default, Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct ContextSummaryContent { pub text: String, pub done: bool, @@ -523,7 +523,11 @@ impl ContextSummary { match self { ContextSummary::Content(content) => content, ContextSummary::Pending | ContextSummary::Error => { - let content = ContextSummaryContent::default(); + let content = ContextSummaryContent { + text: "".to_string(), + done: false, + timestamp: clock::Lamport::MIN, + }; *self = ContextSummary::Content(content); self.content_as_mut().unwrap() } @@ -796,7 +800,7 @@ impl AssistantContext { }; let first_message_id = MessageId(clock::Lamport { - replica_id: 0, + replica_id: ReplicaId::LOCAL, value: 0, }); let message = MessageAnchor { @@ -2692,7 +2696,7 @@ impl AssistantContext { self.summary = ContextSummary::Content(ContextSummaryContent { text: "".to_string(), done: false, - timestamp: clock::Lamport::default(), + timestamp: clock::Lamport::MIN, }); replace_old = true; } @@ -3117,7 +3121,7 @@ impl SavedContext { let mut first_message_metadata = None; for message in self.messages { - if message.id == MessageId(clock::Lamport::default()) { + if message.id == MessageId(clock::Lamport::MIN) { first_message_metadata = Some(message.metadata); } else { operations.push(ContextOperation::InsertMessage { @@ -3141,7 +3145,7 @@ impl SavedContext { if let Some(metadata) = first_message_metadata { let timestamp = next_timestamp.tick(); operations.push(ContextOperation::UpdateMessage { - message_id: MessageId(clock::Lamport::default()), + message_id: MessageId(clock::Lamport::MIN), metadata: MessageMetadata { role: metadata.role, status: metadata.status, diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index 413e32dfcb14273920e9ae4110e5905bdbae5956..2d987f9f845b471438cfb3eb0667fbc36161c53c 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -741,7 +741,7 @@ async fn test_serialization(cx: &mut TestAppContext) { ); } -#[gpui::test(iterations = 100)] +#[gpui::test(iterations = 25)] async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) { cx.update(init_test); @@ -771,7 +771,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let context = cx.new(|cx| { AssistantContext::new( context_id.clone(), - i as ReplicaId, + ReplicaId::new(i as u16), language::Capability::ReadWrite, registry.clone(), prompt_builder.clone(), @@ -789,7 +789,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std if let ContextEvent::Operation(op) = event { network .lock() - .broadcast(i as ReplicaId, vec![op.to_proto()]); + .broadcast(ReplicaId::new(i as u16), vec![op.to_proto()]); } } }) @@ -797,7 +797,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std }); contexts.push(context); - network.lock().add_peer(i as ReplicaId); + network.lock().add_peer(ReplicaId::new(i as u16)); } let mut mutation_count = operations; @@ -943,9 +943,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std mutation_count -= 1; } _ => { - let replica_id = context_index as ReplicaId; + let replica_id = ReplicaId::new(context_index as u16); if network.lock().is_disconnected(replica_id) { - network.lock().reconnect_peer(replica_id, 0); + network.lock().reconnect_peer(replica_id, ReplicaId::new(0)); let (ops_to_send, ops_to_receive) = cx.read(|cx| { let host_context = &contexts[0].read(cx); @@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std network.lock().broadcast(replica_id, ops_to_send); context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx)); - } else if rng.random_bool(0.1) && replica_id != 0 { + } else if rng.random_bool(0.1) && replica_id != ReplicaId::new(0) { log::info!("Context {}: disconnecting", context_index); network.lock().disconnect_peer(replica_id); } else if network.lock().has_unreceived(replica_id) { @@ -996,25 +996,25 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std assert_eq!( context.buffer.read(cx).text(), first_context.buffer.read(cx).text(), - "Context {} text != Context 0 text", + "Context {:?} text != Context 0 text", context.buffer.read(cx).replica_id() ); assert_eq!( context.message_anchors, first_context.message_anchors, - "Context {} messages != Context 0 messages", + "Context {:?} messages != Context 0 messages", context.buffer.read(cx).replica_id() ); assert_eq!( context.messages_metadata, first_context.messages_metadata, - "Context {} message metadata != Context 0 message metadata", + "Context {:?} message metadata != Context 0 message metadata", context.buffer.read(cx).replica_id() ); assert_eq!( context.slash_command_output_sections, first_context.slash_command_output_sections, - "Context {} slash command output sections != Context 0 slash command output sections", + "Context {:?} slash command output sections != Context 0 slash command output sections", context.buffer.read(cx).replica_id() ); } diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 13479f6428b02d52f45415a989b694cc04ab5c25..b6883d39a76d212a9d9505999ad2fab9df2d9d82 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -85,7 +85,7 @@ struct PendingHunk { new_status: DiffHunkSecondaryStatus, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct DiffHunkSummary { buffer_range: Range, } @@ -114,7 +114,9 @@ impl sum_tree::Summary for DiffHunkSummary { type Context<'a> = &'a text::BufferSnapshot; fn zero(_cx: Self::Context<'_>) -> Self { - Default::default() + DiffHunkSummary { + buffer_range: Anchor::MIN..Anchor::MIN, + } } fn add_summary(&mut self, other: &Self, buffer: Self::Context<'_>) { @@ -937,7 +939,9 @@ impl BufferDiff { pub fn clear_pending_hunks(&mut self, cx: &mut Context) { if self.secondary_diff.is_some() { - self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default()); + self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary { + buffer_range: Anchor::MIN..Anchor::MIN, + }); cx.emit(BufferDiffEvent::DiffChanged { changed_range: Some(Anchor::MIN..Anchor::MAX), }); @@ -1368,7 +1372,7 @@ mod tests { use gpui::TestAppContext; use pretty_assertions::{assert_eq, assert_ne}; use rand::{Rng as _, rngs::StdRng}; - use text::{Buffer, BufferId, Rope}; + use text::{Buffer, BufferId, ReplicaId, Rope}; use unindent::Unindent as _; use util::test::marked_text_ranges; @@ -1393,7 +1397,7 @@ mod tests { " .unindent(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx); assert_hunks( diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer), @@ -1467,7 +1471,7 @@ mod tests { " .unindent(); - let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx); let mut uncommitted_diff = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx); @@ -1536,7 +1540,7 @@ mod tests { " .unindent(); - let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let diff = cx .update(|cx| { BufferDiffSnapshot::new_with_base_text( @@ -1799,7 +1803,7 @@ mod tests { for example in table { let (buffer_text, ranges) = marked_text_ranges(&example.buffer_marked_text, false); - let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text); let hunk_range = buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end); @@ -1872,7 +1876,11 @@ mod tests { " .unindent(); - let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone()); + let buffer = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(1).unwrap(), + buffer_text.clone(), + ); let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx); let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx); let unstaged_diff = cx.new(|cx| { @@ -1945,7 +1953,7 @@ mod tests { " .unindent(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text_1); let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx)); let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx); diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 828248b330b6ef6cfe0e13eab426de2900d364b2..efa0850753887c2116ee7916727a870a3528b627 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -9,7 +9,7 @@ use rpc::{ proto::{self, PeerId}, }; use std::{sync::Arc, time::Duration}; -use text::BufferId; +use text::{BufferId, ReplicaId}; use util::ResultExt; pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250); @@ -65,7 +65,12 @@ impl ChannelBuffer { let buffer = cx.new(|cx| { let capability = channel_store.read(cx).channel_capability(channel.id); - language::Buffer::remote(buffer_id, response.replica_id as u16, capability, base_text) + language::Buffer::remote( + buffer_id, + ReplicaId::new(response.replica_id as u16), + capability, + base_text, + ) })?; buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; @@ -272,7 +277,7 @@ impl ChannelBuffer { self.connected } - pub fn replica_id(&self, cx: &App) -> u16 { + pub fn replica_id(&self, cx: &App) -> ReplicaId { self.buffer.read(cx).replica_id() } } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index de0668b406c512eabfc70f4702466f013eb8c515..525a3e960ce8bc2aede4b0665af23ab3c33cac15 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -943,7 +943,7 @@ impl Collaborator { pub fn from_proto(message: proto::Collaborator) -> Result { Ok(Self { peer_id: message.peer_id.context("invalid peer id")?, - replica_id: message.replica_id as ReplicaId, + replica_id: ReplicaId::new(message.replica_id as u16), user_id: message.user_id as UserId, is_host: message.is_host, committer_name: message.committer_name, diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index 64645c9b46f68416c6792b17258baf8e49ca9585..a3cf2b819ec11e22ac533e7743ee884487ef9724 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -4,33 +4,73 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, - fmt, iter, + fmt, }; pub use system_clock::*; -pub const LOCAL_BRANCH_REPLICA_ID: u16 = u16::MAX; -pub const AGENT_REPLICA_ID: u16 = u16::MAX - 1; - /// A unique identifier for each distributed node. -pub type ReplicaId = u16; +#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] +pub struct ReplicaId(u16); + +impl ReplicaId { + /// The local replica + pub const LOCAL: ReplicaId = ReplicaId(0); + /// The remote replica of the connected remote server. + pub const REMOTE_SERVER: ReplicaId = ReplicaId(1); + /// The agent's unique identifier. + pub const AGENT: ReplicaId = ReplicaId(2); + /// A local branch. + pub const LOCAL_BRANCH: ReplicaId = ReplicaId(3); + /// The first collaborative replica ID, any replica equal or greater than this is a collaborative replica. + pub const FIRST_COLLAB_ID: ReplicaId = ReplicaId(Self::LOCAL_BRANCH.0 + 1); + + pub fn new(id: u16) -> Self { + ReplicaId(id) + } + + pub fn as_u16(&self) -> u16 { + self.0 + } + + pub fn is_remote(self) -> bool { + self == ReplicaId::REMOTE_SERVER || self >= ReplicaId::FIRST_COLLAB_ID + } +} + +impl fmt::Debug for ReplicaId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if *self == ReplicaId::LOCAL { + write!(f, "") + } else if *self == ReplicaId::REMOTE_SERVER { + write!(f, "") + } else if *self == ReplicaId::AGENT { + write!(f, "") + } else if *self == ReplicaId::LOCAL_BRANCH { + write!(f, "") + } else { + write!(f, "{}", self.0) + } + } +} /// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp). pub type Seq = u32; /// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp), /// used to determine the ordering of events in the editor. -#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)] pub struct Lamport { pub replica_id: ReplicaId, pub value: Seq, } -/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock). +/// A [version vector](https://en.wikipedia.org/wiki/Version_vector). #[derive(Clone, Default, Hash, Eq, PartialEq)] pub struct Global { - values: SmallVec<[u32; 8]>, - local_branch_value: u32, + // 4 is chosen as it is the biggest count that does not increase the size of the field itself. + // Coincidentally, it also covers all the important non-collab replica ids. + values: SmallVec<[u32; 4]>, } impl Global { @@ -38,30 +78,31 @@ impl Global { Self::default() } + /// Fetches the sequence number for the given replica ID. pub fn get(&self, replica_id: ReplicaId) -> Seq { - if replica_id == LOCAL_BRANCH_REPLICA_ID { - self.local_branch_value - } else { - self.values.get(replica_id as usize).copied().unwrap_or(0) as Seq - } + self.values.get(replica_id.0 as usize).copied().unwrap_or(0) as Seq } + /// Observe the lamport timestampe. + /// + /// This sets the current sequence number of the observed replica ID to the maximum of this global's observed sequence and the observed timestamp. pub fn observe(&mut self, timestamp: Lamport) { + debug_assert_ne!(timestamp.replica_id, Lamport::MAX.replica_id); if timestamp.value > 0 { - if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID { - self.local_branch_value = cmp::max(self.local_branch_value, timestamp.value); - } else { - let new_len = timestamp.replica_id as usize + 1; - if new_len > self.values.len() { - self.values.resize(new_len, 0); - } - - let entry = &mut self.values[timestamp.replica_id as usize]; - *entry = cmp::max(*entry, timestamp.value); + let new_len = timestamp.replica_id.0 as usize + 1; + if new_len > self.values.len() { + self.values.resize(new_len, 0); } + + let entry = &mut self.values[timestamp.replica_id.0 as usize]; + *entry = cmp::max(*entry, timestamp.value); } } + /// Join another global. + /// + /// This observes all timestamps from the other global. + #[doc(alias = "synchronize")] pub fn join(&mut self, other: &Self) { if other.values.len() > self.values.len() { self.values.resize(other.values.len(), 0); @@ -70,34 +111,36 @@ impl Global { for (left, right) in self.values.iter_mut().zip(&other.values) { *left = cmp::max(*left, *right); } - - self.local_branch_value = cmp::max(self.local_branch_value, other.local_branch_value); } + /// Meet another global. + /// + /// Sets all unobserved timestamps of this global to the sequences of other and sets all observed timestamps of this global to the minimum observed of both globals. pub fn meet(&mut self, other: &Self) { if other.values.len() > self.values.len() { self.values.resize(other.values.len(), 0); } let mut new_len = 0; - for (ix, (left, right)) in self - .values - .iter_mut() - .zip(other.values.iter().chain(iter::repeat(&0))) - .enumerate() - { - if *left == 0 { - *left = *right; - } else if *right > 0 { - *left = cmp::min(*left, *right); + for (ix, (left, &right)) in self.values.iter_mut().zip(&other.values).enumerate() { + match (*left, right) { + // left has not observed the replica + (0, _) => *left = right, + // right has not observed the replica + (_, 0) => (), + (_, _) => *left = cmp::min(*left, right), } - if *left != 0 { new_len = ix + 1; } } - self.values.resize(new_len, 0); - self.local_branch_value = cmp::min(self.local_branch_value, other.local_branch_value); + if other.values.len() == self.values.len() { + // only truncate if other was equal or shorter (which at this point + // cant be due to the resize above) to `self` as otherwise we would + // truncate the unprocessed tail that is guaranteed to contain + // non-null timestamps + self.values.truncate(new_len); + } } pub fn observed(&self, timestamp: Lamport) -> bool { @@ -105,20 +148,18 @@ impl Global { } pub fn observed_any(&self, other: &Self) -> bool { - self.values - .iter() - .zip(other.values.iter()) - .any(|(left, right)| *right > 0 && left >= right) - || (other.local_branch_value > 0 && self.local_branch_value >= other.local_branch_value) + self.iter() + .zip(other.iter()) + .any(|(left, right)| right.value > 0 && left.value >= right.value) } pub fn observed_all(&self, other: &Self) -> bool { - let mut rhs = other.values.iter(); - self.values.iter().all(|left| match rhs.next() { - Some(right) => left >= right, - None => true, - }) && rhs.next().is_none() - && self.local_branch_value >= other.local_branch_value + if self.values.len() < other.values.len() { + return false; + } + self.iter() + .zip(other.iter()) + .all(|(left, right)| left.value >= right.value) } pub fn changed_since(&self, other: &Self) -> bool { @@ -128,21 +169,21 @@ impl Global { .iter() .zip(other.values.iter()) .any(|(left, right)| left > right) - || self.local_branch_value > other.local_branch_value } + pub fn most_recent(&self) -> Option { + self.iter().max_by_key(|timestamp| timestamp.value) + } + + /// Iterates all replicas observed by this global as well as any unobserved replicas whose ID is lower than the highest observed replica. pub fn iter(&self) -> impl Iterator + '_ { self.values .iter() .enumerate() .map(|(replica_id, seq)| Lamport { - replica_id: replica_id as ReplicaId, + replica_id: ReplicaId(replica_id as u16), value: *seq, }) - .chain((self.local_branch_value > 0).then_some(Lamport { - replica_id: LOCAL_BRANCH_REPLICA_ID, - value: self.local_branch_value, - })) } } @@ -173,12 +214,12 @@ impl PartialOrd for Lamport { impl Lamport { pub const MIN: Self = Self { - replica_id: ReplicaId::MIN, + replica_id: ReplicaId(u16::MIN), value: Seq::MIN, }; pub const MAX: Self = Self { - replica_id: ReplicaId::MAX, + replica_id: ReplicaId(u16::MAX), value: Seq::MAX, }; @@ -190,7 +231,7 @@ impl Lamport { } pub fn as_u64(self) -> u64 { - ((self.value as u64) << 32) | (self.replica_id as u64) + ((self.value as u64) << 32) | (self.replica_id.0 as u64) } pub fn tick(&mut self) -> Self { @@ -211,7 +252,7 @@ impl fmt::Debug for Lamport { } else if *self == Self::MIN { write!(f, "Lamport {{MIN}}") } else { - write!(f, "Lamport {{{}: {}}}", self.replica_id, self.value) + write!(f, "Lamport {{{:?}: {}}}", self.replica_id, self.value) } } } @@ -220,16 +261,10 @@ impl fmt::Debug for Global { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Global {{")?; for timestamp in self.iter() { - if timestamp.replica_id > 0 { + if timestamp.replica_id.0 > 0 { write!(f, ", ")?; } - if timestamp.replica_id == LOCAL_BRANCH_REPLICA_ID { - write!(f, ": {}", timestamp.value)?; - } else if timestamp.replica_id == AGENT_REPLICA_ID { - write!(f, ": {}", timestamp.value)?; - } else { - write!(f, "{}: {}", timestamp.replica_id, timestamp.value)?; - } + write!(f, "{:?}: {}", timestamp.replica_id, timestamp.value)?; } write!(f, "}}") } diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 2e6b4719d1c126230849ac81bc1f215092bc0b5e..6c4cd58d132bdeaaa791f4da8406e0e6d9052981 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -62,9 +62,9 @@ impl Database { .iter() .map(|c| c.replica_id) .collect::>(); - let mut replica_id = ReplicaId(0); + let mut replica_id = ReplicaId(clock::ReplicaId::FIRST_COLLAB_ID.as_u16() as i32); while replica_ids.contains(&replica_id) { - replica_id.0 += 1; + replica_id = ReplicaId(replica_id.0 + 1); } let collaborator = channel_buffer_collaborator::ActiveModel { channel_id: ActiveValue::Set(channel_id), @@ -203,7 +203,7 @@ impl Database { while let Some(row) = rows.next().await { let row = row?; let timestamp = clock::Lamport { - replica_id: row.replica_id as u16, + replica_id: clock::ReplicaId::new(row.replica_id as u16), value: row.lamport_timestamp as u32, }; server_version.observe(timestamp); @@ -701,7 +701,11 @@ impl Database { return Ok(()); } - let mut text_buffer = text::Buffer::new(0, text::BufferId::new(1).unwrap(), base_text); + let mut text_buffer = text::Buffer::new( + clock::ReplicaId::LOCAL, + text::BufferId::new(1).unwrap(), + base_text, + ); text_buffer.apply_ops(operations.into_iter().filter_map(operation_from_wire)); let base_text = text_buffer.text(); @@ -934,7 +938,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option Some(text::Operation::Edit(EditOperation { timestamp: clock::Lamport { - replica_id: edit.replica_id as text::ReplicaId, + replica_id: clock::ReplicaId::new(edit.replica_id as u16), value: edit.lamport_timestamp, }, version: version_from_wire(&edit.version), @@ -949,7 +953,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option Some(text::Operation::Undo(UndoOperation { timestamp: clock::Lamport { - replica_id: undo.replica_id as text::ReplicaId, + replica_id: clock::ReplicaId::new(undo.replica_id as u16), value: undo.lamport_timestamp, }, version: version_from_wire(&undo.version), @@ -959,7 +963,7 @@ pub fn operation_from_wire(operation: proto::Operation) -> Option clock::Global { let mut version = clock::Global::new(); for entry in message { version.observe(clock::Lamport { - replica_id: entry.replica_id as text::ReplicaId, + replica_id: clock::ReplicaId::new(entry.replica_id as u16), value: entry.timestamp, }); } @@ -986,7 +990,7 @@ fn version_to_wire(version: &clock::Global) -> Vec { let mut message = Vec::new(); for entry in version.iter() { message.push(proto::VectorClockEntry { - replica_id: entry.replica_id as u32, + replica_id: entry.replica_id.as_u16() as u32, timestamp: entry.value, }); } diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index c1f9043a550ea4ea02a9fc241aed9e810f03d0e2..51a0ef83323ec70675283d2fdec7ca1ad791b12d 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -91,14 +91,18 @@ impl Database { .await?; } - let replica_id = if is_ssh_project { 1 } else { 0 }; + let replica_id = if is_ssh_project { + clock::ReplicaId::REMOTE_SERVER + } else { + clock::ReplicaId::LOCAL + }; project_collaborator::ActiveModel { project_id: ActiveValue::set(project.id), connection_id: ActiveValue::set(connection.id as i32), connection_server_id: ActiveValue::set(ServerId(connection.owner_id as i32)), user_id: ActiveValue::set(participant.user_id), - replica_id: ActiveValue::set(ReplicaId(replica_id)), + replica_id: ActiveValue::set(ReplicaId(replica_id.as_u16() as i32)), is_host: ActiveValue::set(true), id: ActiveValue::NotSet, committer_name: ActiveValue::Set(None), @@ -841,7 +845,7 @@ impl Database { .iter() .map(|c| c.replica_id) .collect::>(); - let mut replica_id = ReplicaId(1); + let mut replica_id = ReplicaId(clock::ReplicaId::FIRST_COLLAB_ID.as_u16() as i32); while replica_ids.contains(&replica_id) { replica_id.0 += 1; } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 49da0f37148cffc117c9423a8cbb18a65fe2d290..4eae7a54cba4a906351f05e5945cff5691fd1126 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -1,7 +1,7 @@ use super::*; use crate::test_both_dbs; use language::proto::{self, serialize_version}; -use text::Buffer; +use text::{Buffer, ReplicaId}; test_both_dbs!( test_channel_buffers, @@ -70,7 +70,11 @@ async fn test_channel_buffers(db: &Arc) { .await .unwrap(); - let mut buffer_a = Buffer::new(0, text::BufferId::new(1).unwrap(), "".to_string()); + let mut buffer_a = Buffer::new( + ReplicaId::new(0), + text::BufferId::new(1).unwrap(), + "".to_string(), + ); let operations = vec![ buffer_a.edit([(0..0, "hello world")]), buffer_a.edit([(5..5, ", cruel")]), @@ -95,7 +99,7 @@ async fn test_channel_buffers(db: &Arc) { .unwrap(); let mut buffer_b = Buffer::new( - 0, + ReplicaId::new(0), text::BufferId::new(1).unwrap(), buffer_response_b.base_text, ); @@ -124,7 +128,7 @@ async fn test_channel_buffers(db: &Arc) { rpc::proto::Collaborator { user_id: a_id.to_proto(), peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), - replica_id: 0, + replica_id: ReplicaId::FIRST_COLLAB_ID.as_u16() as u32, is_host: false, committer_name: None, committer_email: None, @@ -132,7 +136,7 @@ async fn test_channel_buffers(db: &Arc) { rpc::proto::Collaborator { user_id: b_id.to_proto(), peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), - replica_id: 1, + replica_id: ReplicaId::FIRST_COLLAB_ID.as_u16() as u32 + 1, is_host: false, committer_name: None, committer_email: None, @@ -228,7 +232,8 @@ async fn test_channel_buffers_last_operations(db: &Database) { .await .unwrap(); - db.join_channel_buffer(channel, user_id, connection_id) + let res = db + .join_channel_buffer(channel, user_id, connection_id) .await .unwrap(); @@ -239,7 +244,7 @@ async fn test_channel_buffers_last_operations(db: &Database) { ); text_buffers.push(Buffer::new( - 0, + ReplicaId::new(res.replica_id as u16), text::BufferId::new(1).unwrap(), "".to_string(), )); @@ -276,7 +281,12 @@ async fn test_channel_buffers_last_operations(db: &Database) { db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id) .await .unwrap(); - text_buffers[1] = Buffer::new(1, text::BufferId::new(1).unwrap(), "def".to_string()); + let replica_id = text_buffers[1].replica_id(); + text_buffers[1] = Buffer::new( + replica_id, + text::BufferId::new(1).unwrap(), + "def".to_string(), + ); update_buffer( buffers[1].channel_id, user_id, @@ -304,20 +314,32 @@ async fn test_channel_buffers_last_operations(db: &Database) { rpc::proto::ChannelBufferVersion { channel_id: buffers[0].channel_id.to_proto(), epoch: 0, - version: serialize_version(&text_buffers[0].version()), + version: serialize_version(&text_buffers[0].version()) + .into_iter() + .filter( + |vector| vector.replica_id == text_buffers[0].replica_id().as_u16() as u32 + ) + .collect::>(), }, rpc::proto::ChannelBufferVersion { channel_id: buffers[1].channel_id.to_proto(), epoch: 1, version: serialize_version(&text_buffers[1].version()) .into_iter() - .filter(|vector| vector.replica_id == text_buffers[1].replica_id() as u32) + .filter( + |vector| vector.replica_id == text_buffers[1].replica_id().as_u16() as u32 + ) .collect::>(), }, rpc::proto::ChannelBufferVersion { channel_id: buffers[2].channel_id.to_proto(), epoch: 0, - version: serialize_version(&text_buffers[2].version()), + version: serialize_version(&text_buffers[2].version()) + .into_iter() + .filter( + |vector| vector.replica_id == text_buffers[2].replica_id().as_u16() as u32 + ) + .collect::>(), }, ] ); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index c4532a93f1d50e91dbd4791b4621b74ee0813cbe..7aeb14fe0eef687ed375e28c6a726799e3876b12 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1278,7 +1278,7 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - position: text::Anchor::default(), + position: text::Anchor::MIN, padding_left: false, padding_right: false, tooltip: None, @@ -1298,7 +1298,7 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - position: text::Anchor::default(), + position: text::Anchor::MIN, padding_left: true, padding_right: true, tooltip: None, @@ -1318,7 +1318,7 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - position: text::Anchor::default(), + position: text::Anchor::MIN, padding_left: false, padding_right: false, tooltip: None, @@ -1338,7 +1338,7 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - position: text::Anchor::default(), + position: text::Anchor::MIN, padding_left: true, padding_right: true, tooltip: None, @@ -1361,7 +1361,7 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("🎨".to_string()), - position: text::Anchor::default(), + position: text::Anchor::MIN, padding_left: true, padding_right: true, tooltip: None, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9baa1c892e8dc7e0f62cdc2c0e7abbed82ae9cdd..3b2e30a1761af381c4a3a52e660f8d26dc043ce8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -82,7 +82,7 @@ use anyhow::{Context as _, Result, anyhow}; use blink_manager::BlinkManager; use buffer_diff::DiffHunkStatus; use client::{Collaborator, ParticipantIndex, parse_zed_link}; -use clock::{AGENT_REPLICA_ID, ReplicaId}; +use clock::ReplicaId; use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, CompletionsMenu, ContextMenuOrigin, @@ -1301,7 +1301,7 @@ enum SelectionHistoryMode { #[derive(Clone, PartialEq, Eq, Hash)] struct HoveredCursor { - replica_id: u16, + replica_id: ReplicaId, selection_id: usize, } @@ -23482,7 +23482,7 @@ impl EditorSnapshot { self.buffer_snapshot() .selections_in_range(range, false) .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { - if replica_id == AGENT_REPLICA_ID { + if replica_id == ReplicaId::AGENT { Some(RemoteSelection { replica_id, selection, diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 201a699e2f0e8527ed62babdc941febcf9426a2d..f89afb0a64b4377b235866c4da66e8255f2320d1 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -8,7 +8,7 @@ use gpui::{ }; use language::{ Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _, - Point, Rope, TextBuffer, + Point, ReplicaId, Rope, TextBuffer, }; use multi_buffer::PathKey; use project::{Project, WorktreeId, git_store::Repository}; @@ -135,7 +135,7 @@ impl CommitView { }); let buffer = cx.new(|cx| { let buffer = TextBuffer::new_normalized( - 0, + ReplicaId::LOCAL, cx.entity_id().as_non_zero_u64().into(), LineEnding::default(), format_commit(&commit).into(), @@ -316,7 +316,7 @@ async fn build_buffer( }; let buffer = cx.new(|cx| { let buffer = TextBuffer::new_normalized( - 0, + ReplicaId::LOCAL, cx.entity_id().as_non_zero_u64().into(), line_ending, text, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1605eea051b660f7285481223b0b3b9f97aef732..3b90ae6ba5df5484c79f90cf91649b9f363e92b6 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -18,8 +18,8 @@ pub use crate::{ proto, }; use anyhow::{Context as _, Result}; +use clock::Lamport; pub use clock::ReplicaId; -use clock::{AGENT_REPLICA_ID, Lamport}; use collections::HashMap; use fs::MTime; use futures::channel::oneshot; @@ -828,7 +828,11 @@ impl Buffer { /// Create a new buffer with the given base text. pub fn local>(base_text: T, cx: &Context) -> Self { Self::build( - TextBuffer::new(0, cx.entity_id().as_non_zero_u64().into(), base_text.into()), + TextBuffer::new( + ReplicaId::LOCAL, + cx.entity_id().as_non_zero_u64().into(), + base_text.into(), + ), None, Capability::ReadWrite, ) @@ -842,7 +846,7 @@ impl Buffer { ) -> Self { Self::build( TextBuffer::new_normalized( - 0, + ReplicaId::LOCAL, cx.entity_id().as_non_zero_u64().into(), line_ending, base_text_normalized, @@ -991,10 +995,10 @@ impl Buffer { language: None, remote_selections: Default::default(), diagnostics: Default::default(), - diagnostics_timestamp: Default::default(), + diagnostics_timestamp: Lamport::MIN, completion_triggers: Default::default(), completion_triggers_per_language_server: Default::default(), - completion_triggers_timestamp: Default::default(), + completion_triggers_timestamp: Lamport::MIN, deferred_ops: OperationQueue::new(), has_conflict: false, change_bits: Default::default(), @@ -1012,7 +1016,8 @@ impl Buffer { let buffer_id = entity_id.as_non_zero_u64().into(); async move { let text = - TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); + TextBuffer::new_normalized(ReplicaId::LOCAL, buffer_id, Default::default(), text) + .snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { let language_registry = language_registry.clone(); @@ -1033,8 +1038,13 @@ impl Buffer { pub fn build_empty_snapshot(cx: &mut App) -> BufferSnapshot { let entity_id = cx.reserve_entity::().entity_id(); let buffer_id = entity_id.as_non_zero_u64().into(); - let text = - TextBuffer::new_normalized(0, buffer_id, Default::default(), Rope::new()).snapshot(); + let text = TextBuffer::new_normalized( + ReplicaId::LOCAL, + buffer_id, + Default::default(), + Rope::new(), + ) + .snapshot(); let syntax = SyntaxMap::new(&text).snapshot(); BufferSnapshot { text, @@ -1056,7 +1066,9 @@ impl Buffer { ) -> BufferSnapshot { let entity_id = cx.reserve_entity::().entity_id(); let buffer_id = entity_id.as_non_zero_u64().into(); - let text = TextBuffer::new_normalized(0, buffer_id, Default::default(), text).snapshot(); + let text = + TextBuffer::new_normalized(ReplicaId::LOCAL, buffer_id, Default::default(), text) + .snapshot(); let mut syntax = SyntaxMap::new(&text).snapshot(); if let Some(language) = language.clone() { syntax.reparse(&text, language_registry, language); @@ -2260,7 +2272,7 @@ impl Buffer { ) { let lamport_timestamp = self.text.lamport_clock.tick(); self.remote_selections.insert( - AGENT_REPLICA_ID, + ReplicaId::AGENT, SelectionSet { selections, lamport_timestamp, @@ -2917,7 +2929,7 @@ impl Buffer { edits.push((range, new_text)); } - log::info!("mutating buffer {} with {:?}", self.replica_id(), edits); + log::info!("mutating buffer {:?} with {:?}", self.replica_id(), edits); self.edit(edits, None, cx); } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 0e23a69f075fc35b2f284c244a15428d57a354a4..f824639ad762191f4168586551af51fb4e37c8dc 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -70,7 +70,13 @@ fn test_line_endings(cx: &mut gpui::App) { fn test_set_line_ending(cx: &mut TestAppContext) { let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx)); let base_replica = cx.new(|cx| { - Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap() + Buffer::from_proto( + ReplicaId::new(1), + Capability::ReadWrite, + base.read(cx).to_proto(cx), + None, + ) + .unwrap() }); base.update(cx, |_buffer, cx| { cx.subscribe(&base_replica, |this, _, event, cx| { @@ -397,7 +403,7 @@ fn test_edit_events(cx: &mut gpui::App) { let buffer2 = cx.new(|cx| { Buffer::remote( BufferId::from(cx.entity_id().as_non_zero_u64()), - 1, + ReplicaId::new(1), Capability::ReadWrite, "abcdef", ) @@ -2775,7 +2781,8 @@ fn test_serialization(cx: &mut gpui::App) { .background_executor() .block(buffer1.read(cx).serialize_ops(None, cx)); let buffer2 = cx.new(|cx| { - let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap(); + let mut buffer = + Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None).unwrap(); buffer.apply_ops( ops.into_iter() .map(|op| proto::deserialize_operation(op).unwrap()), @@ -2794,7 +2801,13 @@ fn test_branch_and_merge(cx: &mut TestAppContext) { // Create a remote replica of the base buffer. let base_replica = cx.new(|cx| { - Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap() + Buffer::from_proto( + ReplicaId::new(1), + Capability::ReadWrite, + base.read(cx).to_proto(cx), + None, + ) + .unwrap() }); base.update(cx, |_buffer, cx| { cx.subscribe(&base_replica, |this, _, event, cx| { @@ -3108,7 +3121,8 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { .background_executor() .block(base_buffer.read(cx).serialize_ops(None, cx)); let mut buffer = - Buffer::from_proto(i as ReplicaId, Capability::ReadWrite, state, None).unwrap(); + Buffer::from_proto(ReplicaId::new(i as u16), Capability::ReadWrite, state, None) + .unwrap(); buffer.apply_ops( ops.into_iter() .map(|op| proto::deserialize_operation(op).unwrap()), @@ -3133,9 +3147,9 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { }); buffers.push(buffer); - replica_ids.push(i as ReplicaId); - network.lock().add_peer(i as ReplicaId); - log::info!("Adding initial peer with replica id {}", i); + replica_ids.push(ReplicaId::new(i as u16)); + network.lock().add_peer(ReplicaId::new(i as u16)); + log::info!("Adding initial peer with replica id {:?}", replica_ids[i]); } log::info!("initial text: {:?}", base_text); @@ -3155,14 +3169,14 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { buffer.start_transaction_at(now); buffer.randomly_edit(&mut rng, 5, cx); buffer.end_transaction_at(now, cx); - log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text()); + log::info!("buffer {:?} text: {:?}", buffer.replica_id(), buffer.text()); }); mutation_count -= 1; } 30..=39 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { if rng.random_bool(0.2) { - log::info!("peer {} clearing active selections", replica_id); + log::info!("peer {:?} clearing active selections", replica_id); active_selections.remove(&replica_id); buffer.remove_active_selections(cx); } else { @@ -3179,7 +3193,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { } let selections: Arc<[Selection]> = selections.into(); log::info!( - "peer {} setting active selections: {:?}", + "peer {:?} setting active selections: {:?}", replica_id, selections ); @@ -3189,7 +3203,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { }); mutation_count -= 1; } - 40..=49 if mutation_count != 0 && replica_id == 0 => { + 40..=49 if mutation_count != 0 && replica_id == ReplicaId::REMOTE_SERVER => { let entry_count = rng.random_range(1..=5); buffer.update(cx, |buffer, cx| { let diagnostics = DiagnosticSet::new( @@ -3207,7 +3221,11 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { }), buffer, ); - log::info!("peer {} setting diagnostics: {:?}", replica_id, diagnostics); + log::info!( + "peer {:?} setting diagnostics: {:?}", + replica_id, + diagnostics + ); buffer.update_diagnostics(LanguageServerId(0), diagnostics, cx); }); mutation_count -= 1; @@ -3217,12 +3235,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { let old_buffer_ops = cx .background_executor() .block(buffer.read(cx).serialize_ops(None, cx)); - let new_replica_id = (0..=replica_ids.len() as ReplicaId) + let new_replica_id = (0..=replica_ids.len() as u16) + .map(ReplicaId::new) .filter(|replica_id| *replica_id != buffer.read(cx).replica_id()) .choose(&mut rng) .unwrap(); log::info!( - "Adding new replica {} (replicating from {})", + "Adding new replica {:?} (replicating from {:?})", new_replica_id, replica_id ); @@ -3241,7 +3260,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { cx, ); log::info!( - "New replica {} text: {:?}", + "New replica {:?} text: {:?}", new_buffer.replica_id(), new_buffer.text() ); @@ -3264,7 +3283,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { })); network.lock().replicate(replica_id, new_replica_id); - if new_replica_id as usize == replica_ids.len() { + if new_replica_id.as_u16() as usize == replica_ids.len() { replica_ids.push(new_replica_id); } else { let new_buffer = new_buffer.take().unwrap(); @@ -3276,7 +3295,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { .map(|op| proto::deserialize_operation(op).unwrap()); if ops.len() > 0 { log::info!( - "peer {} (version: {:?}) applying {} ops from the network. {:?}", + "peer {:?} (version: {:?}) applying {} ops from the network. {:?}", new_replica_id, buffer.read(cx).version(), ops.len(), @@ -3287,13 +3306,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { }); } } - buffers[new_replica_id as usize] = new_buffer; + buffers[new_replica_id.as_u16() as usize] = new_buffer; } } 60..=69 if mutation_count != 0 => { buffer.update(cx, |buffer, cx| { buffer.randomly_undo_redo(&mut rng, cx); - log::info!("buffer {} text: {:?}", buffer.replica_id(), buffer.text()); + log::info!("buffer {:?} text: {:?}", buffer.replica_id(), buffer.text()); }); mutation_count -= 1; } @@ -3305,7 +3324,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { .map(|op| proto::deserialize_operation(op).unwrap()); if ops.len() > 0 { log::info!( - "peer {} (version: {:?}) applying {} ops from the network. {:?}", + "peer {:?} (version: {:?}) applying {} ops from the network. {:?}", replica_id, buffer.read(cx).version(), ops.len(), @@ -3335,13 +3354,13 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { assert_eq!( buffer.version(), first_buffer.version(), - "Replica {} version != Replica 0 version", + "Replica {:?} version != Replica 0 version", buffer.replica_id() ); assert_eq!( buffer.text(), first_buffer.text(), - "Replica {} text != Replica 0 text", + "Replica {:?} text != Replica 0 text", buffer.replica_id() ); assert_eq!( @@ -3351,7 +3370,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { first_buffer .diagnostics_in_range::<_, usize>(0..first_buffer.len(), false) .collect::>(), - "Replica {} diagnostics != Replica 0 diagnostics", + "Replica {:?} diagnostics != Replica 0 diagnostics", buffer.replica_id() ); } @@ -3370,7 +3389,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) { assert_eq!( actual_remote_selections, expected_remote_selections, - "Replica {} remote selections != expected selections", + "Replica {:?} remote selections != expected selections", buffer.replica_id() ); } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index bc85b10859632fc3e2cf61c663b7159a023f4f3a..5c8200b84002c104ce1e2c3d1a42aff5876bd1ee 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -39,14 +39,14 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { crate::Operation::Buffer(text::Operation::Undo(undo)) => { proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: undo.timestamp.replica_id as u32, + replica_id: undo.timestamp.replica_id.as_u16() as u32, lamport_timestamp: undo.timestamp.value, version: serialize_version(&undo.version), counts: undo .counts .iter() .map(|(edit_id, count)| proto::UndoCount { - replica_id: edit_id.replica_id as u32, + replica_id: edit_id.replica_id.as_u16() as u32, lamport_timestamp: edit_id.value, count: *count, }) @@ -60,7 +60,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { lamport_timestamp, cursor_shape, } => proto::operation::Variant::UpdateSelections(proto::operation::UpdateSelections { - replica_id: lamport_timestamp.replica_id as u32, + replica_id: lamport_timestamp.replica_id.as_u16() as u32, lamport_timestamp: lamport_timestamp.value, selections: serialize_selections(selections), line_mode: *line_mode, @@ -72,7 +72,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { server_id, diagnostics, } => proto::operation::Variant::UpdateDiagnostics(proto::UpdateDiagnostics { - replica_id: lamport_timestamp.replica_id as u32, + replica_id: lamport_timestamp.replica_id.as_u16() as u32, lamport_timestamp: lamport_timestamp.value, server_id: server_id.0 as u64, diagnostics: serialize_diagnostics(diagnostics.iter()), @@ -84,7 +84,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { server_id, } => proto::operation::Variant::UpdateCompletionTriggers( proto::operation::UpdateCompletionTriggers { - replica_id: lamport_timestamp.replica_id as u32, + replica_id: lamport_timestamp.replica_id.as_u16() as u32, lamport_timestamp: lamport_timestamp.value, triggers: triggers.clone(), language_server_id: server_id.to_proto(), @@ -95,7 +95,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { line_ending, lamport_timestamp, } => proto::operation::Variant::UpdateLineEnding(proto::operation::UpdateLineEnding { - replica_id: lamport_timestamp.replica_id as u32, + replica_id: lamport_timestamp.replica_id.as_u16() as u32, lamport_timestamp: lamport_timestamp.value, line_ending: serialize_line_ending(*line_ending) as i32, }), @@ -106,7 +106,7 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation { /// Serializes an [`EditOperation`] to be sent over RPC. pub fn serialize_edit_operation(operation: &EditOperation) -> proto::operation::Edit { proto::operation::Edit { - replica_id: operation.timestamp.replica_id as u32, + replica_id: operation.timestamp.replica_id.as_u16() as u32, lamport_timestamp: operation.timestamp.value, version: serialize_version(&operation.version), ranges: operation.ranges.iter().map(serialize_range).collect(), @@ -123,12 +123,12 @@ pub fn serialize_undo_map_entry( (edit_id, counts): (&clock::Lamport, &[(clock::Lamport, u32)]), ) -> proto::UndoMapEntry { proto::UndoMapEntry { - replica_id: edit_id.replica_id as u32, + replica_id: edit_id.replica_id.as_u16() as u32, local_timestamp: edit_id.value, counts: counts .iter() .map(|(undo_id, count)| proto::UndoCount { - replica_id: undo_id.replica_id as u32, + replica_id: undo_id.replica_id.as_u16() as u32, lamport_timestamp: undo_id.value, count: *count, }) @@ -246,7 +246,7 @@ pub fn serialize_diagnostics<'a>( /// Serializes an [`Anchor`] to be sent over RPC. pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { proto::Anchor { - replica_id: anchor.timestamp.replica_id as u32, + replica_id: anchor.timestamp.replica_id.as_u16() as u32, timestamp: anchor.timestamp.value, offset: anchor.offset as u64, bias: match anchor.bias { @@ -283,7 +283,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result { crate::Operation::Buffer(text::Operation::Undo(UndoOperation { timestamp: clock::Lamport { - replica_id: undo.replica_id as ReplicaId, + replica_id: ReplicaId::new(undo.replica_id as u16), value: undo.lamport_timestamp, }, version: deserialize_version(&undo.version), @@ -293,7 +293,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result Result Result { crate::Operation::UpdateDiagnostics { lamport_timestamp: clock::Lamport { - replica_id: message.replica_id as ReplicaId, + replica_id: ReplicaId::new(message.replica_id as u16), value: message.lamport_timestamp, }, server_id: LanguageServerId(message.server_id as usize), @@ -344,7 +344,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result Result { crate::Operation::UpdateLineEnding { lamport_timestamp: clock::Lamport { - replica_id: message.replica_id as ReplicaId, + replica_id: ReplicaId::new(message.replica_id as u16), value: message.lamport_timestamp, }, line_ending: deserialize_line_ending( @@ -370,7 +370,7 @@ pub fn deserialize_operation(message: proto::Operation) -> Result EditOperation { EditOperation { timestamp: clock::Lamport { - replica_id: edit.replica_id as ReplicaId, + replica_id: ReplicaId::new(edit.replica_id as u16), value: edit.lamport_timestamp, }, version: deserialize_version(&edit.version), @@ -385,7 +385,7 @@ pub fn deserialize_undo_map_entry( ) -> (clock::Lamport, Vec<(clock::Lamport, u32)>) { ( clock::Lamport { - replica_id: entry.replica_id as u16, + replica_id: ReplicaId::new(entry.replica_id as u16), value: entry.local_timestamp, }, entry @@ -394,7 +394,7 @@ pub fn deserialize_undo_map_entry( .map(|undo_count| { ( clock::Lamport { - replica_id: undo_count.replica_id as u16, + replica_id: ReplicaId::new(undo_count.replica_id as u16), value: undo_count.lamport_timestamp, }, undo_count.count, @@ -480,7 +480,7 @@ pub fn deserialize_anchor(anchor: proto::Anchor) -> Option { }; Some(Anchor { timestamp: clock::Lamport { - replica_id: anchor.replica_id as ReplicaId, + replica_id: ReplicaId::new(anchor.replica_id as u16), value: anchor.timestamp, }, offset: anchor.offset as usize, @@ -524,7 +524,7 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option Result proto::LamportTimestamp { proto::LamportTimestamp { - replica_id: timestamp.replica_id as u32, + replica_id: timestamp.replica_id.as_u16() as u32, value: timestamp.value, } } @@ -567,7 +567,7 @@ pub fn serialize_timestamp(timestamp: clock::Lamport) -> proto::LamportTimestamp /// Deserializes a [`clock::Lamport`] timestamp from the RPC representation. pub fn deserialize_timestamp(timestamp: proto::LamportTimestamp) -> clock::Lamport { clock::Lamport { - replica_id: timestamp.replica_id as ReplicaId, + replica_id: ReplicaId::new(timestamp.replica_id as u16), value: timestamp.value, } } @@ -590,7 +590,7 @@ pub fn deserialize_version(message: &[proto::VectorClockEntry]) -> clock::Global let mut version = clock::Global::new(); for entry in message { version.observe(clock::Lamport { - replica_id: entry.replica_id as ReplicaId, + replica_id: ReplicaId::new(entry.replica_id as u16), value: entry.timestamp, }); } @@ -602,7 +602,7 @@ pub fn serialize_version(version: &clock::Global) -> Vec (Buf .now_or_never() .unwrap() .unwrap(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), ""); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); let mut mutated_syntax_map = SyntaxMap::new(&buffer); mutated_syntax_map.set_language_registry(registry.clone()); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 18a619212dba49bf1c384679ec90120af80e0e2a..94966708ac0e49fc01c7e1617fdd41149fc0a4e9 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -666,7 +666,7 @@ impl MultiBuffer { paths_by_excerpt: Default::default(), buffer_changed_since_sync: Default::default(), history: History { - next_transaction_id: clock::Lamport::default(), + next_transaction_id: clock::Lamport::MIN, undo_stack: Vec::new(), redo_stack: Vec::new(), transaction_depth: 0, diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 1532e5b68ec29e6befbbd17fa97b966449874822..49db1fc2e264583f90f1a96195c560f0e52e8205 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -78,7 +78,9 @@ fn test_remote(cx: &mut App) { let ops = cx .background_executor() .block(host_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap(); + let mut buffer = + Buffer::from_proto(ReplicaId::REMOTE_SERVER, Capability::ReadWrite, state, None) + .unwrap(); buffer.apply_ops( ops.into_iter() .map(|op| language::proto::deserialize_operation(op).unwrap()), @@ -3636,7 +3638,7 @@ fn assert_position_translation(snapshot: &MultiBufferSnapshot) { fn assert_line_indents(snapshot: &MultiBufferSnapshot) { let max_row = snapshot.max_point().row; let buffer_id = snapshot.excerpts().next().unwrap().1.remote_id(); - let text = text::Buffer::new(0, buffer_id, snapshot.text()); + let text = text::Buffer::new(ReplicaId::LOCAL, buffer_id, snapshot.text()); let mut line_indents = text .line_indents_in_row_range(0..max_row + 1) .collect::>(); diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 8a4d4f7918c12abd94cf7bf8fc97c939db7ce033..9c7caa6280c6502a5279a48d63e1ee4f42d9e11c 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -25,7 +25,7 @@ use rpc::{ }; use smol::channel::Receiver; use std::{io, pin::pin, sync::Arc, time::Instant}; -use text::BufferId; +use text::{BufferId, ReplicaId}; use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath}; use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; @@ -158,7 +158,7 @@ impl RemoteBufferStore { pub fn handle_create_buffer_for_peer( &mut self, envelope: TypedEnvelope, - replica_id: u16, + replica_id: ReplicaId, capability: Capability, cx: &mut Context, ) -> Result>> { @@ -626,7 +626,9 @@ impl LocalBufferStore { cx.spawn(async move |_, cx| { let loaded = load_file.await?; let text_buffer = cx - .background_spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) }) + .background_spawn(async move { + text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) + }) .await; cx.insert_entity(reservation, |_| { Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) @@ -639,7 +641,7 @@ impl LocalBufferStore { Ok(buffer) => Ok(buffer), Err(error) if is_not_found_error(&error) => cx.new(|cx| { let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); - let text_buffer = text::Buffer::new(0, buffer_id, ""); + let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, ""); Buffer::build( text_buffer, Some(Arc::new(File { @@ -917,7 +919,7 @@ impl BufferStore { path: file.path.clone(), worktree_id: file.worktree_id(cx), }); - let is_remote = buffer.replica_id() != 0; + let is_remote = buffer.replica_id().is_remote(); let open_buffer = OpenBuffer::Complete { buffer: buffer_entity.downgrade(), }; @@ -1317,7 +1319,7 @@ impl BufferStore { pub fn handle_create_buffer_for_peer( &mut self, envelope: TypedEnvelope, - replica_id: u16, + replica_id: ReplicaId, capability: Capability, cx: &mut Context, ) -> Result<()> { diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index 879280c885a0bfda20e5faa70e1e07f7d9fe038c..160a384a4a0ff4481c97b6eda75faded28f01624 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -271,7 +271,7 @@ mod tests { use language::language_settings::AllLanguageSettings; use serde_json::json; use settings::Settings as _; - use text::{Buffer, BufferId, Point, ToOffset as _}; + use text::{Buffer, BufferId, Point, ReplicaId, ToOffset as _}; use unindent::Unindent as _; use util::{path, rel_path::rel_path}; use worktree::WorktreeSettings; @@ -299,7 +299,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content); + let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -374,7 +374,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content); + let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -405,7 +405,7 @@ mod tests { >>>>>>> "# .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content); + let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); @@ -447,7 +447,7 @@ mod tests { .unindent(); let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(0, buffer_id, test_content.clone()); + let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content.clone()); let snapshot = buffer.snapshot(); let conflict_snapshot = ConflictSet::parse(&snapshot); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index bdce7c26fd4b40dd37fd9bf67a03490bdf3c7bc5..c68d14d38866b2fbe8a97792bdf46a94469124a1 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3025,9 +3025,8 @@ impl LocalLspStore { Some(buffer_to_edit.read(cx).saved_version().clone()) }; - let most_recent_edit = version.and_then(|version| { - version.iter().max_by_key(|timestamp| timestamp.value) - }); + let most_recent_edit = + version.and_then(|version| version.most_recent()); // Check if the edit that triggered that edit has been made by this participant. if let Some(most_recent_edit) = most_recent_edit { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 56a2811f07a4c3f37c610df48bb7c6db1904f9f2..4a7ac5fa50fac05bb81e5374fd213a6e4ff5bda4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1560,7 +1560,7 @@ impl Project { })?; let agent_server_store = cx.new(|cx| AgentServerStore::collab(cx))?; - let replica_id = response.payload.replica_id as ReplicaId; + let replica_id = ReplicaId::new(response.payload.replica_id as u16); let project = cx.new(|cx| { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); @@ -1975,9 +1975,9 @@ impl Project { ProjectClientState::Remote { replica_id, .. } => replica_id, _ => { if self.remote_client.is_some() { - 1 + ReplicaId::REMOTE_SERVER } else { - 0 + ReplicaId::LOCAL } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 24612d974d43fa2d8b9ad7bf188c7e3b51726f25..43f233b2d1fdb8ffdeca6fe7e40d7c28a8e3084c 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4307,7 +4307,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) { let remote = cx.update(|cx| { Worktree::remote( 0, - 1, + ReplicaId::REMOTE_SERVER, metadata, project.read(cx).client().into(), project.read(cx).path_style(cx), diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 670b405ed33757117ec62bfbbb4c947f79e5026a..e6da207dadbde3ebc725fbb84ed19b3b35414f87 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -551,7 +551,7 @@ impl WorktreeStore { let worktree = cx.update(|cx| { Worktree::remote( REMOTE_SERVER_PROJECT_ID, - 0, + ReplicaId::REMOTE_SERVER, proto::WorktreeMetadata { id: response.worktree_id, root_name, diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 56172c21afcf9fa70a9039218feea59f055a27c5..6b0db2f9352997cd5cef4544edc388bc9c5cd209 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -6,7 +6,7 @@ use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::{Bias, Dimensions}; /// A timestamped position in a buffer -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] pub struct Anchor { pub timestamp: clock::Lamport, /// The byte offset in the buffer diff --git a/crates/text/src/operation_queue.rs b/crates/text/src/operation_queue.rs index 6604817edfe2dcc243ba837a770b361bd505a7ef..f87af381ff314f91469a9b5d438e667fe6ea190f 100644 --- a/crates/text/src/operation_queue.rs +++ b/crates/text/src/operation_queue.rs @@ -1,3 +1,4 @@ +use clock::Lamport; use std::{fmt::Debug, ops::Add}; use sum_tree::{ContextLessSummary, Dimension, Edit, Item, KeyedItem, SumTree}; @@ -11,10 +12,10 @@ struct OperationItem(T); #[derive(Clone, Debug)] pub struct OperationQueue(SumTree>); -#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct OperationKey(clock::Lamport); -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct OperationSummary { pub key: OperationKey, pub len: usize, @@ -69,7 +70,10 @@ impl OperationQueue { impl ContextLessSummary for OperationSummary { fn zero() -> Self { - Default::default() + OperationSummary { + key: OperationKey::new(Lamport::MIN), + len: 0, + } } fn add_summary(&mut self, other: &Self) { @@ -93,7 +97,7 @@ impl Add<&Self> for OperationSummary { impl Dimension<'_, OperationSummary> for OperationKey { fn zero(_cx: ()) -> Self { - Default::default() + OperationKey::new(Lamport::MIN) } fn add_summary(&mut self, summary: &OperationSummary, _: ()) { @@ -123,11 +127,13 @@ impl KeyedItem for OperationItem { #[cfg(test)] mod tests { + use clock::ReplicaId; + use super::*; #[test] fn test_len() { - let mut clock = clock::Lamport::new(0); + let mut clock = clock::Lamport::new(ReplicaId::LOCAL); let mut queue = OperationQueue::new(); assert_eq!(queue.len(), 0); diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 4298e704ab5f8fbe57af363379395ef23624cfcf..c9e04e407ffdb8ffde6b139e01d78822e54e1a4b 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -16,7 +16,7 @@ fn init_logger() { #[test] fn test_edit() { - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "abc"); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "abc"); assert_eq!(buffer.text(), "abc"); buffer.edit([(3..3, "def")]); assert_eq!(buffer.text(), "abcdef"); @@ -40,7 +40,11 @@ fn test_random_edits(mut rng: StdRng) { let mut reference_string = RandomCharIter::new(&mut rng) .take(reference_string_len) .collect::(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), reference_string.clone()); + let mut buffer = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(1).unwrap(), + reference_string.clone(), + ); LineEnding::normalize(&mut reference_string); buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200))); @@ -176,7 +180,11 @@ fn test_line_endings() { LineEnding::Windows ); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "one\r\ntwo\rthree"); + let mut buffer = Buffer::new( + ReplicaId::LOCAL, + BufferId::new(1).unwrap(), + "one\r\ntwo\rthree", + ); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); buffer.check_invariants(); @@ -190,7 +198,7 @@ fn test_line_endings() { #[test] fn test_line_len() { - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), ""); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); buffer.edit([(0..0, "abcd\nefg\nhij")]); buffer.edit([(12..12, "kl\nmno")]); buffer.edit([(18..18, "\npqrs\n")]); @@ -207,7 +215,7 @@ fn test_line_len() { #[test] fn test_common_prefix_at_position() { let text = "a = str; b = δα"; - let buffer = Buffer::new(0, BufferId::new(1).unwrap(), text); + let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text); let offset1 = offset_after(text, "str"); let offset2 = offset_after(text, "δα"); @@ -256,7 +264,7 @@ fn test_common_prefix_at_position() { #[test] fn test_text_summary_for_range() { let buffer = Buffer::new( - 0, + ReplicaId::LOCAL, BufferId::new(1).unwrap(), "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ); @@ -348,7 +356,7 @@ fn test_text_summary_for_range() { #[test] fn test_chars_at() { - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), ""); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); buffer.edit([(0..0, "abcd\nefgh\nij")]); buffer.edit([(12..12, "kl\nmno")]); buffer.edit([(18..18, "\npqrs")]); @@ -370,7 +378,7 @@ fn test_chars_at() { assert_eq!(chars.collect::(), "PQrs"); // Regression test: - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), ""); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]); buffer.edit([(60..60, "\n")]); @@ -380,7 +388,7 @@ fn test_chars_at() { #[test] fn test_anchors() { - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), ""); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); buffer.edit([(0..0, "abc")]); let left_anchor = buffer.anchor_before(2); let right_anchor = buffer.anchor_after(2); @@ -498,7 +506,7 @@ fn test_anchors() { #[test] fn test_anchors_at_start_and_end() { - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), ""); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); let before_start_anchor = buffer.anchor_before(0); let after_end_anchor = buffer.anchor_after(0); @@ -521,7 +529,7 @@ fn test_anchors_at_start_and_end() { #[test] fn test_undo_redo() { - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234"); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234"); // Set group interval to zero so as to not group edits in the undo stack. buffer.set_group_interval(Duration::from_secs(0)); @@ -558,7 +566,7 @@ fn test_undo_redo() { #[test] fn test_history() { let mut now = Instant::now(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456"); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "123456"); buffer.set_group_interval(Duration::from_millis(300)); let transaction_1 = buffer.start_transaction_at(now).unwrap(); @@ -625,7 +633,7 @@ fn test_history() { #[test] fn test_finalize_last_transaction() { let now = Instant::now(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "123456"); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "123456"); buffer.history.group_interval = Duration::from_millis(1); buffer.start_transaction_at(now); @@ -661,7 +669,7 @@ fn test_finalize_last_transaction() { #[test] fn test_edited_ranges_for_transaction() { let now = Instant::now(); - let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), "1234567"); + let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234567"); buffer.start_transaction_at(now); buffer.edit([(2..4, "cd")]); @@ -700,9 +708,9 @@ fn test_edited_ranges_for_transaction() { fn test_concurrent_edits() { let text = "abcdef"; - let mut buffer1 = Buffer::new(1, BufferId::new(1).unwrap(), text); - let mut buffer2 = Buffer::new(2, BufferId::new(1).unwrap(), text); - let mut buffer3 = Buffer::new(3, BufferId::new(1).unwrap(), text); + let mut buffer1 = Buffer::new(ReplicaId::new(1), BufferId::new(1).unwrap(), text); + let mut buffer2 = Buffer::new(ReplicaId::new(2), BufferId::new(1).unwrap(), text); + let mut buffer3 = Buffer::new(ReplicaId::new(3), BufferId::new(1).unwrap(), text); let buf1_op = buffer1.edit([(1..2, "12")]); assert_eq!(buffer1.text(), "a12cdef"); @@ -741,11 +749,15 @@ fn test_random_concurrent_edits(mut rng: StdRng) { let mut network = Network::new(rng.clone()); for i in 0..peers { - let mut buffer = Buffer::new(i as ReplicaId, BufferId::new(1).unwrap(), base_text.clone()); + let mut buffer = Buffer::new( + ReplicaId::new(i as u16), + BufferId::new(1).unwrap(), + base_text.clone(), + ); buffer.history.group_interval = Duration::from_millis(rng.random_range(0..=200)); buffers.push(buffer); - replica_ids.push(i as u16); - network.add_peer(i as u16); + replica_ids.push(ReplicaId::new(i as u16)); + network.add_peer(ReplicaId::new(i as u16)); } log::info!("initial text: {:?}", base_text); @@ -759,7 +771,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) { 0..=50 if mutation_count != 0 => { let op = buffer.randomly_edit(&mut rng, 5).1; network.broadcast(buffer.replica_id, vec![op]); - log::info!("buffer {} text: {:?}", buffer.replica_id, buffer.text()); + log::info!("buffer {:?} text: {:?}", buffer.replica_id, buffer.text()); mutation_count -= 1; } 51..=70 if mutation_count != 0 => { @@ -771,7 +783,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) { let ops = network.receive(replica_id); if !ops.is_empty() { log::info!( - "peer {} applying {} ops from the network.", + "peer {:?} applying {} ops from the network.", replica_id, ops.len() ); @@ -792,7 +804,7 @@ fn test_random_concurrent_edits(mut rng: StdRng) { assert_eq!( buffer.text(), first_buffer.text(), - "Replica {} text != Replica 0 text", + "Replica {:?} text != Replica 0 text", buffer.replica_id ); buffer.check_invariants(); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 2eacef5ae037e0d45c53404f30d8c81bdedf4dc1..0516bf21c949db266aad025500f51aab9cec0958 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -12,7 +12,7 @@ mod undo_map; pub use anchor::*; use anyhow::{Context as _, Result}; -use clock::LOCAL_BRANCH_REPLICA_ID; +use clock::Lamport; pub use clock::ReplicaId; use collections::{HashMap, HashSet}; use locator::Locator; @@ -573,7 +573,7 @@ struct InsertionFragment { fragment_id: Locator, } -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] struct InsertionFragmentKey { timestamp: clock::Lamport, split_offset: usize, @@ -709,7 +709,7 @@ impl FromIterator for LineIndent { } impl Buffer { - pub fn new(replica_id: u16, remote_id: BufferId, base_text: impl Into) -> Buffer { + pub fn new(replica_id: ReplicaId, remote_id: BufferId, base_text: impl Into) -> Buffer { let mut base_text = base_text.into(); let line_ending = LineEnding::detect(&base_text); LineEnding::normalize(&mut base_text); @@ -717,7 +717,7 @@ impl Buffer { } pub fn new_normalized( - replica_id: u16, + replica_id: ReplicaId, remote_id: BufferId, line_ending: LineEnding, normalized: Rope, @@ -731,10 +731,7 @@ impl Buffer { let visible_text = history.base_text.clone(); if !visible_text.is_empty() { - let insertion_timestamp = clock::Lamport { - replica_id: 0, - value: 1, - }; + let insertion_timestamp = clock::Lamport::new(ReplicaId::LOCAL); lamport_clock.observe(insertion_timestamp); version.observe(insertion_timestamp); let fragment_id = Locator::between(&Locator::min(), &Locator::max()); @@ -788,7 +785,7 @@ impl Buffer { history: History::new(self.base_text().clone()), deferred_ops: OperationQueue::new(), deferred_replicas: HashSet::default(), - lamport_clock: clock::Lamport::new(LOCAL_BRANCH_REPLICA_ID), + lamport_clock: clock::Lamport::new(ReplicaId::LOCAL_BRANCH), subscriptions: Default::default(), edit_id_resolvers: Default::default(), wait_for_version_txs: Default::default(), @@ -1254,7 +1251,7 @@ impl Buffer { for edit_id in edit_ids { let insertion_slice = InsertionSlice { edit_id: *edit_id, - insertion_id: clock::Lamport::default(), + insertion_id: clock::Lamport::MIN, range: 0..0, }; let slices = self @@ -1858,7 +1855,7 @@ impl Buffer { T: rand::Rng, { let mut edits = self.get_random_edits(rng, edit_count); - log::info!("mutating buffer {} with {:?}", self.replica_id, edits); + log::info!("mutating buffer {:?} with {:?}", self.replica_id, edits); let op = self.edit(edits.iter().cloned()); if let Operation::Edit(edit) = &op { @@ -1881,7 +1878,7 @@ impl Buffer { if let Some(entry) = self.history.undo_stack.choose(rng) { let transaction = entry.transaction.clone(); log::info!( - "undoing buffer {} transaction {:?}", + "undoing buffer {:?} transaction {:?}", self.replica_id, transaction ); @@ -2918,7 +2915,10 @@ impl InsertionFragment { impl sum_tree::ContextLessSummary for InsertionFragmentKey { fn zero() -> Self { - Default::default() + InsertionFragmentKey { + timestamp: Lamport::MIN, + split_offset: 0, + } } fn add_summary(&mut self, summary: &Self) { diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs index 60b22a9edba70b65d30c60a0b9ca15b8f286cc85..2c2eba8de62ace68b5953b832a2a29be2317175e 100644 --- a/crates/text/src/undo_map.rs +++ b/crates/text/src/undo_map.rs @@ -1,4 +1,5 @@ use crate::UndoOperation; +use clock::Lamport; use std::cmp; use sum_tree::{Bias, SumTree}; @@ -24,7 +25,7 @@ impl sum_tree::KeyedItem for UndoMapEntry { } } -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] struct UndoMapKey { edit_id: clock::Lamport, undo_id: clock::Lamport, @@ -32,7 +33,10 @@ struct UndoMapKey { impl sum_tree::ContextLessSummary for UndoMapKey { fn zero() -> Self { - Default::default() + UndoMapKey { + edit_id: Lamport::MIN, + undo_id: Lamport::MIN, + } } fn add_summary(&mut self, summary: &Self) { @@ -69,7 +73,7 @@ impl UndoMap { cursor.seek( &UndoMapKey { edit_id, - undo_id: Default::default(), + undo_id: Lamport::MIN, }, Bias::Left, ); @@ -93,7 +97,7 @@ impl UndoMap { cursor.seek( &UndoMapKey { edit_id, - undo_id: Default::default(), + undo_id: Lamport::MIN, }, Bias::Left, ); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 447dc0eeb5fe69aa8a936a5850e788d315a69bac..f889fb3b8218b18983c4509b653cf0e0ce863fcd 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -656,7 +656,7 @@ impl Worktree { pub fn replica_id(&self) -> ReplicaId { match self { - Worktree::Local(_) => 0, + Worktree::Local(_) => ReplicaId::LOCAL, Worktree::Remote(worktree) => worktree.replica_id, } } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 1d48571d7b06f35d82934122919e75bbbd087ffa..454a1526a9e8c6a75d47bda875feb6843b454a0d 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1581,7 +1581,7 @@ fn guess_token_count(bytes: usize) -> usize { #[cfg(test)] mod tests { use client::test::FakeServer; - use clock::FakeSystemClock; + use clock::{FakeSystemClock, ReplicaId}; use cloud_api_types::{CreateLlmTokenResponse, LlmToken}; use gpui::TestAppContext; use http_client::FakeHttpClient; @@ -1839,7 +1839,7 @@ mod tests { let buffer = cx.new(|_cx| { Buffer::remote( language::BufferId::new(1).unwrap(), - 1, + ReplicaId::new(1), language::Capability::ReadWrite, "fn main() {\n println!(\"Hello\");\n}", ) From 9a72453a2b54b849c6984c178f6f0564305b4787 Mon Sep 17 00:00:00 2001 From: Hayashi Mikihiro <34ttrweoewiwe28@gmail.com> Date: Mon, 20 Oct 2025 20:28:54 +0900 Subject: [PATCH 047/202] Highlight control flow in Rust/C/C++ (#39683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit part of https://github.com/zed-industries/zed/issues/9461 Release Notes: - Added the ability to seperately highlight control flow keywords for Rust, C and C++ for users and theme authors via the `keyword.control` syntax property スクリーンショット 2025-10-07 22 21 59 --- crates/languages/src/c/highlights.scm | 25 ++++++++------- crates/languages/src/cpp/highlights.scm | 39 +++++++++++++----------- crates/languages/src/rust/highlights.scm | 25 ++++++++------- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/crates/languages/src/c/highlights.scm b/crates/languages/src/c/highlights.scm index b80c462ae6d32974d27d8a532bf6edd15ba86a82..40e0d7147e98287f5ed7587d690e25bc8bacaa0b 100644 --- a/crates/languages/src/c/highlights.scm +++ b/crates/languages/src/c/highlights.scm @@ -1,27 +1,30 @@ +[ + "const" + "enum" + "extern" + "inline" + "sizeof" + "static" + "struct" + "typedef" + "union" + "volatile" +] @keyword + [ "break" "case" - "const" "continue" "default" "do" "else" - "enum" - "extern" "for" "goto" "if" - "inline" "return" - "sizeof" - "static" - "struct" "switch" - "typedef" - "union" - "volatile" "while" -] @keyword +] @keyword.control [ "#define" diff --git a/crates/languages/src/cpp/highlights.scm b/crates/languages/src/cpp/highlights.scm index bd988445bb155e8851ffa8bc3771bdd235fc7dff..af906e67122333b6e1834f1280d4458189daf105 100644 --- a/crates/languages/src/cpp/highlights.scm +++ b/crates/languages/src/cpp/highlights.scm @@ -106,32 +106,19 @@ type: (primitive_type) @type.builtin [ "alignas" "alignof" - "break" - "case" - "catch" "class" - "co_await" - "co_return" - "co_yield" "concept" "consteval" "constexpr" "constinit" - "continue" "decltype" - "default" "delete" - "do" - "else" "enum" "explicit" "export" "extern" "final" - "for" "friend" - "goto" - "if" "import" "inline" "module" @@ -144,24 +131,40 @@ type: (primitive_type) @type.builtin "protected" "public" "requires" - "return" "sizeof" "struct" - "switch" "template" "thread_local" - "throw" - "try" "typedef" "typename" "union" "using" "virtual" - "while" (storage_class_specifier) (type_qualifier) ] @keyword +[ + "break" + "case" + "catch" + "co_await" + "co_return" + "co_yield" + "continue" + "default" + "do" + "else" + "for" + "goto" + "if" + "return" + "switch" + "throw" + "try" + "while" +] @keyword.control + [ "#define" "#elif" diff --git a/crates/languages/src/rust/highlights.scm b/crates/languages/src/rust/highlights.scm index b0daac71a097b922aa810aadef64a18e95b5b649..36f638e825b117673bd88b3abaf75d0fc433f4e7 100644 --- a/crates/languages/src/rust/highlights.scm +++ b/crates/languages/src/rust/highlights.scm @@ -83,29 +83,20 @@ "as" "async" "await" - "break" "const" - "continue" "default" "dyn" - "else" "enum" "extern" "fn" - "for" - "if" "impl" - "in" "let" - "loop" "macro_rules!" - "match" "mod" "move" "pub" "raw" "ref" - "return" "static" "struct" "trait" @@ -114,13 +105,25 @@ "unsafe" "use" "where" - "while" - "yield" (crate) (mutable_specifier) (super) ] @keyword +[ + "break" + "continue" + "else" + "for" + "if" + "in" + "loop" + "match" + "return" + "while" + "yield" +] @keyword.control + [ (string_literal) (raw_string_literal) From b827d8cfc0e9c03c3e5b6233393ca392dbb8df86 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:35:28 -0300 Subject: [PATCH 048/202] ai onboarding: Add dismiss button to the sign in banner (#40660) Release Notes: - N/A --- crates/ai_onboarding/src/ai_onboarding.rs | 87 +++++++---------------- 1 file changed, 26 insertions(+), 61 deletions(-) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index d953ae612199cea15f6718bf3d5cd7dd55ef856e..20bb0a5f6895ea225cad59ad8fef6cc6ef168b39 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -84,10 +84,32 @@ impl ZedAiOnboarding { self } + fn render_dismiss_button(&self) -> Option { + self.dismiss_onboarding.as_ref().map(|dismiss_callback| { + let callback = dismiss_callback.clone(); + + h_flex() + .absolute() + .top_0() + .right_0() + .child( + IconButton::new("dismiss_onboarding", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Dismiss")) + .on_click(move |_, window, cx| { + telemetry::event!("Banner Dismissed", source = "AI Onboarding",); + callback(window, cx) + }), + ) + .into_any_element() + }) + } + fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); v_flex() + .relative() .gap_1() .child(Headline::new("Welcome to Zed AI")) .child( @@ -109,6 +131,7 @@ impl ZedAiOnboarding { } }), ) + .children(self.render_dismiss_button()) .into_any_element() } @@ -180,27 +203,7 @@ impl ZedAiOnboarding { ) .child(PlanDefinitions.free_plan(is_v2)), ) - .when_some( - self.dismiss_onboarding.as_ref(), - |this, dismiss_callback| { - let callback = dismiss_callback.clone(); - - this.child( - h_flex().absolute().top_0().right_0().child( - IconButton::new("dismiss_onboarding", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| { - telemetry::event!( - "Banner Dismissed", - source = "AI Onboarding", - ); - callback(window, cx) - }), - ), - ) - }, - ) + .children(self.render_dismiss_button()) .child( v_flex() .mt_2() @@ -245,26 +248,7 @@ impl ZedAiOnboarding { .mb_2(), ) .child(PlanDefinitions.pro_trial(is_v2, false)) - .when_some( - self.dismiss_onboarding.as_ref(), - |this, dismiss_callback| { - let callback = dismiss_callback.clone(); - this.child( - h_flex().absolute().top_0().right_0().child( - IconButton::new("dismiss_onboarding", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| { - telemetry::event!( - "Banner Dismissed", - source = "AI Onboarding", - ); - callback(window, cx) - }), - ), - ) - }, - ) + .children(self.render_dismiss_button()) .into_any_element() } @@ -278,26 +262,7 @@ impl ZedAiOnboarding { .mb_2(), ) .child(PlanDefinitions.pro_plan(is_v2, false)) - .when_some( - self.dismiss_onboarding.as_ref(), - |this, dismiss_callback| { - let callback = dismiss_callback.clone(); - this.child( - h_flex().absolute().top_0().right_0().child( - IconButton::new("dismiss_onboarding", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| { - telemetry::event!( - "Banner Dismissed", - source = "AI Onboarding", - ); - callback(window, cx) - }), - ), - ) - }, - ) + .children(self.render_dismiss_button()) .into_any_element() } } From bdb7c642a12793d3a5231b1336baa823e46dd777 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 13:59:35 +0200 Subject: [PATCH 049/202] clock: Bump the min collaborator ID (#40694) This allows us to play with IDs < 8 without having to do another redeploy Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/clock/src/clock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index a3cf2b819ec11e22ac533e7743ee884487ef9724..04a7c7c881bf47bf901bc9738deafda18ab0ff21 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -23,7 +23,7 @@ impl ReplicaId { /// A local branch. pub const LOCAL_BRANCH: ReplicaId = ReplicaId(3); /// The first collaborative replica ID, any replica equal or greater than this is a collaborative replica. - pub const FIRST_COLLAB_ID: ReplicaId = ReplicaId(Self::LOCAL_BRANCH.0 + 1); + pub const FIRST_COLLAB_ID: ReplicaId = ReplicaId(8); pub fn new(id: u16) -> Self { ReplicaId(id) From cc210897367ca4c7cbeeb6be1a1fdb4ca7a158c8 Mon Sep 17 00:00:00 2001 From: localcc Date: Mon, 20 Oct 2025 14:09:36 +0200 Subject: [PATCH 050/202] Fix default window size on small displays (#40398) Fixed: #40039 Fixed: #40404 Fixed: #40272 Fixed: #40666 Release Notes: - N/A --- crates/gpui/src/platform.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 6847a34a843395a18531bcf3114d1dfe24f7bf29..047a005548ba0c7c34f6641a91333723883203cc 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -289,10 +289,13 @@ pub trait PlatformDisplay: Send + Sync + Debug { /// Get the default bounds for this display to place a window fn default_bounds(&self) -> Bounds { - let center = self.bounds().center(); - let offset = DEFAULT_WINDOW_SIZE / 2.0; + let bounds = self.bounds(); + let center = bounds.center(); + let clipped_window_size = DEFAULT_WINDOW_SIZE.min(&bounds.size); + + let offset = clipped_window_size / 2.0; let origin = point(center.x - offset.width, center.y - offset.height); - Bounds::new(origin, DEFAULT_WINDOW_SIZE) + Bounds::new(origin, clipped_window_size) } } From 9a3c7945a9a5d2708b81a6d6b4efea1e98d9195c Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:11:11 -0300 Subject: [PATCH 051/202] docs: Add section about MCP servers with external agents (#40658) Adding this content after seeing people ask about how to make MCP servers installed from Zed be picked up by external agents. At the moment, this varies depending on the agent, and felt relevant to be documented. Release Notes: - N/A --- docs/src/ai/external-agents.md | 12 ++++++++++-- docs/src/ai/mcp.md | 19 +++++++++++++------ docs/src/ai/overview.md | 8 ++++---- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index 508fd5211a119b1d36fe038f8b7d3a2d2b45d532..054ffb08f7605cd0f690dfbddc9f83e42a1f91a1 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -3,9 +3,10 @@ Zed supports terminal-based agents through the [Agent Client Protocol (ACP)](https://agentclientprotocol.com). Currently, [Gemini CLI](https://github.com/google-gemini/gemini-cli) serves as the reference implementation. -[Claude Code](https://www.anthropic.com/claude-code) is also included by default, and you can [add custom ACP-compatible agents](#add-custom-agents) as well. +[Claude Code](https://www.anthropic.com/claude-code) and [Codex](https://developers.openai.com/codex) are also included by default, and you can [add custom ACP-compatible agents](#add-custom-agents) as well. -Zed's affordance for external agents is strictly UI-based; the billing and legal/terms arrangement is directly between you and the agent provider. Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models. +> Note that Zed's affordance for external agents is strictly UI-based; the billing and legal/terms arrangement is directly between you and the agent provider. +> Zed does not charge for use of external agents, and our [zero-data retention agreements/privacy guarantees](./ai-improvement.md) are **_only_** applicable for Zed's hosted models. ## Gemini CLI {#gemini-cli} @@ -206,3 +207,10 @@ You can also specify a custom path, arguments, or environment for the builtin in When using external agents in Zed, you can access the debug view via with `dev: open acp logs` from the Command Palette. This lets you see the messages being sent and received between Zed and the agent. ![The debug view for ACP logs.](https://zed.dev/img/acp/acp-logs.webp) + +## MCP Servers + +Note that for external agents, access to MCP servers [installed from Zed](./mcp.md) may vary depending on the ACP agent implementation. + +Regarding the built-in ones, Claude Code and Codex both support it, and Gemini CLI does not yet. +In the meantime, learn how to add MCP server support to Gemini CLI through [their documentation](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#using-mcp-servers). diff --git a/docs/src/ai/mcp.md b/docs/src/ai/mcp.md index d11b666993aaf8d445e9a1ef3f83156217b3b2db..8fa36675ec46ed6ae1830dd32196815c34ab587f 100644 --- a/docs/src/ai/mcp.md +++ b/docs/src/ai/mcp.md @@ -11,7 +11,7 @@ Check out the [Anthropic news post](https://www.anthropic.com/news/model-context ### As Extensions One of the ways you can use MCP servers in Zed is by exposing them as an extension. -To learn how to create your own, check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page for more details. +Check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page to learn how to create your own. Thanks to our awesome community, many MCP servers have already been added as extensions. You can check which ones are available via any of these routes: @@ -20,7 +20,7 @@ You can check which ones are available via any of these routes: 2. in the app, open the Command Palette and run the `zed: extensions` action 3. in the app, go to the Agent Panel's top-right menu and look for the "View Server Extensions" menu item -In any case, here are some of the ones available: +In any case, here are some popular available servers: - [Context7](https://zed.dev/extensions/context7-mcp-server) - [GitHub](https://zed.dev/extensions/github-mcp-server) @@ -57,9 +57,9 @@ From there, you can add it through the modal that appears when you click the "Ad ### Configuration Check -Regardless of how you've installed MCP servers, whether as an extension or adding them directly, most servers out there still require some sort of configuration as part of the set up process. +Regardless of how you've installed MCP servers, whether as an extension or adding them directly, most servers out there still require some sort of configuration as part of the setup process. -In the case of server extensions, after installing it, Zed will pop up a modal displaying what is required for you to properly set it up. +In the case of extensions, after installing it, Zed will pop up a modal displaying what is required for you to properly set it up. For example, the GitHub MCP extension requires you to add a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). In the case of custom servers, make sure you check the provider documentation to determine what type of command, arguments, and environment variables need to be added to the JSON. @@ -68,14 +68,14 @@ To check if your MCP server is properly configured, go to the Agent Panel's sett If they're running correctly, the indicator will be green and its tooltip will say "Server is active". If not, other colors and tooltip messages will indicate what is happening. -### Using it in the Agent Panel +### Agent Panel Usage Once installation is complete, you can return to the Agent Panel and start prompting. Some models are better than others when it comes to picking up tools from MCP servers. Mentioning your server by name always helps the model to pick it up. -However, if you want to ensure a given MCP server will be used, you can create [a custom profile](./agent-panel.md#custom-profiles) where all built-in tools (or the ones that could cause conflicts with the server's tools) are turned off and only the tools coming from the MCP server are turned on. +However, if you want to _ensure_ a given MCP server will be used, you can create [a custom profile](./agent-panel.md#custom-profiles) where all built-in tools (or the ones that could cause conflicts with the server's tools) are turned off and only the tools coming from the MCP server are turned on. As an example, [the Dagger team suggests](https://container-use.com/agent-integrations#zed) doing that with their [Container Use MCP server](https://zed.dev/extensions/mcp-server-container-use): @@ -127,3 +127,10 @@ As an example, [the Dagger team suggests](https://container-use.com/agent-integr Zed's Agent Panel includes the `agent.always_allow_tool_actions` setting that, if set to `false`, will require you to give permission for any editing attempt as well as tool calls coming from MCP servers. You can change this by setting this key to `true` in either your `settings.json` or through the Agent Panel's settings view. + +### External Agents + +Note that for [external agents](./external-agents.md) connected through the [Agent Client Protocol](https://agentclientprotocol.com/), access to MCP servers installed from Zed may vary depending on the ACP agent implementation. + +Regarding the built-in ones, Claude Code and Codex both support it, and Gemini CLI does not yet. +In the meantime, learn how to add MCP server support to Gemini CLI through [their documentation](https://github.com/google-gemini/gemini-cli?tab=readme-ov-file#using-mcp-servers). diff --git a/docs/src/ai/overview.md b/docs/src/ai/overview.md index ca06a4b1ed53d1fc87136a1d5e82da35552082aa..e1a9cb77a996e5e2d42de7f825cd100535b8dcc6 100644 --- a/docs/src/ai/overview.md +++ b/docs/src/ai/overview.md @@ -18,11 +18,11 @@ Learn how to get started using AI with Zed and all its capabilities. - [Rules](./rules.md): How to define rules for AI interactions. -- [Tools](./tools.md): Explore the tools that enable agentic capabilities. +- [Tools](./tools.md): Explore the tools that power Zed's built-in agent. -- [Model Context Protocol](./mcp.md): Learn about how to install and configure MCP servers. +- [Model Context Protocol](./mcp.md): Learn about how to configure and use MCP servers. -- [Inline Assistant](./inline-assistant.md): Discover how to use the agent to power inline transformations directly within a file or terminal. +- [Inline Assistant](./inline-assistant.md): Discover how to use AI to generate inline transformations directly within a file or terminal. ## Edit Prediction @@ -30,4 +30,4 @@ Learn how to get started using AI with Zed and all its capabilities. ## Text Threads -- [Text Threads](./text-threads.md): Learn about an alternative, text-based interface for interacting with language models. +- [Text Threads](./text-threads.md): Learn about an editor-based interface for interacting with language models. From 96415e2d19df23f9efe293f6ced57a667a6c003b Mon Sep 17 00:00:00 2001 From: Alex Zeier Date: Mon, 20 Oct 2025 05:40:05 -0700 Subject: [PATCH 052/202] languages: Separate control flow keywords for JS/TS/TSX (#39801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to #9461, inspired by #39683 `await` and `yield` both seem somewhat debatable on whether they should be considered the be control flow keywords. For now I went with: - `await`: no – The control flow effect of `await` is at a level does not seem relevant for syntax highlighting. - `yield`: yes – `yield` directly affects the output of a generator, and is also included for consistency with Rust (#39683). Happy to change these either direction. SCR-20251008-izus --- Release Notes: - Improved granularity of keyword highlighting for JS/TS/TSX: Themes can now specify `keyword.control` for control flow keywords like `if`, `else`, `return`, etc. --- .../languages/src/javascript/highlights.scm | 35 +++++++++++-------- crates/languages/src/tsx/highlights.scm | 35 +++++++++++-------- .../languages/src/typescript/highlights.scm | 35 +++++++++++-------- 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index ebeac7efffb8770616fbc94ee4bbf3c25275a198..e5b84ab68df2b32061691f469046569a6597750e 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -171,47 +171,52 @@ "as" "async" "await" - "break" - "case" - "catch" "class" "const" - "continue" "debugger" "default" "delete" - "do" - "else" "export" "extends" - "finally" - "for" "from" "function" "get" - "if" "import" "in" "instanceof" "let" "new" "of" - "return" "set" "static" - "switch" "target" - "throw" - "try" "typeof" "using" "var" "void" - "while" "with" - "yield" ] @keyword +[ + "break" + "case" + "catch" + "continue" + "do" + "else" + "finally" + "for" + "if" + "return" + "switch" + "throw" + "try" + "while" + "yield" +] @keyword.control + +(switch_default "default" @keyword.control) + (template_substitution "${" @punctuation.special "}" @punctuation.special) @embedded diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index f7cb987831578f1d3e78decbf89f71c91d3a3b7e..ef12b3d7913e07109e32bb5bf41909511aa2b555 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -171,25 +171,16 @@ "as" "async" "await" - "break" - "case" - "catch" "class" "const" - "continue" "debugger" "default" "delete" - "do" - "else" "export" "extends" - "finally" - "for" "from" "function" "get" - "if" "import" "in" "instanceof" @@ -197,23 +188,37 @@ "let" "new" "of" - "return" "satisfies" "set" "static" - "switch" "target" - "throw" - "try" "typeof" "using" "var" "void" - "while" "with" - "yield" ] @keyword +[ + "break" + "case" + "catch" + "continue" + "do" + "else" + "finally" + "for" + "if" + "return" + "switch" + "throw" + "try" + "while" + "yield" +] @keyword.control + +(switch_default "default" @keyword.control) + (template_substitution "${" @punctuation.special "}" @punctuation.special) @embedded diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index 84cbbae77d43c96e62578c444ee913055604e11a..8a85dfea07fe4f50cb271f65ec1bdeeaf2ea150c 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -218,27 +218,18 @@ "as" "async" "await" - "break" - "case" - "catch" "class" "const" - "continue" "debugger" "declare" "default" "delete" - "do" - "else" "enum" "export" "extends" - "finally" - "for" "from" "function" "get" - "if" "implements" "import" "in" @@ -257,20 +248,34 @@ "protected" "public" "readonly" - "return" "satisfies" "set" "static" - "switch" "target" - "throw" - "try" "type" "typeof" "using" "var" "void" - "while" "with" - "yield" ] @keyword + +[ + "break" + "case" + "catch" + "continue" + "do" + "else" + "finally" + "for" + "if" + "return" + "switch" + "throw" + "try" + "while" + "yield" +] @keyword.control + +(switch_default "default" @keyword.control) From 08ecaa3931b24931c70ddfdb2bbbb4975e2c4251 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Mon, 20 Oct 2025 08:45:51 -0400 Subject: [PATCH 053/202] Add comment language injection for supported languages (#39884) Release Notes: - Added comment language injections for builtin languages. This enables highlighting of `TODO`s and similar notes with the comment extension installed. Signed-off-by: Donnie Adams --- crates/languages/src/bash/injections.scm | 3 +++ crates/languages/src/c/injections.scm | 4 ++++ crates/languages/src/cpp/injections.scm | 4 ++++ crates/languages/src/gitcommit/injections.scm | 4 ++++ crates/languages/src/go/injections.scm | 4 ++++ crates/languages/src/javascript/injections.scm | 4 ++++ crates/languages/src/python/injections.scm | 3 +++ crates/languages/src/tsx/injections.scm | 4 ++++ crates/languages/src/typescript/injections.scm | 4 ++++ crates/languages/src/yaml/injections.scm | 3 +++ extensions/html/languages/html/injections.scm | 4 ++++ 11 files changed, 41 insertions(+) create mode 100644 crates/languages/src/bash/injections.scm create mode 100644 crates/languages/src/python/injections.scm create mode 100644 crates/languages/src/yaml/injections.scm diff --git a/crates/languages/src/bash/injections.scm b/crates/languages/src/bash/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..9117c713b98fdd2896b13e4949a77c6489b9ee36 --- /dev/null +++ b/crates/languages/src/bash/injections.scm @@ -0,0 +1,3 @@ +((comment) @injection.content + (#set! injection.language "comment") +) diff --git a/crates/languages/src/c/injections.scm b/crates/languages/src/c/injections.scm index 73d2628225f05db94d53381fc9a9e10c29b6189d..447897340cc735ed77099b20fd6fc8c52ac19ec8 100644 --- a/crates/languages/src/c/injections.scm +++ b/crates/languages/src/c/injections.scm @@ -1,3 +1,7 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + (preproc_def value: (preproc_arg) @injection.content (#set! injection.language "c")) diff --git a/crates/languages/src/cpp/injections.scm b/crates/languages/src/cpp/injections.scm index e903e1affd53a5c641a30736599ebedb2f53169f..160770f3cc1d69f5cb3d1679c8a48726d8d437ed 100644 --- a/crates/languages/src/cpp/injections.scm +++ b/crates/languages/src/cpp/injections.scm @@ -1,3 +1,7 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + (preproc_def value: (preproc_arg) @injection.content (#set! injection.language "c++")) diff --git a/crates/languages/src/gitcommit/injections.scm b/crates/languages/src/gitcommit/injections.scm index db0af176578cfe1ba50db0cc7543d9b805ed8163..8fb9b459679489be7588d1ab9b6d53e40ea10c60 100644 --- a/crates/languages/src/gitcommit/injections.scm +++ b/crates/languages/src/gitcommit/injections.scm @@ -1,3 +1,7 @@ +((comment) @content + (#set! injection.language "comment") +) + ((scissors) @content (#set! "language" "diff")) diff --git a/crates/languages/src/go/injections.scm b/crates/languages/src/go/injections.scm index 7bb68d760e1a556ef93a9477dc578c88d9350dcb..52edce417798bcc8cd9cbc38ba3443ff3fc561c6 100644 --- a/crates/languages/src/go/injections.scm +++ b/crates/languages/src/go/injections.scm @@ -1,4 +1,8 @@ ; Refer to https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/go/injections.scm#L4C1-L16C41 +((comment) @injection.content + (#set! injection.language "comment") +) + (call_expression (selector_expression) @_function (#any-of? @_function diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index 987be660d3c5ebd706284990d7d21a481b24a2af..f79cd788d78964f61f611023d0645c95c88aaf17 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -1,3 +1,7 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + (((comment) @_jsdoc_comment (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content (#set! injection.language "jsdoc")) diff --git a/crates/languages/src/python/injections.scm b/crates/languages/src/python/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..9117c713b98fdd2896b13e4949a77c6489b9ee36 --- /dev/null +++ b/crates/languages/src/python/injections.scm @@ -0,0 +1,3 @@ +((comment) @injection.content + (#set! injection.language "comment") +) diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index f749aac43a713dadc6abe81a0523f241610b2675..3cca9e8e81c31d3565554595456fa62be89bc81f 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -1,3 +1,7 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + (((comment) @_jsdoc_comment (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content (#set! injection.language "jsdoc")) diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index f98e36b72d049080eb98ffe2b69a67c6b852e4a7..5321e606c118a41df127c8aa37c7c2811dc8bd23 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -1,3 +1,7 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + (((comment) @_jsdoc_comment (#match? @_jsdoc_comment "(?s)^/[*][*][^*].*[*]/$")) @injection.content (#set! injection.language "jsdoc")) diff --git a/crates/languages/src/yaml/injections.scm b/crates/languages/src/yaml/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..9117c713b98fdd2896b13e4949a77c6489b9ee36 --- /dev/null +++ b/crates/languages/src/yaml/injections.scm @@ -0,0 +1,3 @@ +((comment) @injection.content + (#set! injection.language "comment") +) diff --git a/extensions/html/languages/html/injections.scm b/extensions/html/languages/html/injections.scm index 0884d8f516706ee3058636931073d9aac1c3016a..525b3efe29dca541afc8829dd41ff217f48439c3 100644 --- a/extensions/html/languages/html/injections.scm +++ b/extensions/html/languages/html/injections.scm @@ -1,3 +1,7 @@ +((comment) @injection.content + (#set! injection.language "comment") +) + (script_element (raw_text) @injection.content (#set! injection.language "javascript")) From 85c2aa7325e39ea95eea9adefe3cb2c51591a8f0 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 20 Oct 2025 15:52:05 +0200 Subject: [PATCH 054/202] Update to acp 0.5 (#40701) Release Notes: - N/A --- Cargo.lock | 75 ++++++++++++++++++++++--------- Cargo.toml | 2 +- crates/acp_tools/src/acp_tools.rs | 9 ++-- crates/agent_servers/src/acp.rs | 2 +- 4 files changed, 62 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea89f8f36231676bdb32c65d528d0c8f8cde5787..99ae6a0315c2320db18ebdc70c8c19ececbdbbe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ dependencies = [ "context_server", "ctor", "db", - "derive_more", + "derive_more 0.99.20", "editor", "env_logger 0.11.8", "fs", @@ -210,16 +210,30 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aaa2bd05a2401887945f8bfd70026e90bc3cf96c62ab9eba2779835bf21dc60" +checksum = "2f655394a107cd601bd2e5375c2d909ea83adc65678a0e0e8d77613d3c848a7d" dependencies = [ + "agent-client-protocol-schema", "anyhow", "async-broadcast", "async-trait", + "derive_more 2.0.1", "futures 0.3.31", "log", "parking_lot", + "serde", + "serde_json", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61be4454304d7df1a5b44c4ae55e707ffe72eac4dfb1ef8762510ce8d8f6d924" +dependencies = [ + "anyhow", + "derive_more 2.0.1", "schemars 1.0.4", "serde", "serde_json", @@ -842,7 +856,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more", + "derive_more 0.99.20", "extension", "futures 0.3.31", "gpui", @@ -2064,7 +2078,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "log", "prettyplease", "proc-macro2", @@ -2084,7 +2098,7 @@ dependencies = [ "bitflags 2.9.4", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "log", "prettyplease", "proc-macro2", @@ -3079,7 +3093,7 @@ dependencies = [ "cloud_llm_client", "collections", "credentials_provider", - "derive_more", + "derive_more 0.99.20", "feature_flags", "fs", "futures 0.3.31", @@ -3539,7 +3553,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more", + "derive_more 0.99.20", "gpui", "workspace", ] @@ -4856,6 +4870,27 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "unicode-xid", +] + [[package]] name = "derive_refineable" version = "0.1.0" @@ -5002,7 +5037,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5606,7 +5641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6883,7 +6918,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more", + "derive_more 0.99.20", "futures 0.3.31", "git2", "gpui", @@ -7149,7 +7184,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more", + "derive_more 0.99.20", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7648,7 +7683,7 @@ dependencies = [ "async-fs", "async-tar", "bytes 1.10.1", - "derive_more", + "derive_more 0.99.20", "futures 0.3.31", "http 1.3.1", "http-body 1.0.1", @@ -10302,7 +10337,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -13291,7 +13326,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -14374,7 +14409,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -15159,7 +15194,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.20", "ec4rs", "fs", "futures 0.3.31", @@ -16824,7 +16859,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -16952,7 +16987,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.20", "fs", "futures 0.3.31", "gpui", @@ -19506,7 +19541,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5f4a72a5a6f62b407dd7a796bf5eb79b3f441559..a76cb403bc41d1ec068050789e8a0c5af6d61d58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -438,7 +438,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agent-client-protocol = { version = "=0.4.3", features = ["unstable"] } +agent-client-protocol = { version = "0.5.0", features = ["unstable"] } aho-corasick = "1.1" alacritty_terminal = "0.25.1-rc1" any_vec = "0.14" diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index 7ba4f555a2a42303f82cfdc1f8e24860ed3e1d69..69722815306e412745a62832115d2f010b2b8607 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -93,8 +93,8 @@ struct WatchedConnection { messages: Vec, list_state: ListState, connection: Weak, - incoming_request_methods: HashMap>, - outgoing_request_methods: HashMap>, + incoming_request_methods: HashMap>, + outgoing_request_methods: HashMap>, _task: Task<()>, } @@ -175,7 +175,7 @@ impl AcpTools { } }; - method_map.insert(id, method.clone()); + method_map.insert(id.clone(), method.clone()); (Some(id), method.into(), MessageType::Request, Ok(params)) } acp::StreamMessageContent::Response { id, result } => { @@ -338,6 +338,7 @@ impl AcpTools { .children( message .request_id + .as_ref() .map(|req_id| div().child(ui::Chip::new(req_id.to_string()))), ), ) @@ -389,7 +390,7 @@ impl AcpTools { struct WatchedConnectionMessage { name: SharedString, - request_id: Option, + request_id: Option, direction: acp::StreamMessageDirection, message_type: MessageType, params: Result, acp::Error>, diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index dac6db1b12c38a967c3abd05cf34a96a44f77b08..ad205137a44f3fd7e33e4998c023d552e4007b5c 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -40,7 +40,7 @@ pub struct AcpConnection { // NB: Don't move this into the wait_task, since we need to ensure the process is // killed on drop (setting kill_on_drop on the command seems to not always work). child: smol::process::Child, - _io_task: Task>, + _io_task: Task>, _wait_task: Task>, _stderr_task: Task>, } From 7c4fb5a899c34efaea9e52ddd84daebc8d9ccf49 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:40:02 +0200 Subject: [PATCH 055/202] search: New old search implementation (#39956) This is an in-progress work on changing how task scheduler affects performance of project search. Instead of relying on tasks being executed at a discretion of the task scheduler, we want to experiment with having a set of "agents" that prioritize driving in-progress project search matches to completion over pushing the whole thing to completion. This should hopefully significantly improve throughput & latency of project search. Release Notes: - Improved project search performance --------- Co-authored-by: Smit Barmase Co-authored-by: Smit Barmase --- Cargo.lock | 27 + Cargo.toml | 2 + crates/project/src/buffer_store.rs | 71 +- crates/project/src/project.rs | 235 ++---- crates/project/src/project_search.rs | 754 +++++++++++++++++++ crates/project/src/worktree_store.rs | 151 +--- crates/project_benchmarks/Cargo.toml | 21 + crates/project_benchmarks/LICENSE-GPL | 1 + crates/project_benchmarks/src/main.rs | 136 ++++ crates/remote_server/Cargo.toml | 2 +- crates/remote_server/src/headless_project.rs | 12 +- 11 files changed, 1023 insertions(+), 389 deletions(-) create mode 100644 crates/project/src/project_search.rs create mode 100644 crates/project_benchmarks/Cargo.toml create mode 120000 crates/project_benchmarks/LICENSE-GPL create mode 100644 crates/project_benchmarks/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 99ae6a0315c2320db18ebdc70c8c19ececbdbbe1..6ff72c08b4482a7b5ae5e4abeed90157f6bcd124 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12890,6 +12890,23 @@ dependencies = [ "zlog", ] +[[package]] +name = "project_benchmarks" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "client", + "futures 0.3.31", + "gpui", + "http_client", + "language", + "node_runtime", + "project", + "settings", + "watch", +] + [[package]] name = "project_panel" version = "0.1.0" @@ -20590,6 +20607,16 @@ dependencies = [ "zlog", ] +[[package]] +name = "worktree_benchmarks" +version = "0.1.0" +dependencies = [ + "fs", + "gpui", + "settings", + "worktree", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index a76cb403bc41d1ec068050789e8a0c5af6d61d58..33cbacf8040441e8a7df20452d625aef74f5e9d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ members = [ "crates/picker", "crates/prettier", "crates/project", + "crates/project_benchmarks", "crates/project_panel", "crates/project_symbols", "crates/prompt_store", @@ -194,6 +195,7 @@ members = [ "crates/web_search_providers", "crates/workspace", "crates/worktree", + "crates/worktree_benchmarks", "crates/x_ai", "crates/zed", "crates/zed_actions", diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 9c7caa6280c6502a5279a48d63e1ee4f42d9e11c..b36b44cfd4d0b3f73e808becf5168cd47f7e4c06 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,14 +1,12 @@ use crate::{ - ProjectItem as _, ProjectPath, + ProjectPath, lsp_store::OpenLspBufferHandle, - search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; use anyhow::{Context as _, Result, anyhow}; use client::Client; use collections::{HashMap, HashSet, hash_map}; -use fs::Fs; -use futures::{Future, FutureExt as _, StreamExt, channel::oneshot, future::Shared}; +use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -23,8 +21,8 @@ use rpc::{ AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope, proto::{self}, }; -use smol::channel::Receiver; -use std::{io, pin::pin, sync::Arc, time::Instant}; + +use std::{io, sync::Arc, time::Instant}; use text::{BufferId, ReplicaId}; use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath}; use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; @@ -972,6 +970,10 @@ impl BufferStore { .filter_map(|buffer| buffer.upgrade()) } + pub(crate) fn is_searchable(&self, id: &BufferId) -> bool { + !self.non_searchable_buffers.contains(&id) + } + pub fn loading_buffers( &self, ) -> impl Iterator>>)> { @@ -1096,63 +1098,6 @@ impl BufferStore { Some(()) } - pub fn find_search_candidates( - &mut self, - query: &SearchQuery, - mut limit: usize, - fs: Arc, - cx: &mut Context, - ) -> Receiver> { - let (tx, rx) = smol::channel::unbounded(); - let mut open_buffers = HashSet::default(); - let mut unnamed_buffers = Vec::new(); - for handle in self.buffers() { - let buffer = handle.read(cx); - if self.non_searchable_buffers.contains(&buffer.remote_id()) { - continue; - } else if let Some(entry_id) = buffer.entry_id(cx) { - open_buffers.insert(entry_id); - } else { - limit = limit.saturating_sub(1); - unnamed_buffers.push(handle) - }; - } - - const MAX_CONCURRENT_BUFFER_OPENS: usize = 64; - let project_paths_rx = self - .worktree_store - .update(cx, |worktree_store, cx| { - worktree_store.find_search_candidates(query.clone(), limit, open_buffers, fs, cx) - }) - .chunks(MAX_CONCURRENT_BUFFER_OPENS); - - cx.spawn(async move |this, cx| { - for buffer in unnamed_buffers { - tx.send(buffer).await.ok(); - } - - let mut project_paths_rx = pin!(project_paths_rx); - while let Some(project_paths) = project_paths_rx.next().await { - let buffers = this.update(cx, |this, cx| { - project_paths - .into_iter() - .map(|project_path| this.open_buffer(project_path, cx)) - .collect::>() - })?; - for buffer_task in buffers { - if let Some(buffer) = buffer_task.await.log_err() - && tx.send(buffer).await.is_err() - { - return anyhow::Ok(()); - } - } - } - anyhow::Ok(()) - }) - .detach(); - rx - } - fn on_buffer_event( &mut self, buffer: Entity, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4a7ac5fa50fac05bb81e5374fd213a6e4ff5bda4..678607b53219992317e1762ff15b57500eb33d79 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,6 +11,7 @@ pub mod lsp_command; pub mod lsp_store; mod manifest_tree; pub mod prettier_store; +mod project_search; pub mod project_settings; pub mod search; mod task_inventory; @@ -39,6 +40,7 @@ use crate::{ agent_server_store::AllAgentServersSettings, git_store::GitStore, lsp_store::{SymbolLocation, log_store::LogKind}, + project_search::SearchResultsHandle, }; pub use agent_server_store::{AgentServerStore, AgentServersUpdated}; pub use git_store::{ @@ -46,6 +48,7 @@ pub use git_store::{ git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal}, }; pub use manifest_tree::ManifestTree; +pub use project_search::Search; use anyhow::{Context as _, Result, anyhow}; use buffer_store::{BufferStore, BufferStoreEvent}; @@ -109,7 +112,7 @@ use snippet_provider::SnippetProvider; use std::{ borrow::Cow, collections::BTreeMap, - ops::Range, + ops::{Not as _, Range}, path::{Path, PathBuf}, pin::pin, str, @@ -123,7 +126,7 @@ use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, maybe, - paths::{PathStyle, SanitizedPath, compare_paths, is_absolute}, + paths::{PathStyle, SanitizedPath, is_absolute}, rel_path::RelPath, }; use worktree::{CreatedEntry, Snapshot, Traversal}; @@ -150,8 +153,6 @@ pub use lsp_store::{ }; pub use toolchain_store::{ToolchainStore, Toolchains}; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; -const MAX_SEARCH_RESULT_FILES: usize = 5_000; -const MAX_SEARCH_RESULT_RANGES: usize = 10_000; pub trait ProjectItem: 'static { fn try_open( @@ -3935,179 +3936,44 @@ impl Project { }) } - pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { - let (result_tx, result_rx) = smol::channel::unbounded(); - - let matching_buffers_rx = if query.is_opened_only() { - self.sort_search_candidates(&query, cx) - } else { - self.find_search_candidate_buffers(&query, MAX_SEARCH_RESULT_FILES + 1, cx) - }; - - cx.spawn(async move |_, cx| { - let mut range_count = 0; - let mut buffer_count = 0; - let mut limit_reached = false; - let query = Arc::new(query); - let chunks = matching_buffers_rx.ready_chunks(64); - - // Now that we know what paths match the query, we will load at most - // 64 buffers at a time to avoid overwhelming the main thread. For each - // opened buffer, we will spawn a background task that retrieves all the - // ranges in the buffer matched by the query. - let mut chunks = pin!(chunks); - 'outer: while let Some(matching_buffer_chunk) = chunks.next().await { - let mut chunk_results = Vec::with_capacity(matching_buffer_chunk.len()); - for buffer in matching_buffer_chunk { - let query = query.clone(); - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - chunk_results.push(cx.background_spawn(async move { - let ranges = query - .search(&snapshot, None) - .await - .iter() - .map(|range| { - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .collect::>(); - anyhow::Ok((buffer, ranges)) - })); - } - - let chunk_results = futures::future::join_all(chunk_results).await; - for result in chunk_results { - if let Some((buffer, ranges)) = result.log_err() { - range_count += ranges.len(); - buffer_count += 1; - result_tx - .send(SearchResult::Buffer { buffer, ranges }) - .await?; - if buffer_count > MAX_SEARCH_RESULT_FILES - || range_count > MAX_SEARCH_RESULT_RANGES - { - limit_reached = true; - break 'outer; - } - } - } - } - - if limit_reached { - result_tx.send(SearchResult::LimitReached).await?; - } - - anyhow::Ok(()) - }) - .detach(); - - result_rx - } - - fn find_search_candidate_buffers( - &mut self, - query: &SearchQuery, - limit: usize, - cx: &mut Context, - ) -> Receiver> { - if self.is_local() { - let fs = self.fs.clone(); - self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.find_search_candidates(query, limit, fs, cx) - }) - } else { - self.find_search_candidates_remote(query, limit, cx) - } - } - - fn sort_search_candidates( - &mut self, - search_query: &SearchQuery, - cx: &mut Context, - ) -> Receiver> { - let worktree_store = self.worktree_store.read(cx); - let mut buffers = search_query - .buffers() - .into_iter() - .flatten() - .filter(|buffer| { - let b = buffer.read(cx); - if let Some(file) = b.file() { - if !search_query.match_path(file.path().as_std_path()) { - return false; - } - if let Some(entry) = b - .entry_id(cx) - .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) - && entry.is_ignored - && !search_query.include_ignored() - { - return false; - } - } - true - }) - .collect::>(); - let (tx, rx) = smol::channel::unbounded(); - buffers.sort_by(|a, b| match (a.read(cx).file(), b.read(cx).file()) { - (None, None) => a.read(cx).remote_id().cmp(&b.read(cx).remote_id()), - (None, Some(_)) => std::cmp::Ordering::Less, - (Some(_), None) => std::cmp::Ordering::Greater, - (Some(a), Some(b)) => compare_paths( - (a.path().as_std_path(), true), - (b.path().as_std_path(), true), - ), - }); - for buffer in buffers { - tx.send_blocking(buffer.clone()).unwrap() - } - - rx - } - - fn find_search_candidates_remote( - &mut self, - query: &SearchQuery, - limit: usize, - cx: &mut Context, - ) -> Receiver> { - let (tx, rx) = smol::channel::unbounded(); - - let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client - { - (ssh_client.read(cx).proto_client(), 0) + fn search_impl(&mut self, query: SearchQuery, cx: &mut Context) -> SearchResultsHandle { + let client: Option<(AnyProtoClient, _)> = if let Some(ssh_client) = &self.remote_client { + Some((ssh_client.read(cx).proto_client(), 0)) } else if let Some(remote_id) = self.remote_id() { - (self.collab_client.clone().into(), remote_id) + self.is_local() + .not() + .then(|| (self.collab_client.clone().into(), remote_id)) } else { - return rx; + None }; - - let request = client.request(proto::FindSearchCandidates { - project_id: remote_id, - query: Some(query.to_proto()), - limit: limit as _, - }); - let guard = self.retain_remotely_created_models(cx); - - cx.spawn(async move |project, cx| { - let response = request.await?; - for buffer_id in response.buffer_ids { - let buffer_id = BufferId::new(buffer_id)?; - let buffer = project - .update(cx, |project, cx| { - project.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(buffer_id, cx) - }) - })? - .await?; - let _ = tx.send(buffer).await; + let searcher = if query.is_opened_only() { + project_search::Search::open_buffers_only( + self.buffer_store.clone(), + self.worktree_store.clone(), + project_search::Search::MAX_SEARCH_RESULT_FILES + 1, + ) + } else { + match client { + Some((client, remote_id)) => project_search::Search::remote( + self.buffer_store.clone(), + self.worktree_store.clone(), + project_search::Search::MAX_SEARCH_RESULT_FILES + 1, + (client, remote_id, self.remotely_created_models.clone()), + ), + None => project_search::Search::local( + self.fs.clone(), + self.buffer_store.clone(), + self.worktree_store.clone(), + project_search::Search::MAX_SEARCH_RESULT_FILES + 1, + cx, + ), } + }; + searcher.into_handle(query, cx) + } - drop(guard); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - rx + pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { + self.search_impl(query, cx).results(cx) } pub fn request_lsp( @@ -4832,18 +4698,31 @@ impl Project { fn retain_remotely_created_models( &mut self, cx: &mut Context, + ) -> RemotelyCreatedModelGuard { + Self::retain_remotely_created_models_impl( + &self.remotely_created_models, + &self.buffer_store, + &self.worktree_store, + cx, + ) + } + + fn retain_remotely_created_models_impl( + models: &Arc>, + buffer_store: &Entity, + worktree_store: &Entity, + cx: &mut App, ) -> RemotelyCreatedModelGuard { { - let mut remotely_create_models = self.remotely_created_models.lock(); + let mut remotely_create_models = models.lock(); if remotely_create_models.retain_count == 0 { - remotely_create_models.buffers = self.buffer_store.read(cx).buffers().collect(); - remotely_create_models.worktrees = - self.worktree_store.read(cx).worktrees().collect(); + remotely_create_models.buffers = buffer_store.read(cx).buffers().collect(); + remotely_create_models.worktrees = worktree_store.read(cx).worktrees().collect(); } remotely_create_models.retain_count += 1; } RemotelyCreatedModelGuard { - remote_models: Arc::downgrade(&self.remotely_created_models), + remote_models: Arc::downgrade(&models), } } @@ -4913,7 +4792,7 @@ impl Project { let query = SearchQuery::from_proto(message.query.context("missing query field")?, path_style)?; let results = this.update(&mut cx, |this, cx| { - this.find_search_candidate_buffers(&query, message.limit as _, cx) + this.search_impl(query, cx).matching_buffers(cx) })?; let mut response = proto::FindSearchCandidatesResponse { diff --git a/crates/project/src/project_search.rs b/crates/project/src/project_search.rs new file mode 100644 index 0000000000000000000000000000000000000000..25fe578bd7dc2302645dcfb4fd557de1f2b22081 --- /dev/null +++ b/crates/project/src/project_search.rs @@ -0,0 +1,754 @@ +use std::{ + io::{BufRead, BufReader}, + path::Path, + pin::pin, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, +}; + +use anyhow::Context; +use collections::HashSet; +use fs::Fs; +use futures::{SinkExt, StreamExt, select_biased, stream::FuturesOrdered}; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; +use language::{Buffer, BufferSnapshot}; +use parking_lot::Mutex; +use postage::oneshot; +use rpc::{AnyProtoClient, proto}; +use smol::{ + channel::{Receiver, Sender, bounded, unbounded}, + future::FutureExt, +}; + +use text::BufferId; +use util::{ResultExt, maybe, paths::compare_rel_paths}; +use worktree::{Entry, ProjectEntryId, Snapshot, Worktree}; + +use crate::{ + Project, ProjectItem, ProjectPath, RemotelyCreatedModels, + buffer_store::BufferStore, + search::{SearchQuery, SearchResult}, + worktree_store::WorktreeStore, +}; + +pub struct Search { + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + kind: SearchKind, +} + +/// Represents search setup, before it is actually kicked off with Search::into_results +enum SearchKind { + /// Search for candidates by inspecting file contents on file system, avoiding loading the buffer unless we know that a given file contains a match. + Local { + fs: Arc, + worktrees: Vec>, + }, + /// Query remote host for candidates. As of writing, the host runs a local search in "buffers with matches only" mode. + Remote { + client: AnyProtoClient, + remote_id: u64, + models: Arc>, + }, + /// Run search against a known set of candidates. Even when working with a remote host, this won't round-trip to host. + OpenBuffersOnly, +} + +/// Represents results of project search and allows one to either obtain match positions OR +/// just the handles to buffers that may match the search. Grabbing the handles is cheaper than obtaining full match positions, because in that case we'll look for +/// at most one match in each file. +#[must_use] +pub struct SearchResultsHandle { + results: Receiver, + matching_buffers: Receiver>, + trigger_search: Box Task<()> + Send + Sync>, +} + +impl SearchResultsHandle { + pub fn results(self, cx: &mut App) -> Receiver { + (self.trigger_search)(cx).detach(); + self.results + } + pub fn matching_buffers(self, cx: &mut App) -> Receiver> { + (self.trigger_search)(cx).detach(); + self.matching_buffers + } +} + +#[derive(Clone)] +enum FindSearchCandidates { + Local { + fs: Arc, + /// Start off with all paths in project and filter them based on: + /// - Include filters + /// - Exclude filters + /// - Only open buffers + /// - Scan ignored files + /// Put another way: filter out files that can't match (without looking at file contents) + input_paths_rx: Receiver, + /// After that, if the buffer is not yet loaded, we'll figure out if it contains at least one match + /// based on disk contents of a buffer. This step is not performed for buffers we already have in memory. + confirm_contents_will_match_tx: Sender, + confirm_contents_will_match_rx: Receiver, + /// Of those that contain at least one match (or are already in memory), look for rest of matches (and figure out their ranges). + /// But wait - first, we need to go back to the main thread to open a buffer (& create an entity for it). + get_buffer_for_full_scan_tx: Sender, + }, + Remote, + OpenBuffersOnly, +} + +impl Search { + pub fn local( + fs: Arc, + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + cx: &mut App, + ) -> Self { + let worktrees = worktree_store.read(cx).visible_worktrees(cx).collect(); + Self { + kind: SearchKind::Local { fs, worktrees }, + buffer_store, + worktree_store, + limit, + } + } + + pub(crate) fn remote( + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + client_state: (AnyProtoClient, u64, Arc>), + ) -> Self { + Self { + kind: SearchKind::Remote { + client: client_state.0, + remote_id: client_state.1, + models: client_state.2, + }, + buffer_store, + worktree_store, + limit, + } + } + pub(crate) fn open_buffers_only( + buffer_store: Entity, + worktree_store: Entity, + limit: usize, + ) -> Self { + Self { + kind: SearchKind::OpenBuffersOnly, + buffer_store, + worktree_store, + limit, + } + } + + pub(crate) const MAX_SEARCH_RESULT_FILES: usize = 5_000; + pub(crate) const MAX_SEARCH_RESULT_RANGES: usize = 10_000; + /// Prepares a project search run. The resulting [`SearchResultsHandle`] has to be used to specify whether you're interested in matching buffers + /// or full search results. + pub fn into_handle(mut self, query: SearchQuery, cx: &mut App) -> SearchResultsHandle { + let mut open_buffers = HashSet::default(); + let mut unnamed_buffers = Vec::new(); + const MAX_CONCURRENT_BUFFER_OPENS: usize = 64; + let buffers = self.buffer_store.read(cx); + for handle in buffers.buffers() { + let buffer = handle.read(cx); + if !buffers.is_searchable(&buffer.remote_id()) { + continue; + } else if let Some(entry_id) = buffer.entry_id(cx) { + open_buffers.insert(entry_id); + } else { + self.limit -= self.limit.saturating_sub(1); + unnamed_buffers.push(handle) + }; + } + let executor = cx.background_executor().clone(); + let (tx, rx) = unbounded(); + let (grab_buffer_snapshot_tx, grab_buffer_snapshot_rx) = unbounded(); + let matching_buffers = grab_buffer_snapshot_rx.clone(); + let trigger_search = Box::new(move |cx: &mut App| { + cx.spawn(async move |cx| { + for buffer in unnamed_buffers { + _ = grab_buffer_snapshot_tx.send(buffer).await; + } + + let (find_all_matches_tx, find_all_matches_rx) = + bounded(MAX_CONCURRENT_BUFFER_OPENS); + + let (candidate_searcher, tasks) = match self.kind { + SearchKind::OpenBuffersOnly => { + let Ok(open_buffers) = cx.update(|cx| self.all_loaded_buffers(&query, cx)) + else { + return; + }; + let fill_requests = cx + .background_spawn(async move { + for buffer in open_buffers { + if let Err(_) = grab_buffer_snapshot_tx.send(buffer).await { + return; + } + } + }) + .boxed_local(); + (FindSearchCandidates::OpenBuffersOnly, vec![fill_requests]) + } + SearchKind::Local { + fs, + ref mut worktrees, + } => { + let (get_buffer_for_full_scan_tx, get_buffer_for_full_scan_rx) = + unbounded(); + let (confirm_contents_will_match_tx, confirm_contents_will_match_rx) = + bounded(64); + let (sorted_search_results_tx, sorted_search_results_rx) = unbounded(); + + let (input_paths_tx, input_paths_rx) = unbounded(); + + let tasks = vec![ + cx.spawn(Self::provide_search_paths( + std::mem::take(worktrees), + query.include_ignored(), + input_paths_tx, + sorted_search_results_tx, + )) + .boxed_local(), + Self::open_buffers( + &self.buffer_store, + get_buffer_for_full_scan_rx, + grab_buffer_snapshot_tx, + cx.clone(), + ) + .boxed_local(), + cx.background_spawn(Self::maintain_sorted_search_results( + sorted_search_results_rx, + get_buffer_for_full_scan_tx.clone(), + self.limit, + )) + .boxed_local(), + ]; + ( + FindSearchCandidates::Local { + fs, + get_buffer_for_full_scan_tx, + confirm_contents_will_match_tx, + confirm_contents_will_match_rx, + input_paths_rx, + }, + tasks, + ) + } + SearchKind::Remote { + client, + remote_id, + models, + } => { + let request = client.request(proto::FindSearchCandidates { + project_id: remote_id, + query: Some(query.to_proto()), + limit: self.limit as _, + }); + let Ok(guard) = cx.update(|cx| { + Project::retain_remotely_created_models_impl( + &models, + &self.buffer_store, + &self.worktree_store, + cx, + ) + }) else { + return; + }; + let buffer_store = self.buffer_store.downgrade(); + let issue_remote_buffers_request = cx + .spawn(async move |cx| { + let _ = maybe!(async move { + let response = request.await?; + + for buffer_id in response.buffer_ids { + let buffer_id = BufferId::new(buffer_id)?; + let buffer = buffer_store + .update(cx, |buffer_store, cx| { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + })? + .await?; + let _ = grab_buffer_snapshot_tx.send(buffer).await; + } + + drop(guard); + anyhow::Ok(()) + }) + .await + .log_err(); + }) + .boxed_local(); + ( + FindSearchCandidates::Remote, + vec![issue_remote_buffers_request], + ) + } + }; + + let matches_count = AtomicUsize::new(0); + let matched_buffer_count = AtomicUsize::new(0); + + let worker_pool = executor.scoped(|scope| { + let num_cpus = executor.num_cpus(); + + assert!(num_cpus > 0); + for _ in 0..executor.num_cpus() - 1 { + let worker = Worker { + query: &query, + open_buffers: &open_buffers, + matched_buffer_count: &matched_buffer_count, + matches_count: &matches_count, + candidates: candidate_searcher.clone(), + find_all_matches_rx: find_all_matches_rx.clone(), + publish_matches: tx.clone(), + }; + scope.spawn(worker.run()); + } + drop(tx); + drop(find_all_matches_rx); + drop(candidate_searcher); + }); + + let buffer_snapshots = Self::grab_buffer_snapshots( + grab_buffer_snapshot_rx, + find_all_matches_tx, + cx.clone(), + ); + futures::future::join_all( + [worker_pool.boxed_local(), buffer_snapshots.boxed_local()] + .into_iter() + .chain(tasks), + ) + .await; + }) + }); + + SearchResultsHandle { + results: rx, + matching_buffers, + trigger_search, + } + } + + fn provide_search_paths( + worktrees: Vec>, + include_ignored: bool, + tx: Sender, + results: Sender>, + ) -> impl AsyncFnOnce(&mut AsyncApp) { + async move |cx| { + _ = maybe!(async move { + for worktree in worktrees { + let (mut snapshot, worktree_settings) = worktree + .read_with(cx, |this, _| { + Some((this.snapshot(), this.as_local()?.settings())) + })? + .context("The worktree is not local")?; + if include_ignored { + // Pre-fetch all of the ignored directories as they're going to be searched. + let mut entries_to_refresh = vec![]; + for entry in snapshot.entries(include_ignored, 0) { + if entry.is_ignored && entry.kind.is_unloaded() { + if !worktree_settings.is_path_excluded(&entry.path) { + entries_to_refresh.push(entry.path.clone()); + } + } + } + let barrier = worktree.update(cx, |this, _| { + let local = this.as_local_mut()?; + let barrier = entries_to_refresh + .into_iter() + .map(|path| local.add_path_prefix_to_scan(path).into_future()) + .collect::>(); + Some(barrier) + })?; + if let Some(barriers) = barrier { + futures::future::join_all(barriers).await; + } + snapshot = worktree.read_with(cx, |this, _| this.snapshot())?; + } + cx.background_executor() + .scoped(|scope| { + scope.spawn(async { + for entry in snapshot.files(include_ignored, 0) { + let (should_scan_tx, should_scan_rx) = oneshot::channel(); + let Ok(_) = tx + .send(InputPath { + entry: entry.clone(), + snapshot: snapshot.clone(), + should_scan_tx, + }) + .await + else { + return; + }; + if results.send(should_scan_rx).await.is_err() { + return; + }; + } + }) + }) + .await; + } + anyhow::Ok(()) + }) + .await; + } + } + + async fn maintain_sorted_search_results( + rx: Receiver>, + paths_for_full_scan: Sender, + limit: usize, + ) { + let mut rx = pin!(rx); + let mut matched = 0; + while let Some(mut next_path_result) = rx.next().await { + let Some(successful_path) = next_path_result.next().await else { + // This math did not produce a match, hence skip it. + continue; + }; + if paths_for_full_scan.send(successful_path).await.is_err() { + return; + }; + matched += 1; + if matched >= limit { + break; + } + } + } + + /// Background workers cannot open buffers by themselves, hence main thread will do it on their behalf. + async fn open_buffers( + buffer_store: &Entity, + rx: Receiver, + find_all_matches_tx: Sender>, + mut cx: AsyncApp, + ) { + let mut rx = pin!(rx.ready_chunks(64)); + _ = maybe!(async move { + while let Some(requested_paths) = rx.next().await { + let mut buffers = buffer_store.update(&mut cx, |this, cx| { + requested_paths + .into_iter() + .map(|path| this.open_buffer(path, cx)) + .collect::>() + })?; + + while let Some(buffer) = buffers.next().await { + if let Some(buffer) = buffer.log_err() { + find_all_matches_tx.send(buffer).await?; + } + } + } + Result::<_, anyhow::Error>::Ok(()) + }) + .await; + } + + async fn grab_buffer_snapshots( + rx: Receiver>, + find_all_matches_tx: Sender<(Entity, BufferSnapshot)>, + mut cx: AsyncApp, + ) { + _ = maybe!(async move { + while let Ok(buffer) = rx.recv().await { + let snapshot = buffer.read_with(&mut cx, |this, _| this.snapshot())?; + find_all_matches_tx.send((buffer, snapshot)).await?; + } + Result::<_, anyhow::Error>::Ok(()) + }) + .await; + } + + fn all_loaded_buffers(&self, search_query: &SearchQuery, cx: &App) -> Vec> { + let worktree_store = self.worktree_store.read(cx); + let mut buffers = search_query + .buffers() + .into_iter() + .flatten() + .filter(|buffer| { + let b = buffer.read(cx); + if let Some(file) = b.file() { + if !search_query.match_path(file.path().as_std_path()) { + return false; + } + if !search_query.include_ignored() + && let Some(entry) = b + .entry_id(cx) + .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) + && entry.is_ignored + { + return false; + } + } + true + }) + .cloned() + .collect::>(); + buffers.sort_by(|a, b| { + let a = a.read(cx); + let b = b.read(cx); + match (a.file(), b.file()) { + (None, None) => a.remote_id().cmp(&b.remote_id()), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (Some(a), Some(b)) => compare_rel_paths((a.path(), true), (b.path(), true)), + } + }); + + buffers + } +} + +struct Worker<'search> { + query: &'search SearchQuery, + matched_buffer_count: &'search AtomicUsize, + matches_count: &'search AtomicUsize, + open_buffers: &'search HashSet, + candidates: FindSearchCandidates, + /// Ok, we're back in background: run full scan & find all matches in a given buffer snapshot. + find_all_matches_rx: Receiver<(Entity, BufferSnapshot)>, + /// Cool, we have results; let's share them with the world. + publish_matches: Sender, +} + +impl Worker<'_> { + async fn run(mut self) { + let ( + input_paths_rx, + confirm_contents_will_match_rx, + mut confirm_contents_will_match_tx, + mut get_buffer_for_full_scan_tx, + fs, + ) = match self.candidates { + FindSearchCandidates::Local { + fs, + input_paths_rx, + confirm_contents_will_match_rx, + confirm_contents_will_match_tx, + get_buffer_for_full_scan_tx, + } => ( + input_paths_rx, + confirm_contents_will_match_rx, + confirm_contents_will_match_tx, + get_buffer_for_full_scan_tx, + Some(fs), + ), + FindSearchCandidates::Remote | FindSearchCandidates::OpenBuffersOnly => ( + unbounded().1, + unbounded().1, + unbounded().0, + unbounded().0, + None, + ), + }; + let mut find_all_matches = pin!(self.find_all_matches_rx.fuse()); + let mut find_first_match = pin!(confirm_contents_will_match_rx.fuse()); + let mut scan_path = pin!(input_paths_rx.fuse()); + + loop { + let handler = RequestHandler { + query: self.query, + open_entries: &self.open_buffers, + fs: fs.as_deref(), + matched_buffer_count: self.matched_buffer_count, + matches_count: self.matches_count, + confirm_contents_will_match_tx: &confirm_contents_will_match_tx, + get_buffer_for_full_scan_tx: &get_buffer_for_full_scan_tx, + publish_matches: &self.publish_matches, + }; + // Whenever we notice that some step of a pipeline is closed, we don't want to close subsequent + // steps straight away. Another worker might be about to produce a value that will + // be pushed there, thus we'll replace current worker's pipe with a dummy one. + // That way, we'll only ever close a next-stage channel when ALL workers do so. + select_biased! { + find_all_matches = find_all_matches.next() => { + + if self.publish_matches.is_closed() { + break; + } + let Some(matches) = find_all_matches else { + self.publish_matches = bounded(1).0; + continue; + }; + let result = handler.handle_find_all_matches(matches).await; + if let Some(_should_bail) = result { + + self.publish_matches = bounded(1).0; + continue; + } + }, + find_first_match = find_first_match.next() => { + if let Some(buffer_with_at_least_one_match) = find_first_match { + handler.handle_find_first_match(buffer_with_at_least_one_match).await; + } else { + get_buffer_for_full_scan_tx = bounded(1).0; + } + + }, + scan_path = scan_path.next() => { + if let Some(path_to_scan) = scan_path { + handler.handle_scan_path(path_to_scan).await; + } else { + // If we're the last worker to notice that this is not producing values, close the upstream. + confirm_contents_will_match_tx = bounded(1).0; + } + + } + complete => { + break + }, + + } + } + } +} + +struct RequestHandler<'worker> { + query: &'worker SearchQuery, + fs: Option<&'worker dyn Fs>, + open_entries: &'worker HashSet, + matched_buffer_count: &'worker AtomicUsize, + matches_count: &'worker AtomicUsize, + + confirm_contents_will_match_tx: &'worker Sender, + get_buffer_for_full_scan_tx: &'worker Sender, + publish_matches: &'worker Sender, +} + +struct LimitReached; + +impl RequestHandler<'_> { + async fn handle_find_all_matches( + &self, + (buffer, snapshot): (Entity, BufferSnapshot), + ) -> Option { + let ranges = self + .query + .search(&snapshot, None) + .await + .iter() + .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)) + .collect::>(); + + let matched_ranges = ranges.len(); + if self.matched_buffer_count.fetch_add(1, Ordering::Release) + > Search::MAX_SEARCH_RESULT_FILES + || self + .matches_count + .fetch_add(matched_ranges, Ordering::Release) + > Search::MAX_SEARCH_RESULT_RANGES + { + _ = self.publish_matches.send(SearchResult::LimitReached).await; + Some(LimitReached) + } else { + _ = self + .publish_matches + .send(SearchResult::Buffer { buffer, ranges }) + .await; + None + } + } + async fn handle_find_first_match(&self, mut entry: MatchingEntry) { + _=maybe!(async move { + let abs_path = entry.worktree_root.join(entry.path.path.as_std_path()); + let Some(file) = self.fs.context("Trying to query filesystem in remote project search")?.open_sync(&abs_path).await.log_err() else { + return anyhow::Ok(()); + }; + + let mut file = BufReader::new(file); + let file_start = file.fill_buf()?; + + if let Err(Some(starting_position)) = + std::str::from_utf8(file_start).map_err(|e| e.error_len()) + { + // Before attempting to match the file content, throw away files that have invalid UTF-8 sequences early on; + // That way we can still match files in a streaming fashion without having look at "obviously binary" files. + log::debug!( + "Invalid UTF-8 sequence in file {abs_path:?} at byte position {starting_position}" + ); + return Ok(()); + } + + if self.query.detect(file).unwrap_or(false) { + // Yes, we should scan the whole file. + entry.should_scan_tx.send(entry.path).await?; + } + Ok(()) + }).await; + } + + async fn handle_scan_path(&self, req: InputPath) { + _ = maybe!(async move { + let InputPath { + entry, + + snapshot, + should_scan_tx, + } = req; + + if entry.is_fifo || !entry.is_file() { + return Ok(()); + } + + if self.query.filters_path() { + let matched_path = if self.query.match_full_paths() { + let mut full_path = snapshot.root_name().as_std_path().to_owned(); + full_path.push(entry.path.as_std_path()); + self.query.match_path(&full_path) + } else { + self.query.match_path(entry.path.as_std_path()) + }; + if !matched_path { + return Ok(()); + } + } + + if self.open_entries.contains(&entry.id) { + // The buffer is already in memory and that's the version we want to scan; + // hence skip the dilly-dally and look for all matches straight away. + self.get_buffer_for_full_scan_tx + .send(ProjectPath { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }) + .await?; + } else { + self.confirm_contents_will_match_tx + .send(MatchingEntry { + should_scan_tx: should_scan_tx, + worktree_root: snapshot.abs_path().clone(), + path: ProjectPath { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }, + }) + .await?; + } + + anyhow::Ok(()) + }) + .await; + } +} + +struct InputPath { + entry: Entry, + snapshot: Snapshot, + should_scan_tx: oneshot::Sender, +} + +struct MatchingEntry { + worktree_root: Arc, + path: ProjectPath, + should_scan_tx: oneshot::Sender, +} diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index e6da207dadbde3ebc725fbb84ed19b3b35414f87..676c96f4331d73b87d4bc16766a5f6c4d6194864 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -8,10 +8,7 @@ use std::{ use anyhow::{Context as _, Result, anyhow, bail}; use collections::{HashMap, HashSet}; use fs::{Fs, copy_recursive}; -use futures::{ - FutureExt, SinkExt, - future::{BoxFuture, Shared}, -}; +use futures::{FutureExt, SinkExt, future::Shared}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity, }; @@ -999,148 +996,14 @@ impl WorktreeStore { matching_paths_rx } - fn scan_ignored_dir<'a>( - fs: &'a Arc, - snapshot: &'a worktree::Snapshot, - path: &'a RelPath, - query: &'a SearchQuery, - filter_tx: &'a Sender, - output_tx: &'a Sender>, - ) -> BoxFuture<'a, Result<()>> { - async move { - let abs_path = snapshot.absolutize(path); - let Some(mut files) = fs - .read_dir(&abs_path) - .await - .with_context(|| format!("listing ignored path {abs_path:?}")) - .log_err() - else { - return Ok(()); - }; - - let mut results = Vec::new(); - - while let Some(Ok(file)) = files.next().await { - let Some(metadata) = fs - .metadata(&file) - .await - .with_context(|| format!("fetching fs metadata for {abs_path:?}")) - .log_err() - .flatten() - else { - continue; - }; - if metadata.is_symlink || metadata.is_fifo { - continue; - } - let relative_path = file.strip_prefix(snapshot.abs_path())?; - let relative_path = RelPath::new(&relative_path, snapshot.path_style()) - .context("getting relative path")?; - results.push((relative_path.into_arc(), !metadata.is_dir)) - } - results.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path)); - for (path, is_file) in results { - if is_file { - if query.filters_path() { - let matched_path = if query.match_full_paths() { - let mut full_path = snapshot.root_name().as_std_path().to_owned(); - full_path.push(path.as_std_path()); - query.match_path(&full_path) - } else { - query.match_path(&path.as_std_path()) - }; - if !matched_path { - continue; - } - } - let (tx, rx) = oneshot::channel(); - output_tx.send(rx).await?; - filter_tx - .send(MatchingEntry { - respond: tx, - worktree_root: snapshot.abs_path().clone(), - path: ProjectPath { - worktree_id: snapshot.id(), - path: path.into_arc(), - }, - }) - .await?; - } else { - Self::scan_ignored_dir(fs, snapshot, &path, query, filter_tx, output_tx) - .await?; - } - } - Ok(()) - } - .boxed() - } - async fn find_candidate_paths( - fs: Arc, - snapshots: Vec<(worktree::Snapshot, WorktreeSettings)>, - open_entries: HashSet, - query: SearchQuery, - filter_tx: Sender, - output_tx: Sender>, + _: Arc, + _: Vec<(worktree::Snapshot, WorktreeSettings)>, + _: HashSet, + _: SearchQuery, + _: Sender, + _: Sender>, ) -> Result<()> { - for (snapshot, settings) in snapshots { - for entry in snapshot.entries(query.include_ignored(), 0) { - if entry.is_dir() && entry.is_ignored { - if !settings.is_path_excluded(&entry.path) { - Self::scan_ignored_dir( - &fs, - &snapshot, - &entry.path, - &query, - &filter_tx, - &output_tx, - ) - .await?; - } - continue; - } - - if entry.is_fifo || !entry.is_file() { - continue; - } - - if query.filters_path() { - let matched_path = if query.match_full_paths() { - let mut full_path = snapshot.root_name().as_std_path().to_owned(); - full_path.push(entry.path.as_std_path()); - query.match_path(&full_path) - } else { - query.match_path(entry.path.as_std_path()) - }; - if !matched_path { - continue; - } - } - - let (mut tx, rx) = oneshot::channel(); - - if open_entries.contains(&entry.id) { - tx.send(ProjectPath { - worktree_id: snapshot.id(), - path: entry.path.clone(), - }) - .await?; - } else { - filter_tx - .send(MatchingEntry { - respond: tx, - worktree_root: snapshot.abs_path().clone(), - path: ProjectPath { - worktree_id: snapshot.id(), - path: entry.path.clone(), - }, - }) - .await?; - } - - output_tx.send(rx).await?; - } - } Ok(()) } diff --git a/crates/project_benchmarks/Cargo.toml b/crates/project_benchmarks/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..1171d468c649bdd9f76a44b3ef0155dc652c6034 --- /dev/null +++ b/crates/project_benchmarks/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "project_benchmarks" +version = "0.1.0" +publish.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +clap.workspace = true +client.workspace = true +futures.workspace = true +gpui = { workspace = true, features = ["windows-manifest"] } +http_client = { workspace = true, features = ["test-support"]} +language.workspace = true +node_runtime.workspace = true +project.workspace = true +settings.workspace = true +watch.workspace = true + +[lints] +workspace = true diff --git a/crates/project_benchmarks/LICENSE-GPL b/crates/project_benchmarks/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/project_benchmarks/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/project_benchmarks/src/main.rs b/crates/project_benchmarks/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..5075016665a072f172da461cffdf6c5dbcabb4ac --- /dev/null +++ b/crates/project_benchmarks/src/main.rs @@ -0,0 +1,136 @@ +use std::sync::Arc; + +use clap::Parser; +use client::{Client, UserStore}; +use gpui::{AppContext as _, Application}; +use http_client::FakeHttpClient; +use language::LanguageRegistry; +use node_runtime::NodeRuntime; +use project::{ + Project, RealFs, + search::{SearchQuery, SearchResult}, +}; + +#[derive(Parser)] +struct Args { + /// List of worktrees to run the search against. + worktrees: Vec, + #[clap(short)] + query: String, + /// Treat query as a regex. + #[clap(short, long)] + regex: bool, + /// Matches have to be standalone words. + #[clap(long)] + whole_word: bool, + /// Make matching case-sensitive. + #[clap(long, default_value_t = true)] + case_sensitive: bool, + /// Include gitignored files in the search. + #[clap(long)] + include_ignored: bool, +} + +fn main() -> Result<(), anyhow::Error> { + let args = Args::parse(); + let query = if args.regex { + SearchQuery::regex( + args.query, + args.whole_word, + args.case_sensitive, + args.include_ignored, + false, + Default::default(), + Default::default(), + false, + None, + ) + } else { + SearchQuery::text( + args.query, + args.whole_word, + args.case_sensitive, + args.include_ignored, + Default::default(), + Default::default(), + false, + None, + ) + }?; + Application::headless().run(|cx| { + settings::init(cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + let client = Client::production(cx); + let http_client = FakeHttpClient::with_200_response(); + let (_, rx) = watch::channel(None); + let node = NodeRuntime::new(http_client, None, rx); + let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); + let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); + let project = Project::local( + client, + node, + user_store, + registry, + fs, + Some(Default::default()), + cx, + ); + + project.clone().update(cx, move |_, cx| { + cx.spawn(async move |_, cx| { + println!("Loading worktrees"); + let worktrees = project.update(cx, |this, cx| { + args.worktrees + .into_iter() + .map(|worktree| this.find_or_create_worktree(worktree, true, cx)) + .collect::>() + })?; + + let worktrees = futures::future::join_all(worktrees) + .await + .into_iter() + .collect::, anyhow::Error>>()?; + + for (worktree, _) in &worktrees { + worktree + .update(cx, |this, _| this.as_local().unwrap().scan_complete())? + .await; + } + println!("Worktrees loaded"); + + println!("Starting a project search"); + let timer = std::time::Instant::now(); + let mut first_match = None; + let matches = project + .update(cx, |this, cx| this.search(query, cx)) + .unwrap(); + let mut matched_files = 0; + let mut matched_chunks = 0; + while let Ok(match_result) = matches.recv().await { + if first_match.is_none() { + let time = timer.elapsed(); + first_match = Some(time); + println!("First match found after {time:?}"); + } + if let SearchResult::Buffer { ranges, .. } = match_result { + matched_files += 1; + matched_chunks += ranges.len(); + } + } + let elapsed = timer.elapsed(); + println!( + "Finished project search after {elapsed:?}. Matched {matched_files} files and {matched_chunks} excerpts" + ); + drop(project); + cx.update(|cx| cx.quit())?; + + anyhow::Ok(()) + }) + .detach(); + }); + }); + Ok(()) +} diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 3d28f6ba565330a5fc3c0ea0249aaf760c880439..b702c75119af9f49707c079a3a799ae8c59209b1 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -75,7 +75,7 @@ minidumper.workspace = true [dev-dependencies] action_log.workspace = true -agent.workspace = true +agent = { workspace = true, features = ["test-support"] } client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } collections.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 1c42f1995daa03fbcb90323f1a136659baa995f9..2f429ed80aa4bf914e2cf3c6a03ad3e64f39ce81 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -639,9 +639,15 @@ impl HeadlessProject { PathStyle::local(), )?; let results = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx) - }) + project::Search::local( + this.fs.clone(), + this.buffer_store.clone(), + this.worktree_store.clone(), + message.limit as _, + cx, + ) + .into_handle(query, cx) + .matching_buffers(cx) })?; let mut response = proto::FindSearchCandidatesResponse { From 30f3152e65d04ed477d1589d70e33ce29f15dd1b Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Mon, 20 Oct 2025 07:53:49 -0700 Subject: [PATCH 056/202] settings_ui: Use window controls on Linux (#40706) Closes #40657 Release Notes: - Added window controls to the settings window on Linux. --- crates/settings_ui/src/settings_ui.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 138d54fc4a539045b04ca1091ad8b55049fbf1d5..7d38ccd367890e2df8dd360e9a15fc9155dd1ed0 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -474,6 +474,12 @@ pub fn open_settings_editor( let scale_factor = current_rem_size / default_rem_size; let scaled_bounds: gpui::Size = default_bounds.map(|axis| axis * scale_factor); + let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { + Ok(val) if val == "server" => gpui::WindowDecorations::Server, + Ok(val) if val == "client" => gpui::WindowDecorations::Client, + _ => gpui::WindowDecorations::Client, + }; + cx.open_window( WindowOptions { titlebar: Some(TitlebarOptions { @@ -486,6 +492,7 @@ pub fn open_settings_editor( is_movable: true, kind: gpui::WindowKind::Floating, window_background: cx.theme().window_background_appearance(), + window_decorations: Some(window_decorations), window_min_size: Some(scaled_bounds), window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)), ..Default::default() From 67b9d480b444ef3e44b01e722fd595dbdf2c02a9 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 16:57:23 +0200 Subject: [PATCH 057/202] project: Make `textDocument/signatureHelp` implementation lsp compliant (#40707) The parameter label offsets are utf16 offsets, not utf8. Additionally we now validate the language server output. Closes https://github.com/zed-industries/zed/issues/40578 Companion bug on rust-analyzer side https://github.com/rust-lang/rust-analyzer/pull/20876 Release Notes: - Fixed `textDocument/signatureHelp` implementation not being LSP compliant --- crates/project/src/lsp_command.rs | 18 ++- .../project/src/lsp_command/signature_help.rs | 124 +++++++++++++----- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 5ec6e502bd85a25b6755c6994feff7a3062c919c..7d5bf2fcd514d081260e4dbe3d9c3521d2629e17 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1834,13 +1834,20 @@ impl LspCommand for GetSignatureHelp { message: Option, lsp_store: Entity, _: Entity, - _: LanguageServerId, + id: LanguageServerId, cx: AsyncApp, ) -> Result { let Some(message) = message else { return Ok(None); }; - cx.update(|cx| SignatureHelp::new(message, Some(lsp_store.read(cx).languages.clone()), cx)) + cx.update(|cx| { + SignatureHelp::new( + message, + Some(lsp_store.read(cx).languages.clone()), + Some(id), + cx, + ) + }) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { @@ -1900,7 +1907,12 @@ impl LspCommand for GetSignatureHelp { .signature_help .map(proto_to_lsp_signature) .and_then(|signature| { - SignatureHelp::new(signature, Some(lsp_store.read(cx).languages.clone()), cx) + SignatureHelp::new( + signature, + Some(lsp_store.read(cx).languages.clone()), + None, + cx, + ) }) }) } diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs index 8adb69ac7726becada3f6123f9f350237e2aa22e..6a499311837b8ebd70874c89d9fac223b3c8ede1 100644 --- a/crates/project/src/lsp_command/signature_help.rs +++ b/crates/project/src/lsp_command/signature_help.rs @@ -2,8 +2,10 @@ use std::{ops::Range, sync::Arc}; use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString}; use language::LanguageRegistry; +use lsp::LanguageServerId; use markdown::Markdown; use rpc::proto::{self, documentation}; +use util::maybe; #[derive(Debug)] pub struct SignatureHelp { @@ -31,6 +33,7 @@ impl SignatureHelp { pub fn new( help: lsp::SignatureHelp, language_registry: Option>, + lang_server_id: Option, cx: &mut App, ) -> Option { if help.signatures.is_empty() { @@ -39,6 +42,7 @@ impl SignatureHelp { let active_signature = help.active_signature.unwrap_or(0) as usize; let mut signatures = Vec::::with_capacity(help.signatures.capacity()); for signature in &help.signatures { + let label = SharedString::from(signature.label.clone()); let active_parameter = signature .active_parameter .unwrap_or_else(|| help.active_parameter.unwrap_or(0)) @@ -49,39 +53,53 @@ impl SignatureHelp { if let Some(parameters) = &signature.parameters { for (index, parameter) in parameters.iter().enumerate() { let label_range = match ¶meter.label { - lsp::ParameterLabel::LabelOffsets(parameter_label_offsets) => { - let range = *parameter_label_offsets.get(0)? as usize - ..*parameter_label_offsets.get(1)? as usize; - if index == active_parameter { - highlights.push(( - range.clone(), - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..HighlightStyle::default() - }, - )); - } - Some(range) + &lsp::ParameterLabel::LabelOffsets([offset1, offset2]) => { + maybe!({ + let offset1 = offset1 as usize; + let offset2 = offset2 as usize; + if offset1 < offset2 { + let mut indices = label.char_indices().scan( + 0, + |utf16_offset_acc, (offset, c)| { + let utf16_offset = *utf16_offset_acc; + *utf16_offset_acc += c.len_utf16(); + Some((utf16_offset, offset)) + }, + ); + let (_, offset1) = indices + .find(|(utf16_offset, _)| *utf16_offset == offset1)?; + let (_, offset2) = indices + .find(|(utf16_offset, _)| *utf16_offset == offset2)?; + Some(offset1..offset2) + } else { + log::warn!( + "language server {lang_server_id:?} produced invalid parameter label range: {offset1:?}..{offset2:?}", + ); + None + } + }) } lsp::ParameterLabel::Simple(parameter_label) => { if let Some(start) = signature.label.find(parameter_label) { - let range = start..start + parameter_label.len(); - if index == active_parameter { - highlights.push(( - range.clone(), - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..HighlightStyle::default() - }, - )); - } - Some(range) + Some(start..start + parameter_label.len()) } else { None } } }; + if let Some(label_range) = &label_range + && index == active_parameter + { + highlights.push(( + label_range.clone(), + HighlightStyle { + font_weight: Some(FontWeight::EXTRA_BOLD), + ..HighlightStyle::default() + }, + )); + } + let documentation = parameter .documentation .as_ref() @@ -94,7 +112,6 @@ impl SignatureHelp { } } - let label = SharedString::from(signature.label.clone()); let documentation = signature .documentation .as_ref() @@ -290,7 +307,7 @@ mod tests { active_signature: Some(0), active_parameter: Some(0), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -336,7 +353,7 @@ mod tests { active_signature: Some(0), active_parameter: Some(1), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -396,7 +413,7 @@ mod tests { active_signature: Some(0), active_parameter: Some(0), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -449,7 +466,7 @@ mod tests { active_signature: Some(1), active_parameter: Some(0), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -502,7 +519,7 @@ mod tests { active_signature: Some(1), active_parameter: Some(1), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -555,7 +572,7 @@ mod tests { active_signature: Some(1), active_parameter: None, }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -623,7 +640,7 @@ mod tests { active_signature: Some(2), active_parameter: Some(1), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -645,7 +662,7 @@ mod tests { active_signature: None, active_parameter: None, }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_none()); } @@ -670,7 +687,7 @@ mod tests { active_signature: Some(0), active_parameter: Some(0), }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_markdown.is_some()); let markdown = maybe_markdown.unwrap(); @@ -708,7 +725,8 @@ mod tests { active_signature: Some(0), active_parameter: Some(0), }; - let maybe_signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, cx)); + let maybe_signature_help = + cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); assert!(maybe_signature_help.is_some()); let signature_help = maybe_signature_help.unwrap(); @@ -736,4 +754,40 @@ mod tests { // Check that the active parameter is correct assert_eq!(signature.active_parameter, Some(0)); } + + #[gpui::test] + fn test_create_signature_help_implements_utf16_spec(cx: &mut TestAppContext) { + let signature_help = lsp::SignatureHelp { + signatures: vec![lsp::SignatureInformation { + label: "fn test(🦀: u8, 🦀: &str)".to_string(), + documentation: None, + parameters: Some(vec![ + lsp::ParameterInformation { + label: lsp::ParameterLabel::LabelOffsets([8, 10]), + documentation: None, + }, + lsp::ParameterInformation { + label: lsp::ParameterLabel::LabelOffsets([16, 18]), + documentation: None, + }, + ]), + active_parameter: None, + }], + active_signature: Some(0), + active_parameter: Some(0), + }; + let signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); + assert!(signature_help.is_some()); + + let markdown = signature_help.unwrap(); + let signature = markdown.signatures[markdown.active_signature].clone(); + let markdown = (signature.label, signature.highlights); + assert_eq!( + markdown, + ( + SharedString::new("fn test(🦀: u8, 🦀: &str)"), + vec![(8..12, current_parameter())] + ) + ); + } } From 8bef4800f0f04da68bbade0e0d18b2a5b6c5f2a7 Mon Sep 17 00:00:00 2001 From: Rian Drake Date: Tue, 21 Oct 2025 04:01:26 +1300 Subject: [PATCH 058/202] workspace: Add NewFileSplit action with direction (#39726) Add new `workspace::NewFileSplit` action which expects a `SplitDirection` argument, allowing users to programmatically control the direction of the split in keymaps, for example: ```json { "context": "Editor", "bindings": { "ctrl-s ctrl-h": ["workspace::NewFileSplit", "left"], "ctrl-s ctrl-j": ["workspace::NewFileSplit", "down"], "ctrl-s ctrl-k": ["workspace::NewFileSplit", "up"], "ctrl-s ctrl-l": ["workspace::NewFileSplit", "right"] } } ``` Release Notes: - Added `workspace::NewFileSplit` action, which can be used to programmatically split the editor in the provided direction. Co-authored-by: Rian Drake Co-authored-by: dino --- crates/editor/src/editor.rs | 10 ++++++++++ crates/workspace/src/workspace.rs | 6 ++++++ crates/zed/src/zed.rs | 1 + 3 files changed, 17 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3b2e30a1761af381c4a3a52e660f8d26dc043ce8..7e633a40e6525668d76ab95d2163a615c296a6e0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -358,6 +358,7 @@ pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context| { workspace.register_action(Editor::new_file); + workspace.register_action(Editor::new_file_split); workspace.register_action(Editor::new_file_vertical); workspace.register_action(Editor::new_file_horizontal); workspace.register_action(Editor::cancel_language_server_work); @@ -2683,6 +2684,15 @@ impl Editor { Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx) } + fn new_file_split( + workspace: &mut Workspace, + action: &workspace::NewFileSplit, + window: &mut Window, + cx: &mut Context, + ) { + Self::new_file_in_direction(workspace, action.0, window, cx) + } + fn new_file_in_direction( workspace: &mut Workspace, direction: SplitDirection, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 053b578ff9082bd933440a36abab47c9b84928bd..85a17c244bebe5d3c33d1bda1fe4ae5758038e56 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -302,6 +302,12 @@ pub struct MoveItemToPaneInDirection { pub clone: bool, } +/// Creates a new file in a split of the desired direction. +#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] +#[action(namespace = workspace)] +#[serde(deny_unknown_fields)] +pub struct NewFileSplit(pub SplitDirection); + fn default_right() -> SplitDirection { SplitDirection::Right } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4a57939c407ee21bed48e12f7af2c44c6d51a1db..e2116415d4b882b66728c43cee90b251b3448013 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4563,6 +4563,7 @@ mod tests { | "workspace::ActivatePane" | "workspace::MoveItemToPane" | "workspace::MoveItemToPaneInDirection" + | "workspace::NewFileSplit" | "workspace::OpenTerminal" | "workspace::SendKeystrokes" | "agent::NewNativeAgentThreadFromSummary" From eda7a49f0185bec1f899ba3f3aa652152588a0f2 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 20 Oct 2025 12:26:41 -0300 Subject: [PATCH 059/202] zeta2: Max retrieved definitions option (#40515) Release Notes: - N/A --- .../src/edit_prediction_context.rs | 13 ++++++++----- crates/zeta2/src/zeta2.rs | 2 +- crates/zeta2_tools/src/zeta2_tools.rs | 14 ++++++++++++++ crates/zeta_cli/src/main.rs | 6 +++--- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/edit_prediction_context/src/edit_prediction_context.rs b/crates/edit_prediction_context/src/edit_prediction_context.rs index 34941f93b4017dfe8e96802b5078daf5652ed22a..f52a2259cf83ff992a904b6b5c9b3ceea7c0a71e 100644 --- a/crates/edit_prediction_context/src/edit_prediction_context.rs +++ b/crates/edit_prediction_context/src/edit_prediction_context.rs @@ -27,9 +27,9 @@ pub use predict_edits_v3::Line; #[derive(Clone, Debug, PartialEq)] pub struct EditPredictionContextOptions { pub use_imports: bool, - pub use_references: bool, pub excerpt: EditPredictionExcerptOptions, pub score: EditPredictionScoreOptions, + pub max_retrieved_declarations: u8, } #[derive(Clone, Debug)] @@ -118,7 +118,7 @@ impl EditPredictionContext { )?; let excerpt_text = excerpt.text(buffer); - let declarations = if options.use_references + let declarations = if options.max_retrieved_declarations > 0 && let Some(index_state) = index_state { let excerpt_occurrences = @@ -136,7 +136,7 @@ impl EditPredictionContext { let references = get_references(&excerpt, &excerpt_text, buffer); - scored_declarations( + let mut declarations = scored_declarations( &options.score, &index_state, &excerpt, @@ -146,7 +146,10 @@ impl EditPredictionContext { references, cursor_offset_in_file, buffer, - ) + ); + // TODO [zeta2] if we need this when we ship, we should probably do it in a smarter way + declarations.truncate(options.max_retrieved_declarations as usize); + declarations } else { vec![] }; @@ -200,7 +203,6 @@ mod tests { buffer_snapshot, EditPredictionContextOptions { use_imports: true, - use_references: true, excerpt: EditPredictionExcerptOptions { max_bytes: 60, min_bytes: 10, @@ -209,6 +211,7 @@ mod tests { score: EditPredictionScoreOptions { omit_excerpt_overlaps: true, }, + max_retrieved_declarations: u8::MAX, }, Some(index.clone()), cx, diff --git a/crates/zeta2/src/zeta2.rs b/crates/zeta2/src/zeta2.rs index 6e92443b8b4c91ce6bb168d3c87ce4ddce49bc35..7c945d6ebbe9c994977adbcc72c6d8fc175930d4 100644 --- a/crates/zeta2/src/zeta2.rs +++ b/crates/zeta2/src/zeta2.rs @@ -48,7 +48,7 @@ const MAX_EVENT_COUNT: usize = 16; pub const DEFAULT_CONTEXT_OPTIONS: EditPredictionContextOptions = EditPredictionContextOptions { use_imports: true, - use_references: false, + max_retrieved_declarations: 0, excerpt: EditPredictionExcerptOptions { max_bytes: 512, min_bytes: 128, diff --git a/crates/zeta2_tools/src/zeta2_tools.rs b/crates/zeta2_tools/src/zeta2_tools.rs index 005a3f1e48b56332d83aa127781f6dd5d95daa0d..0ac4fb2162ca632618df0c2b0d256b2fd7c30742 100644 --- a/crates/zeta2_tools/src/zeta2_tools.rs +++ b/crates/zeta2_tools/src/zeta2_tools.rs @@ -68,6 +68,7 @@ pub struct Zeta2Inspector { min_excerpt_bytes_input: Entity, cursor_context_ratio_input: Entity, max_prompt_bytes_input: Entity, + max_retrieved_declarations: Entity, active_view: ActiveView, zeta: Entity, _active_editor_subscription: Option, @@ -133,6 +134,7 @@ impl Zeta2Inspector { min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx), cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx), max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx), + max_retrieved_declarations: Self::number_input("Max Retrieved Definitions", window, cx), zeta: zeta.clone(), _active_editor_subscription: None, _update_state_task: Task::ready(()), @@ -170,6 +172,13 @@ impl Zeta2Inspector { self.max_prompt_bytes_input.update(cx, |input, cx| { input.set_text(options.max_prompt_bytes.to_string(), window, cx); }); + self.max_retrieved_declarations.update(cx, |input, cx| { + input.set_text( + options.context.max_retrieved_declarations.to_string(), + window, + cx, + ); + }); cx.notify(); } @@ -246,6 +255,10 @@ impl Zeta2Inspector { cx, ), }, + max_retrieved_declarations: number_input_value( + &this.max_retrieved_declarations, + cx, + ), ..zeta_options.context }; @@ -536,6 +549,7 @@ impl Zeta2Inspector { h_flex() .gap_2() .items_end() + .child(self.max_retrieved_declarations.clone()) .child(self.max_prompt_bytes_input.clone()) .child(self.render_prompt_format_dropdown(window, cx)), ), diff --git a/crates/zeta_cli/src/main.rs b/crates/zeta_cli/src/main.rs index 75b859d2f55d99cc37c961455ddcdb86a5f49351..149b13719f2075143d81c164e8d91bbdaca17384 100644 --- a/crates/zeta_cli/src/main.rs +++ b/crates/zeta_cli/src/main.rs @@ -94,8 +94,8 @@ struct Zeta2Args { file_indexing_parallelism: usize, #[arg(long, default_value_t = false)] disable_imports_gathering: bool, - #[arg(long, default_value_t = false)] - disable_reference_retrieval: bool, + #[arg(long, default_value_t = u8::MAX)] + max_retrieved_definitions: u8, } #[derive(clap::ValueEnum, Default, Debug, Clone)] @@ -302,7 +302,7 @@ impl Zeta2Args { fn to_options(&self, omit_excerpt_overlaps: bool) -> zeta2::ZetaOptions { zeta2::ZetaOptions { context: EditPredictionContextOptions { - use_references: !self.disable_reference_retrieval, + max_retrieved_declarations: self.max_retrieved_definitions, use_imports: !self.disable_imports_gathering, excerpt: EditPredictionExcerptOptions { max_bytes: self.max_excerpt_bytes, From db404fc2e3ddc8992d17db3d8a755f4bb57971ad Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:47:15 -0300 Subject: [PATCH 060/202] git: Add diff view for stash entries (#38280) Continues the work from #35927 to add a git diff view for stash entries. [Screencast From 2025-09-17 19-46-01.webm](https://github.com/user-attachments/assets/ded33782-adef-4696-8e34-3665911c09c7) Stash entries are [represented as commits](https://git-scm.com/docs/git-stash#_discussion) except they have up to 3 parents: ``` .----W (this is the stash entry) / /| -----H----I | \| U ``` Where `H` is the `HEAD` commit, `I` is a commit that records the state of the index, and `U` is another commit that records untracked files (when using `git stash -u`). Given this, I modified the existing commit view struct to allow loading stash and commits entries with git sha identifier so that we can get a similar git diff view for both of them. The stash diff is generated by comparing the stash commit with its parent (`^` or `H` in the diagram) which generates the same diff as doing `git stash show -p `. This *can* be counter-intuitive since a user may expect the comparison to be made between the stash commit and the current commit (`HEAD`), but given that the default behavior in git cli is to compare with the stash parent, I went for that approach. Hoping to get some feedback from a Zed team member to see if they agree with this approach. Release Notes: - Add git diff view for stash entries - Add toolbar on git diff view for stash entries - Prompt before executing a destructive stash action on diff view - Fix commit view for merge commits (see #38289) --- assets/keymaps/default-linux.json | 11 +- assets/keymaps/default-macos.json | 12 +- assets/keymaps/default-windows.json | 12 +- crates/git/src/repository.rs | 8 +- crates/git_ui/src/blame_ui.rs | 31 +-- crates/git_ui/src/commit_tooltip.rs | 3 +- crates/git_ui/src/commit_view.rs | 354 ++++++++++++++++++++++++++-- crates/git_ui/src/git_panel.rs | 3 +- crates/git_ui/src/git_ui.rs | 3 +- crates/git_ui/src/stash_picker.rs | 78 +++++- crates/zed/src/zed.rs | 3 + 11 files changed, 461 insertions(+), 57 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index ff5d7533f412872908d52228590fa3afe45a02d0..d2f70b825b4efa3544e916589846e7944a91771a 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1080,7 +1080,8 @@ { "context": "StashList || (StashList > Picker > Editor)", "bindings": { - "ctrl-shift-backspace": "stash_picker::DropStashItem" + "ctrl-shift-backspace": "stash_picker::DropStashItem", + "ctrl-shift-v": "stash_picker::ShowStashItem" } }, { @@ -1266,6 +1267,14 @@ "ctrl-pagedown": "settings_editor::FocusNextFile" } }, + { + "context": "StashDiff > Editor", + "bindings": { + "ctrl-space": "git::ApplyCurrentStash", + "ctrl-shift-space": "git::PopCurrentStash", + "ctrl-shift-backspace": "git::DropCurrentStash" + } + }, { "context": "SettingsWindow > NavigationMenu", "use_key_equivalents": true, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 9b20c267feeed5068b05cb08e0755d3faab75c96..75dd84f7ae269f0883809c8ec1b2516b25f024c9 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1153,7 +1153,8 @@ "context": "StashList || (StashList > Picker > Editor)", "use_key_equivalents": true, "bindings": { - "ctrl-shift-backspace": "stash_picker::DropStashItem" + "ctrl-shift-backspace": "stash_picker::DropStashItem", + "ctrl-shift-v": "stash_picker::ShowStashItem" } }, { @@ -1371,6 +1372,15 @@ "cmd-}": "settings_editor::FocusNextFile" } }, + { + "context": "StashDiff > Editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-space": "git::ApplyCurrentStash", + "ctrl-shift-space": "git::PopCurrentStash", + "ctrl-shift-backspace": "git::DropCurrentStash" + } + }, { "context": "SettingsWindow > NavigationMenu", "use_key_equivalents": true, diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 87e1c350dc10c1f47ceb260b4ce2a03a032b0996..e38bca90e4394ec415df253a7d9668cadefceac1 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1106,7 +1106,8 @@ "context": "StashList || (StashList > Picker > Editor)", "use_key_equivalents": true, "bindings": { - "ctrl-shift-backspace": "stash_picker::DropStashItem" + "ctrl-shift-backspace": "stash_picker::DropStashItem", + "ctrl-shift-v": "stash_picker::ShowStashItem" } }, { @@ -1294,6 +1295,15 @@ "ctrl-pagedown": "settings_editor::FocusNextFile" } }, + { + "context": "StashDiff > Editor", + "use_key_equivalents": true, + "bindings": { + "ctrl-space": "git::ApplyCurrentStash", + "ctrl-shift-space": "git::PopCurrentStash", + "ctrl-shift-backspace": "git::DropCurrentStash" + } + }, { "context": "SettingsWindow > NavigationMenu", "use_key_equivalents": true, diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 2e132d4eaca55c9307bf3368c412f77ed6726df2..dc674fb3dd5cbe6a55837780f7ac10449e2efd41 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -693,10 +693,11 @@ impl GitRepository for RealGitRepository { .args([ "--no-optional-locks", "show", - "--format=%P", + "--format=", "-z", "--no-renames", "--name-status", + "--first-parent", ]) .arg(&commit) .stdin(Stdio::null()) @@ -707,9 +708,8 @@ impl GitRepository for RealGitRepository { .context("starting git show process")?; let show_stdout = String::from_utf8_lossy(&show_output.stdout); - let mut lines = show_stdout.split('\n'); - let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0'); - let changes = parse_git_diff_name_status(lines.next().unwrap_or("")); + let changes = parse_git_diff_name_status(&show_stdout); + let parent_sha = format!("{}^", commit); let mut cat_file_process = util::command::new_smol_command(&git_binary_path) .current_dir(&working_directory) diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index 31726dc0b33c5274519c7b77b61c10609ac3bfcc..6059bc9e83b63e710815891165fe6e530a0efa1a 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -98,25 +98,10 @@ impl BlameRenderer for GitBlameRenderer { let workspace = workspace.clone(); move |_, window, cx| { CommitView::open( - CommitSummary { - sha: blame_entry.sha.to_string().into(), - subject: blame_entry - .summary - .clone() - .unwrap_or_default() - .into(), - commit_timestamp: blame_entry - .committer_time - .unwrap_or_default(), - author_name: blame_entry - .committer_name - .clone() - .unwrap_or_default() - .into(), - has_parent: true, - }, + blame_entry.sha.to_string(), repository.downgrade(), workspace.clone(), + None, window, cx, ) @@ -335,9 +320,10 @@ impl BlameRenderer for GitBlameRenderer { .icon_size(IconSize::Small) .on_click(move |_, window, cx| { CommitView::open( - commit_summary.clone(), + commit_summary.sha.clone().into(), repository.downgrade(), workspace.clone(), + None, window, cx, ); @@ -374,15 +360,10 @@ impl BlameRenderer for GitBlameRenderer { cx: &mut App, ) { CommitView::open( - CommitSummary { - sha: blame_entry.sha.to_string().into(), - subject: blame_entry.summary.clone().unwrap_or_default().into(), - commit_timestamp: blame_entry.committer_time.unwrap_or_default(), - author_name: blame_entry.committer_name.unwrap_or_default().into(), - has_parent: true, - }, + blame_entry.sha.to_string(), repository.downgrade(), workspace, + None, window, cx, ) diff --git a/crates/git_ui/src/commit_tooltip.rs b/crates/git_ui/src/commit_tooltip.rs index 84ecc0b3a9c0c708ec81a0af1234506ec0208cd0..97224840debcc4cfd8dcc74a56d448ef0d2826c1 100644 --- a/crates/git_ui/src/commit_tooltip.rs +++ b/crates/git_ui/src/commit_tooltip.rs @@ -318,9 +318,10 @@ impl Render for CommitTooltip { .on_click( move |_, window, cx| { CommitView::open( - commit_summary.clone(), + commit_summary.sha.to_string(), repo.downgrade(), workspace.clone(), + None, window, cx, ); diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index f89afb0a64b4377b235866c4da66e8255f2320d1..9738e13984a0b032b09a218990f3466052e9fa61 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -1,10 +1,11 @@ use anyhow::{Context as _, Result}; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines}; -use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath}; +use git::repository::{CommitDetails, CommitDiff, RepoPath}; use gpui::{ - AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, - FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window, + Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, + Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity, + Window, actions, }; use language::{ Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _, @@ -18,17 +19,42 @@ use std::{ path::PathBuf, sync::Arc, }; -use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString}; +use ui::{ + Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*, +}; use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff}; use workspace::{ - Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, + Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, + Workspace, item::{BreadcrumbText, ItemEvent, TabContentParams}, + notifications::NotifyTaskExt, + pane::SaveIntent, searchable::SearchableItemHandle, }; +use crate::git_panel::GitPanel; + +actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]); + +pub fn init(cx: &mut App) { + cx.observe_new(|workspace: &mut Workspace, _window, _cx| { + register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| { + toolbar.apply_stash(window, cx); + }); + register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| { + toolbar.remove_stash(window, cx); + }); + register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| { + toolbar.pop_stash(window, cx); + }); + }) + .detach(); +} + pub struct CommitView { commit: CommitDetails, editor: Entity, + stash: Option, multibuffer: Entity, } @@ -48,17 +74,18 @@ const FILE_NAMESPACE_SORT_PREFIX: u64 = 1; impl CommitView { pub fn open( - commit: CommitSummary, + commit_sha: String, repo: WeakEntity, workspace: WeakEntity, + stash: Option, window: &mut Window, cx: &mut App, ) { let commit_diff = repo - .update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string())) + .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone())) .ok(); let commit_details = repo - .update(cx, |repo, _| repo.show(commit.sha.to_string())) + .update(cx, |repo, _| repo.show(commit_sha.clone())) .ok(); window @@ -77,6 +104,7 @@ impl CommitView { commit_diff, repo, project.clone(), + stash, window, cx, ) @@ -87,7 +115,7 @@ impl CommitView { let ix = pane.items().position(|item| { let commit_view = item.downcast::(); commit_view - .is_some_and(|view| view.read(cx).commit.sha == commit.sha) + .is_some_and(|view| view.read(cx).commit.sha == commit_sha) }); if let Some(ix) = ix { pane.activate_item(ix, true, true, window, cx); @@ -106,6 +134,7 @@ impl CommitView { commit_diff: CommitDiff, repository: Entity, project: Entity, + stash: Option, window: &mut Window, cx: &mut Context, ) -> Self { @@ -127,10 +156,13 @@ impl CommitView { let mut metadata_buffer_id = None; if let Some(worktree_id) = first_worktree_id { + let title = if let Some(stash) = stash { + format!("stash@{{{}}}", stash) + } else { + format!("commit {}", commit.sha) + }; let file = Arc::new(CommitMetadataFile { - title: RelPath::unix(&format!("commit {}", commit.sha)) - .unwrap() - .into(), + title: RelPath::unix(&title).unwrap().into(), worktree_id, }); let buffer = cx.new(|cx| { @@ -138,7 +170,7 @@ impl CommitView { ReplicaId::LOCAL, cx.entity_id().as_non_zero_u64().into(), LineEnding::default(), - format_commit(&commit).into(), + format_commit(&commit, stash.is_some()).into(), ); metadata_buffer_id = Some(buffer.remote_id()); Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite) @@ -211,6 +243,7 @@ impl CommitView { commit, editor, multibuffer, + stash, } } } @@ -369,9 +402,13 @@ async fn build_buffer_diff( }) } -fn format_commit(commit: &CommitDetails) -> String { +fn format_commit(commit: &CommitDetails, is_stash: bool) -> String { let mut result = String::new(); - writeln!(&mut result, "commit {}", commit.sha).unwrap(); + if is_stash { + writeln!(&mut result, "stash commit {}", commit.sha).unwrap(); + } else { + writeln!(&mut result, "commit {}", commit.sha).unwrap(); + } writeln!( &mut result, "Author: {} <{}>", @@ -538,13 +575,296 @@ impl Item for CommitView { editor, multibuffer, commit: self.commit.clone(), + stash: self.stash, } })) } } impl Render for CommitView { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - self.editor.clone() + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_stash = self.stash.is_some(); + div() + .key_context(if is_stash { "StashDiff" } else { "CommitDiff" }) + .bg(cx.theme().colors().editor_background) + .flex() + .items_center() + .justify_center() + .size_full() + .child(self.editor.clone()) + } +} + +pub struct CommitViewToolbar { + commit_view: Option>, + workspace: WeakEntity, +} + +impl CommitViewToolbar { + pub fn new(workspace: &Workspace, _: &mut Context) -> Self { + Self { + commit_view: None, + workspace: workspace.weak_handle(), + } + } + + fn commit_view(&self, _: &App) -> Option> { + self.commit_view.as_ref()?.upgrade() + } + + async fn close_commit_view( + commit_view: Entity, + workspace: WeakEntity, + cx: &mut AsyncWindowContext, + ) -> anyhow::Result<()> { + workspace + .update_in(cx, |workspace, window, cx| { + let active_pane = workspace.active_pane(); + let commit_view_id = commit_view.entity_id(); + active_pane.update(cx, |pane, cx| { + pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx) + }) + })? + .await?; + anyhow::Ok(()) + } + + fn apply_stash(&mut self, window: &mut Window, cx: &mut Context) { + self.stash_action( + "Apply", + window, + cx, + async move |repository, sha, stash, commit_view, workspace, cx| { + let result = repository.update(cx, |repo, cx| { + if !stash_matches_index(&sha, stash, repo) { + return Err(anyhow::anyhow!("Stash has changed, not applying")); + } + Ok(repo.stash_apply(Some(stash), cx)) + })?; + + match result { + Ok(task) => task.await?, + Err(err) => { + Self::close_commit_view(commit_view, workspace, cx).await?; + return Err(err); + } + }; + Self::close_commit_view(commit_view, workspace, cx).await?; + anyhow::Ok(()) + }, + ); + } + + fn pop_stash(&mut self, window: &mut Window, cx: &mut Context) { + self.stash_action( + "Pop", + window, + cx, + async move |repository, sha, stash, commit_view, workspace, cx| { + let result = repository.update(cx, |repo, cx| { + if !stash_matches_index(&sha, stash, repo) { + return Err(anyhow::anyhow!("Stash has changed, pop aborted")); + } + Ok(repo.stash_pop(Some(stash), cx)) + })?; + + match result { + Ok(task) => task.await?, + Err(err) => { + Self::close_commit_view(commit_view, workspace, cx).await?; + return Err(err); + } + }; + Self::close_commit_view(commit_view, workspace, cx).await?; + anyhow::Ok(()) + }, + ); + } + + fn remove_stash(&mut self, window: &mut Window, cx: &mut Context) { + self.stash_action( + "Drop", + window, + cx, + async move |repository, sha, stash, commit_view, workspace, cx| { + let result = repository.update(cx, |repo, cx| { + if !stash_matches_index(&sha, stash, repo) { + return Err(anyhow::anyhow!("Stash has changed, drop aborted")); + } + Ok(repo.stash_drop(Some(stash), cx)) + })?; + + match result { + Ok(task) => task.await??, + Err(err) => { + Self::close_commit_view(commit_view, workspace, cx).await?; + return Err(err); + } + }; + Self::close_commit_view(commit_view, workspace, cx).await?; + anyhow::Ok(()) + }, + ); + } + + fn stash_action( + &mut self, + str_action: &str, + window: &mut Window, + cx: &mut Context, + callback: AsyncFn, + ) where + AsyncFn: AsyncFnOnce( + Entity, + &SharedString, + usize, + Entity, + WeakEntity, + &mut AsyncWindowContext, + ) -> anyhow::Result<()> + + 'static, + { + let Some(commit_view) = self.commit_view(cx) else { + return; + }; + let Some(stash) = commit_view.read(cx).stash else { + return; + }; + let sha = commit_view.read(cx).commit.sha.clone(); + let answer = window.prompt( + PromptLevel::Info, + &format!("{} stash@{{{}}}?", str_action, stash), + None, + &[str_action, "Cancel"], + cx, + ); + + let workspace = self.workspace.clone(); + cx.spawn_in(window, async move |_, cx| { + if answer.await != Ok(0) { + return anyhow::Ok(()); + } + let repo = workspace.update(cx, |workspace, cx| { + workspace + .panel::(cx) + .and_then(|p| p.read(cx).active_repository.clone()) + })?; + + let Some(repo) = repo else { + return Ok(()); + }; + callback(repo, &sha, stash, commit_view, workspace, cx).await?; + anyhow::Ok(()) + }) + .detach_and_notify_err(window, cx); + } +} + +impl EventEmitter for CommitViewToolbar {} + +impl ToolbarItemView for CommitViewToolbar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + if let Some(entity) = active_pane_item.and_then(|i| i.act_as::(cx)) + && entity.read(cx).stash.is_some() + { + self.commit_view = Some(entity.downgrade()); + return ToolbarItemLocation::PrimaryRight; + } + ToolbarItemLocation::Hidden + } + + fn pane_focus_update( + &mut self, + _pane_focused: bool, + _window: &mut Window, + _cx: &mut Context, + ) { + } +} + +impl Render for CommitViewToolbar { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(commit_view) = self.commit_view(cx) else { + return div(); + }; + + let is_stash = commit_view.read(cx).stash.is_some(); + if !is_stash { + return div(); + } + + let focus_handle = commit_view.focus_handle(cx); + + h_group_xl().my_neg_1().py_1().items_center().child( + h_group_sm() + .child( + Button::new("apply-stash", "Apply") + .tooltip(Tooltip::for_action_title_in( + "Apply current stash", + &ApplyCurrentStash, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))), + ) + .child( + Button::new("pop-stash", "Pop") + .tooltip(Tooltip::for_action_title_in( + "Pop current stash", + &PopCurrentStash, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))), + ) + .child( + Button::new("remove-stash", "Remove") + .icon(IconName::Trash) + .tooltip(Tooltip::for_action_title_in( + "Remove current stash", + &DropCurrentStash, + &focus_handle, + )) + .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))), + ), + ) + } +} + +fn register_workspace_action( + workspace: &mut Workspace, + callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context), +) { + workspace.register_action(move |workspace, action: &A, window, cx| { + if workspace.has_active_modal(window, cx) { + cx.propagate(); + return; + } + + workspace.active_pane().update(cx, |pane, cx| { + pane.toolbar().update(cx, move |workspace, cx| { + if let Some(toolbar) = workspace.item_of_type::() { + toolbar.update(cx, move |toolbar, cx| { + callback(toolbar, action, window, cx); + cx.notify(); + }); + } + }); + }) + }); +} + +fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool { + match repo + .cached_stash() + .entries + .iter() + .find(|entry| entry.index == index) + { + Some(entry) => entry.oid.to_string() == sha, + None => false, } } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ce6ddf43f6dbf1e4df32c45f7c96f7c08447df06..2e34c060f61c5dd9ed6718d379e16019c8e17b16 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3611,9 +3611,10 @@ impl GitPanel { let repo = active_repository.downgrade(); move |_, window, cx| { CommitView::open( - commit.clone(), + commit.sha.to_string(), repo.clone(), workspace.clone(), + None, window, cx, ); diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index da2e2ca032aa005ad619eabf094ae6981975b050..303e23c959557efe859cb069c1e41ff8352923fe 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -34,7 +34,7 @@ mod askpass_modal; pub mod branch_picker; mod commit_modal; pub mod commit_tooltip; -mod commit_view; +pub mod commit_view; mod conflict_view; pub mod file_diff_view; pub mod git_panel; @@ -59,6 +59,7 @@ pub fn init(cx: &mut App) { GitPanelSettings::register(cx); editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx); + commit_view::init(cx); cx.observe_new(|editor: &mut Editor, _, cx| { conflict_view::register_editor(editor, editor.buffer().clone(), cx); diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index d82498007d3d38e509e34e86044fa0a0e188c910..3f159035a0ada5a79d26dd0d1d8222678aed23b3 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -5,18 +5,21 @@ use git::stash::StashEntry; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, - SharedString, Styled, Subscription, Task, Window, actions, rems, + SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, svg, }; use picker::{Picker, PickerDelegate}; use project::git_store::{Repository, RepositoryEvent}; use std::sync::Arc; use time::{OffsetDateTime, UtcOffset}; use time_format; -use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; +use ui::{ + ButtonLike, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, +}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; +use crate::commit_view::CommitView; use crate::stash_picker; actions!( @@ -24,6 +27,8 @@ actions!( [ /// Drop the selected stash entry. DropStashItem, + /// Show the diff view of the selected stash entry. + ShowStashItem, ] ); @@ -38,8 +43,9 @@ pub fn open( cx: &mut Context, ) { let repository = workspace.project().read(cx).active_repository(cx); + let weak_workspace = workspace.weak_handle(); workspace.toggle_modal(window, cx, |window, cx| { - StashList::new(repository, rems(34.), window, cx) + StashList::new(repository, weak_workspace, rems(34.), window, cx) }) } @@ -53,6 +59,7 @@ pub struct StashList { impl StashList { fn new( repository: Option>, + workspace: WeakEntity, width: Rems, window: &mut Window, cx: &mut Context, @@ -98,7 +105,7 @@ impl StashList { }) .detach_and_log_err(cx); - let delegate = StashListDelegate::new(repository, window, cx); + let delegate = StashListDelegate::new(repository, workspace, window, cx); let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); let picker_focus_handle = picker.focus_handle(cx); picker.update(cx, |picker, _| { @@ -131,6 +138,20 @@ impl StashList { cx.notify(); } + fn handle_show_stash( + &mut self, + _: &ShowStashItem, + window: &mut Window, + cx: &mut Context, + ) { + self.picker.update(cx, |picker, cx| { + picker + .delegate + .show_stash_at(picker.delegate.selected_index(), window, cx); + }); + cx.notify(); + } + fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, @@ -157,6 +178,7 @@ impl Render for StashList { .w(self.width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_drop_stash)) + .on_action(cx.listener(Self::handle_show_stash)) .child(self.picker.clone()) } } @@ -172,6 +194,7 @@ pub struct StashListDelegate { matches: Vec, all_stash_entries: Option>, repo: Option>, + workspace: WeakEntity, selected_index: usize, last_query: String, modifiers: Modifiers, @@ -182,6 +205,7 @@ pub struct StashListDelegate { impl StashListDelegate { fn new( repo: Option>, + workspace: WeakEntity, _window: &mut Window, cx: &mut Context, ) -> Self { @@ -192,6 +216,7 @@ impl StashListDelegate { Self { matches: vec![], repo, + workspace, all_stash_entries: None, selected_index: 0, last_query: Default::default(), @@ -235,6 +260,25 @@ impl StashListDelegate { }); } + fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context>) { + let Some(entry_match) = self.matches.get(ix) else { + return; + }; + let stash_sha = entry_match.entry.oid.to_string(); + let stash_index = entry_match.entry.index; + let Some(repo) = self.repo.clone() else { + return; + }; + CommitView::open( + stash_sha, + repo.downgrade(), + self.workspace.clone(), + Some(stash_index), + window, + cx, + ); + } + fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context>) { let Some(repo) = self.repo.clone() else { return; @@ -390,7 +434,7 @@ impl PickerDelegate for StashListDelegate { ix: usize, selected: bool, _window: &mut Window, - _cx: &mut Context>, + cx: &mut Context>, ) -> Option { let entry_match = &self.matches[ix]; @@ -432,11 +476,35 @@ impl PickerDelegate for StashListDelegate { .size(LabelSize::Small), ); + let show_button = div() + .group("show-button-hover") + .child( + ButtonLike::new("show-button") + .child( + svg() + .size(IconSize::Medium.rems()) + .flex_none() + .path(IconName::Eye.path()) + .text_color(Color::Default.color(cx)) + .group_hover("show-button-hover", |this| { + this.text_color(Color::Accent.color(cx)) + }) + .hover(|this| this.text_color(Color::Accent.color(cx))), + ) + .tooltip(Tooltip::for_action_title("Show Stash", &ShowStashItem)) + .on_click(cx.listener(move |picker, _, window, cx| { + cx.stop_propagation(); + picker.delegate.show_stash_at(ix, window, cx); + })), + ) + .into_any_element(); + Some( ListItem::new(SharedString::from(format!("stash-{ix}"))) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) + .end_slot(show_button) .child( v_flex() .w_full() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index e2116415d4b882b66728c43cee90b251b3448013..a9b28229de20b8d24e481482e1441482e5d36212 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -25,6 +25,7 @@ use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; use fs::Fs; use futures::future::Either; use futures::{StreamExt, channel::mpsc, select_biased}; +use git_ui::commit_view::CommitViewToolbar; use git_ui::git_panel::GitPanel; use git_ui::project_diff::ProjectDiffToolbar; use gpui::{ @@ -1049,6 +1050,8 @@ fn initialize_pane( toolbar.add_item(migration_banner, window, cx); let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); toolbar.add_item(project_diff_toolbar, window, cx); + let commit_view_toolbar = cx.new(|cx| CommitViewToolbar::new(workspace, cx)); + toolbar.add_item(commit_view_toolbar, window, cx); let agent_diff_toolbar = cx.new(AgentDiffToolbar::new); toolbar.add_item(agent_diff_toolbar, window, cx); let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx)); From ea5f3e6086e48058020860dd812ae34fd4e27019 Mon Sep 17 00:00:00 2001 From: ming_jiang Date: Tue, 21 Oct 2025 00:12:05 +0800 Subject: [PATCH 061/202] Fix PATH lookup to handle Windows case sensitivity (#40711) Closes #40448 On Windows, PATH might be "Path" instead of "PATH" Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/project/src/lsp_store.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index c68d14d38866b2fbe8a97792bdf46a94469124a1..e8eaa493de9dc14493c95307b42f04711b4eaca0 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -12760,7 +12760,16 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { if self.fs.is_file(&worktree_abs_path).await { worktree_abs_path.pop(); } - let shell_path = self.shell_env().await.get("PATH").cloned(); + + let env = self.shell_env().await; + + // On Windows, PATH might be "Path" instead of "PATH" + let shell_path = env + .get("PATH") + .or_else(|| env.get("Path")) + .or_else(|| env.get("path")) + .cloned(); + which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() } From 7aa0626098aad07e7a921b7e958d43d9e5d532ec Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 18:19:57 +0200 Subject: [PATCH 062/202] editor: Refresh document highlights when expanding excerpts (#40715) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7e633a40e6525668d76ab95d2163a615c296a6e0..18fede90dd344d29dfb2407bce6a06ec875c3a35 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -21040,6 +21040,7 @@ impl Editor { } multi_buffer::Event::ExcerptsExpanded { ids } => { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + self.refresh_document_highlights(cx); cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) } multi_buffer::Event::Reparsed(buffer_id) => { From 0a17f91923ccc327779284ff3524d8e3dabdf910 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 20 Oct 2025 09:25:34 -0700 Subject: [PATCH 063/202] chore: Fix displayed git command (#40548) Too small for release notes IMO Release Notes: - N/A --- crates/project/src/git_store.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 461b15ef5f84a22f2a666674e12d27f0ef341d99..326af767102721987f753012ac82649046543ee0 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4030,7 +4030,7 @@ impl Repository { let this = cx.weak_entity(); self.send_job( - Some(format!("git push {} {} {}", args, branch, remote).into()), + Some(format!("git push {} {} {}", args, remote, branch).into()), move |git_repo, mut cx| async move { match git_repo { RepositoryState::Local { From b6fb1d0a19ab6f3959a51603b9948c05da5f6097 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 20 Oct 2025 11:37:55 -0500 Subject: [PATCH 064/202] settings_ui: Add more settings (#40708) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/settings/default.json | 8 +- .../file_finder/src/file_finder_settings.rs | 6 +- crates/migrator/src/migrations.rs | 6 + .../src/migrations/m_2025_10_17/settings.rs | 23 + crates/migrator/src/migrator.rs | 72 +++ crates/settings/src/settings_content.rs | 35 +- .../settings/src/settings_content/terminal.rs | 13 +- crates/settings/src/settings_content/theme.rs | 27 +- .../src/settings_content/workspace.rs | 14 +- crates/settings_ui/src/page_data.rs | 463 +++++++++++++++--- crates/settings_ui/src/settings_ui.rs | 64 +++ 11 files changed, 650 insertions(+), 81 deletions(-) create mode 100644 crates/migrator/src/migrations/m_2025_10_17/settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 331c696bf890ed16762e7702d03257952a30d124..3afa21be2bb4e1e542b2610c70a7672158d274de 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1091,10 +1091,10 @@ // Only the file Zed had indexed will be used, not necessary all the gitignored files. // // Can accept 3 values: - // * `true`: Use all gitignored files - // * `false`: Use only the files Zed had indexed - // * `null`: Be smart and search for ignored when called from a gitignored worktree - "include_ignored": null + // * "all": Use all gitignored files + // * "indexed": Use only the files Zed had indexed + // * "smart": Be smart and search for ignored when called from a gitignored worktree + "include_ignored": "smart" }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 8689e0ad1e3df2c90c2c033953f08eb31aff052d..4d826211c70b24c9f9bad7e23b8981fa8cb7bdd0 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -18,7 +18,11 @@ impl Settings for FileFinderSettings { file_icons: file_finder.file_icons.unwrap(), modal_max_width: file_finder.modal_max_width.unwrap().into(), skip_focus_for_active_in_search: file_finder.skip_focus_for_active_in_search.unwrap(), - include_ignored: file_finder.include_ignored, + include_ignored: match file_finder.include_ignored.unwrap() { + settings::IncludeIgnoredContent::All => Some(true), + settings::IncludeIgnoredContent::Indexed => Some(false), + settings::IncludeIgnoredContent::Smart => None, + }, } } } diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index 87a301dcad83127666888a7530ba43488ac5a72f..084a3348b54acd9d2fc6ba043e1fb1648bbb3f8b 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -123,3 +123,9 @@ pub(crate) mod m_2025_10_16 { pub(crate) use settings::restore_code_actions_on_format; } + +pub(crate) mod m_2025_10_17 { + mod settings; + + pub(crate) use settings::make_file_finder_include_ignored_an_enum; +} diff --git a/crates/migrator/src/migrations/m_2025_10_17/settings.rs b/crates/migrator/src/migrations/m_2025_10_17/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..83f17a5b8afb441b4bbf481c781eb1fbfa3d036a --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_10_17/settings.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use serde_json::Value; + +pub fn make_file_finder_include_ignored_an_enum(value: &mut Value) -> Result<()> { + let Some(file_finder) = value.get_mut("file_finder") else { + return Ok(()); + }; + + let Some(file_finder_obj) = file_finder.as_object_mut() else { + anyhow::bail!("Expected file_finder to be an object"); + }; + + let Some(include_ignored) = file_finder_obj.get_mut("include_ignored") else { + return Ok(()); + }; + *include_ignored = match include_ignored { + Value::Bool(true) => Value::String("all".to_string()), + Value::Bool(false) => Value::String("indexed".to_string()), + Value::Null => Value::String("smart".to_string()), + _ => anyhow::bail!("Expected include_ignored to be a boolean or null"), + }; + Ok(()) +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 7bb5deb9516668eee699d4078eda7dc74a92be2e..e5f0c584c407284aa175a3ac33f3c9a9e01c1365 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -212,6 +212,7 @@ pub fn migrate_settings(text: &str) -> Result> { &SETTINGS_QUERY_2025_10_03, ), MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format), + MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum), ]; run_migrations(text, migrations) } @@ -2090,4 +2091,75 @@ mod tests { ), ); } + + #[test] + fn test_make_file_finder_include_ignored_an_enum() { + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, + )], + &r#"{ }"#.unindent(), + None, + ); + + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, + )], + &r#"{ + "file_finder": { + "include_ignored": true + } + }"# + .unindent(), + Some( + &r#"{ + "file_finder": { + "include_ignored": "all" + } + }"# + .unindent(), + ), + ); + + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, + )], + &r#"{ + "file_finder": { + "include_ignored": false + } + }"# + .unindent(), + Some( + &r#"{ + "file_finder": { + "include_ignored": "indexed" + } + }"# + .unindent(), + ), + ); + + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, + )], + &r#"{ + "file_finder": { + "include_ignored": null + } + }"# + .unindent(), + Some( + &r#"{ + "file_finder": { + "include_ignored": "smart" + } + }"# + .unindent(), + ), + ); + } } diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 045bc21613141ca30f125ab25757a3b3a97307b0..526bb1b5c7ab8134bc53b79d1c37092cd49144b3 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -608,14 +608,33 @@ pub struct FileFinderSettingsContent { /// Whether to use gitignored files when searching. /// Only the file Zed had indexed will be used, not necessary all the gitignored files. /// - /// Can accept 3 values: - /// * `Some(true)`: Use all gitignored files - /// * `Some(false)`: Use only the files Zed had indexed - /// * `None`: Be smart and search for ignored when called from a gitignored worktree - /// - /// Default: None - /// todo() -> Change this type to an enum - pub include_ignored: Option, + /// Default: Smart + pub include_ignored: Option, +} + +#[derive( + Debug, + PartialEq, + Eq, + Clone, + Copy, + Default, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum IncludeIgnoredContent { + /// Use all gitignored files + All, + /// Use only the files Zed had indexed + Indexed, + /// Be smart and search for ignored when called from a gitignored worktree + #[default] + Smart, } #[derive( diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index e5d3ba60b52073963115934afdd368c582ccfff2..aba2d5209a071b1d8aa50a7feea2965d9cafb948 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -153,7 +153,18 @@ pub enum Shell { }, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::EnumDiscriminants, +)] +#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] #[serde(rename_all = "snake_case")] pub enum WorkingDirectory { /// Use the current file's project directory. Will Fallback to the diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index c988b98a4ef04c7ff5e9b20eaf9e2bebeccb88fb..0228fcbfe832f56c8ad504fd289b273b0a1c0ec7 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -160,7 +160,18 @@ pub enum ThemeSelection { } /// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + Eq, + strum::EnumDiscriminants, +)] +#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] #[serde(untagged)] pub enum IconThemeSelection { /// A static icon theme selection, represented by a single icon theme name. @@ -290,7 +301,19 @@ impl From for String { } /// The buffer's line height. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)] +#[derive( + Clone, + Copy, + Debug, + Serialize, + Deserialize, + PartialEq, + JsonSchema, + MergeFrom, + Default, + strum::EnumDiscriminants, +)] +#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] #[serde(rename_all = "snake_case")] pub enum BufferLineHeight { /// A less dense line height. diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 78b17eeb883dd83b16f45c0cabfc3be9a7840eae..577f8fa4f996b2a808bdc785c56210e766dab2fb 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -382,7 +382,19 @@ pub struct StatusBarSettingsContent { pub cursor_position_button: Option, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::EnumDiscriminants, +)] +#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] #[serde(rename_all = "snake_case")] pub enum AutosaveSetting { /// Disable autosave. diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 6683f42daee9832f493878dc3c8721ff3bc9d268..19a495cf770cddd4dd3252e23ef5be8a6c1308eb 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -252,7 +252,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "How to select the theme", field: Box::new(SettingField { pick: |settings_content| { - Some(&<::Discriminant as strum::VariantArray>::VARIANTS[ + Some(&dynamic_variants::()[ settings_content .theme .theme @@ -263,7 +263,9 @@ pub(crate) fn settings_data(cx: &App) -> Vec { let Some(value) = value else { return; }; - let settings_value = settings_content.theme.theme.as_mut().expect("Has Default"); + let settings_value = settings_content.theme.theme.get_or_insert_with(|| { + settings::ThemeSelection::Static(theme::ThemeName(theme::default_theme(theme::SystemAppearance::default().0).into())) + }); *settings_value = match value { settings::ThemeSelectionDiscriminants::Static => { let name = match settings_value { @@ -298,7 +300,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { pick_discriminant: |settings_content| { Some(settings_content.theme.theme.as_ref()?.discriminant() as usize) }, - fields: <::Discriminant as strum::VariantArray>::VARIANTS.into_iter().map(|variant| { + fields: dynamic_variants::().into_iter().map(|variant| { match variant { settings::ThemeSelectionDiscriminants::Static => vec![ SettingItem { @@ -407,20 +409,169 @@ pub(crate) fn settings_data(cx: &App) -> Vec { } }).collect(), }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Icon Theme", - // todo(settings_ui) - // This description is misleading because the icon theme is used in more places than the file explorer) - description: "Choose the icon theme for file explorer", - field: Box::new( - SettingField { - pick: |settings_content| settings_content.theme.icon_theme.as_ref(), - write: |settings_content, value|{ settings_content.theme.icon_theme = value;}, + SettingsPageItem::DynamicItem(DynamicItem { + discriminant: SettingItem { + files: USER, + title: "Icon Theme", + description: "The Icon Theme Zed Will Associate With Files And Directories", + field: Box::new(SettingField { + pick: |settings_content| { + Some(&dynamic_variants::()[ + settings_content + .theme + .icon_theme + .as_ref()? + .discriminant() as usize]) + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + let settings_value = settings_content.theme.icon_theme.get_or_insert_with(|| { + settings::IconThemeSelection::Static(settings::IconThemeName(theme::default_icon_theme().name.clone().into())) + }); + *settings_value = match value { + settings::IconThemeSelectionDiscriminants::Static => { + let name = match settings_value { + settings::IconThemeSelection::Static(_) => return, + settings::IconThemeSelection::Dynamic { mode, light, dark } => { + match mode { + theme::ThemeMode::Light => light.clone(), + theme::ThemeMode::Dark => dark.clone(), + theme::ThemeMode::System => dark.clone(), // no cx, can't determine correct choice + } + }, + }; + settings::IconThemeSelection::Static(name) + }, + settings::IconThemeSelectionDiscriminants::Dynamic => { + let static_name = match settings_value { + settings::IconThemeSelection::Static(theme_name) => theme_name.clone(), + settings::IconThemeSelection::Dynamic {..} => return, + }; + + settings::IconThemeSelection::Dynamic { + mode: settings::ThemeMode::System, + light: static_name.clone(), + dark: static_name, + } + }, + }; + }, + }), + metadata: None, + }, + pick_discriminant: |settings_content| { + Some(settings_content.theme.icon_theme.as_ref()?.discriminant() as usize) + }, + fields: dynamic_variants::().into_iter().map(|variant| { + match variant { + settings::IconThemeSelectionDiscriminants::Static => vec![ + SettingItem { + files: USER, + title: "Icon Theme Name", + description: "The Name Of The Icon Theme To Use", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.theme.icon_theme.as_ref() { + Some(settings::IconThemeSelection::Static(name)) => Some(name), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .theme + .icon_theme.as_mut() { + Some(settings::IconThemeSelection::Static(theme_name)) => *theme_name = value, + _ => return + } + }, + }), + metadata: None, + } + ], + settings::IconThemeSelectionDiscriminants::Dynamic => vec![ + SettingItem { + files: USER, + title: "Mode", + description: "How To Determine Whether to Use a Light or Dark Icon Theme", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.theme.icon_theme.as_ref() { + Some(settings::IconThemeSelection::Dynamic { mode, ..}) => Some(mode), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .theme + .icon_theme.as_mut() { + Some(settings::IconThemeSelection::Dynamic{ mode, ..}) => *mode = value, + _ => return + } + }, + }), + metadata: None, + }, + SettingItem { + files: USER, + title: "Light Icon Theme", + description: "The Icon Theme To Use When Mode Is Set To Light, Or When Mode Is Set To System And The System Is In Light Mode", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.theme.icon_theme.as_ref() { + Some(settings::IconThemeSelection::Dynamic { light, ..}) => Some(light), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .theme + .icon_theme.as_mut() { + Some(settings::IconThemeSelection::Dynamic{ light, ..}) => *light = value, + _ => return + } + }, + }), + metadata: None, + }, + SettingItem { + files: USER, + title: "Dark Icon Theme", + description: "The Icon Theme To Use When Mode Is Set To Dark, Or When Mode Is Set To System And The System Is In Dark Mode", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.theme.icon_theme.as_ref() { + Some(settings::IconThemeSelection::Dynamic { dark, ..}) => Some(dark), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .theme + .icon_theme.as_mut() { + Some(settings::IconThemeSelection::Dynamic{ dark, ..}) => *dark = value, + _ => return + } + }, + }), + metadata: None, + } + ], } - .unimplemented(), - ), - metadata: None, + }).collect(), }), SettingsPageItem::SectionHeader("Buffer Font"), SettingsPageItem::SettingItem(SettingItem { @@ -453,24 +604,79 @@ pub(crate) fn settings_data(cx: &App) -> Vec { metadata: None, files: USER, }), - // todo(settings_ui): This needs custom ui - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Line Height", - description: "Line height for editor text", - field: Box::new( - SettingField { + SettingsPageItem::DynamicItem(DynamicItem { + discriminant: SettingItem { + files: USER, + title: "Line Height", + description: "Line height for editor text", + field: Box::new(SettingField { pick: |settings_content| { - settings_content.theme.buffer_line_height.as_ref() + Some(&dynamic_variants::()[ + settings_content + .theme + .buffer_line_height + .as_ref()? + .discriminant() as usize]) }, write: |settings_content, value| { - settings_content.theme.buffer_line_height = value; - + let Some(value) = value else { + return; + }; + let settings_value = settings_content.theme.buffer_line_height.get_or_insert_with(|| { + settings::BufferLineHeight::default() + }); + *settings_value = match value { + settings::BufferLineHeightDiscriminants::Comfortable => { + settings::BufferLineHeight::Comfortable + }, + settings::BufferLineHeightDiscriminants::Standard => { + settings::BufferLineHeight::Standard + }, + settings::BufferLineHeightDiscriminants::Custom => { + let custom_value = theme::BufferLineHeight::from(*settings_value).value(); + settings::BufferLineHeight::Custom(custom_value) + }, + }; }, + }), + metadata: None, + }, + pick_discriminant: |settings_content| { + Some(settings_content.theme.buffer_line_height.as_ref()?.discriminant() as usize) + }, + fields: dynamic_variants::().into_iter().map(|variant| { + match variant { + settings::BufferLineHeightDiscriminants::Comfortable => vec![], + settings::BufferLineHeightDiscriminants::Standard => vec![], + settings::BufferLineHeightDiscriminants::Custom => vec![ + SettingItem { + files: USER, + title: "Custom Line Height", + description: "Custom line height value (must be at least 1.0)", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.theme.buffer_line_height.as_ref() { + Some(settings::BufferLineHeight::Custom(value)) => Some(value), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .theme + .buffer_line_height.as_mut() { + Some(settings::BufferLineHeight::Custom(line_height)) => *line_height = f32::max(value, 1.0), + _ => return + } + }, + }), + metadata: None, + } + ], } - .unimplemented(), - ), - metadata: None, + }).collect(), }), SettingsPageItem::SettingItem(SettingItem { files: USER, @@ -835,22 +1041,86 @@ pub(crate) fn settings_data(cx: &App) -> Vec { items: { let mut items = vec![ SettingsPageItem::SectionHeader("Auto Save"), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Save Mode", - description: "When to Auto Save Buffer Changes", - field: Box::new( - SettingField { + SettingsPageItem::DynamicItem(DynamicItem { + discriminant: SettingItem { + files: USER, + title: "Auto Save Mode", + description: "When to Auto Save Buffer Changes", + field: Box::new(SettingField { pick: |settings_content| { - settings_content.workspace.autosave.as_ref() + Some(&dynamic_variants::()[ + settings_content + .workspace + .autosave + .as_ref()? + .discriminant() as usize]) }, write: |settings_content, value| { - settings_content.workspace.autosave = value; + let Some(value) = value else { + return; + }; + let settings_value = settings_content.workspace.autosave.get_or_insert_with(|| { + settings::AutosaveSetting::Off + }); + *settings_value = match value { + settings::AutosaveSettingDiscriminants::Off => { + settings::AutosaveSetting::Off + }, + settings::AutosaveSettingDiscriminants::AfterDelay => { + let milliseconds = match settings_value { + settings::AutosaveSetting::AfterDelay { milliseconds } => *milliseconds, + _ => settings::DelayMs(1000), + }; + settings::AutosaveSetting::AfterDelay { milliseconds } + }, + settings::AutosaveSettingDiscriminants::OnFocusChange => { + settings::AutosaveSetting::OnFocusChange + }, + settings::AutosaveSettingDiscriminants::OnWindowChange => { + settings::AutosaveSetting::OnWindowChange + }, + }; }, + }), + metadata: None, + }, + pick_discriminant: |settings_content| { + Some(settings_content.workspace.autosave.as_ref()?.discriminant() as usize) + }, + fields: dynamic_variants::().into_iter().map(|variant| { + match variant { + settings::AutosaveSettingDiscriminants::Off => vec![], + settings::AutosaveSettingDiscriminants::AfterDelay => vec![ + SettingItem { + files: USER, + title: "Delay (milliseconds)", + description: "Save after inactivity period (in milliseconds)", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.workspace.autosave.as_ref() { + Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => Some(milliseconds), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .workspace + .autosave.as_mut() { + Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => *milliseconds = value, + _ => return + } + }, + }), + metadata: None, + } + ], + settings::AutosaveSettingDiscriminants::OnFocusChange => vec![], + settings::AutosaveSettingDiscriminants::OnWindowChange => vec![], } - .unimplemented(), - ), - metadata: None, - files: USER, + }).collect(), }), SettingsPageItem::SectionHeader("Multibuffer"), SettingsPageItem::SettingItem(SettingItem { @@ -2088,7 +2358,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .include_ignored = value; }, } - .unimplemented(), ), metadata: None, files: USER, @@ -3174,8 +3443,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }), SettingsPageItem::SettingItem(SettingItem { files: USER, - title: "Indent Guides Show", - description: "Show indent guides in the project panel", + title: "Show Indent Guides", + description: "Show Indent Guides In The Project Panel", field: Box::new( SettingField { pick: |settings_content| { @@ -3196,7 +3465,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .show = value; }, } - .unimplemented(), ), metadata: None, }), @@ -3466,8 +3734,8 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }), SettingsPageItem::SettingItem(SettingItem { files: USER, - title: "Indent Guides Show", - description: "When to show indent guides in the outline panel", + title: "Show Indent Guides", + description: "When To Show Indent Guides In The Outline Panel", field: Box::new( SettingField { pick: |settings_content| { @@ -3488,7 +3756,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .show = value; }, } - .unimplemented(), ), metadata: None, }), @@ -3959,31 +4226,91 @@ pub(crate) fn settings_data(cx: &App) -> Vec { metadata: None, files: USER | LOCAL, }), - SettingsPageItem::SettingItem(SettingItem { - title: "Working Directory", - description: "What working directory to use when launching the terminal", - field: Box::new( - SettingField { + SettingsPageItem::DynamicItem(DynamicItem { + discriminant: SettingItem { + files: USER | LOCAL, + title: "Working Directory", + description: "What working directory to use when launching the terminal", + field: Box::new(SettingField { pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .project - .working_directory - .as_ref() + Some(&dynamic_variants::()[ + settings_content + .terminal + .as_ref()? + .project + .working_directory + .as_ref()? + .discriminant() as usize]) }, write: |settings_content, value| { - settings_content + let Some(value) = value else { + return; + }; + let settings_value = settings_content .terminal .get_or_insert_default() .project - .working_directory = value; + .working_directory + .get_or_insert_with(|| settings::WorkingDirectory::CurrentProjectDirectory); + *settings_value = match value { + settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => { + settings::WorkingDirectory::CurrentProjectDirectory + }, + settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => { + settings::WorkingDirectory::FirstProjectDirectory + }, + settings::WorkingDirectoryDiscriminants::AlwaysHome => { + settings::WorkingDirectory::AlwaysHome + }, + settings::WorkingDirectoryDiscriminants::Always => { + let directory = match settings_value { + settings::WorkingDirectory::Always { .. } => return, + _ => String::new(), + }; + settings::WorkingDirectory::Always { directory } + }, + }; }, + }), + metadata: None, + }, + pick_discriminant: |settings_content| { + Some(settings_content.terminal.as_ref()?.project.working_directory.as_ref()?.discriminant() as usize) + }, + fields: dynamic_variants::().into_iter().map(|variant| { + match variant { + settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => vec![], + settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => vec![], + settings::WorkingDirectoryDiscriminants::AlwaysHome => vec![], + settings::WorkingDirectoryDiscriminants::Always => vec![ + SettingItem { + files: USER | LOCAL, + title: "Directory", + description: "The directory path to use (will be shell expanded)", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.terminal.as_ref()?.project.working_directory.as_ref() { + Some(settings::WorkingDirectory::Always { directory }) => Some(directory), + _ => None + } + }, + write: |settings_content, value| { + let value = value.unwrap_or_default(); + match settings_content + .terminal + .get_or_insert_default() + .project + .working_directory.as_mut() { + Some(settings::WorkingDirectory::Always { directory }) => *directory = value, + _ => return + } + }, + }), + metadata: None, + } + ], } - .unimplemented(), - ), - metadata: None, - files: USER | LOCAL, + }).collect(), }), SettingsPageItem::SettingItem(SettingItem { title: "Environment Variables", @@ -6293,3 +6620,11 @@ fn show_scrollbar_or_editor( .as_ref() .and_then(|scrollbar| scrollbar.show.as_ref())) } + +fn dynamic_variants() -> &'static [T::Discriminant] +where + T: strum::IntoDiscriminant, + T::Discriminant: strum::VariantArray, +{ + <::Discriminant as strum::VariantArray>::VARIANTS +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 7d38ccd367890e2df8dd360e9a15fc9155dd1ed0..d430f719c9476e4216e5fedd42269b13b916f4fe 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -439,6 +439,13 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_theme_picker) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_icon_theme_picker) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) // please semicolon stay on next line ; } @@ -2942,6 +2949,63 @@ fn render_theme_picker( .into_any_element() } +fn render_icon_theme_picker( + field: SettingField, + file: SettingsUiFile, + _metadata: Option<&SettingsFieldMetadata>, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); + let current_value = value + .cloned() + .map(|icon_theme_name| icon_theme_name.0.into()) + .unwrap_or_else(|| theme::default_icon_theme().name.clone()); + + DropdownMenu::new( + "font-picker", + current_value.clone(), + ContextMenu::build(window, cx, move |mut menu, _, cx| { + let all_theme_names = theme::ThemeRegistry::global(cx) + .list_icon_themes() + .into_iter() + .map(|theme| theme.name); + for theme_name in all_theme_names { + let file = file.clone(); + let selected = theme_name.as_ref() == current_value.as_ref(); + menu = menu.toggleable_entry( + theme_name.clone(), + selected, + IconPosition::End, + None, + move |_, cx| { + if selected { + return; + } + let theme_name = theme_name.clone(); + update_settings_file(file.clone(), cx, move |settings, _cx| { + (field.write)( + settings, + Some(settings::IconThemeName(theme_name.into())), + ); + }) + .log_err(); // todo(settings_ui) don't log err + }, + ); + } + menu + }), + ) + .trigger_size(ButtonSize::Medium) + .style(DropdownStyle::Outlined) + .offset(gpui::Point { + x: px(0.0), + y: px(2.0), + }) + .tab_index(0) + .into_any_element() +} + #[cfg(test)] mod test { From d8f4293ac3f5807703016cdba575ed2612b335e0 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 19:20:09 +0200 Subject: [PATCH 065/202] sum_tree: Implement recursive `Sumtree::find`, use it over `Cursor::seek` if possible (#40700) Reduces peak stack usage in these functions and should generally be a bit performant. Display map benchmark results ``` To tab point/to_tab_point/1024 time: [531.40 ns 532.10 ns 532.97 ns] change: [-2.1824% -2.0054% -1.8125%] (p = 0.00 < 0.05) Performance has improved. Found 1 outliers among 100 measurements (1.00%) 1 (1.00%) high severe To fold point/to_fold_point/1024 time: [530.81 ns 531.30 ns 531.80 ns] change: [-2.0295% -1.9054% -1.7716%] (p = 0.00 < 0.05) Performance has improved. Found 3 outliers among 100 measurements (3.00%) 2 (2.00%) high mild 1 (1.00%) high severe ``` Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/display_map/block_map.rs | 60 +++++---- crates/editor/src/display_map/fold_map.rs | 87 ++++++------ crates/editor/src/display_map/inlay_map.rs | 56 ++++---- crates/editor/src/display_map/wrap_map.rs | 63 ++++----- crates/gpui/src/elements/list.rs | 27 ++-- crates/multi_buffer/src/multi_buffer.rs | 5 +- .../notifications/src/notification_store.rs | 14 +- crates/rope/src/rope.rs | 38 +++--- crates/sum_tree/src/cursor.rs | 4 +- crates/sum_tree/src/sum_tree.rs | 126 +++++++++++++++++- crates/sum_tree/src/tree_map.rs | 7 +- crates/text/src/anchor.rs | 13 +- crates/text/src/text.rs | 38 +++--- 13 files changed, 332 insertions(+), 206 deletions(-) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index c954e1ba1b487c1c33187e895b7897f8ed67f94e..4b8a48ac2d9d2b35fbd15724c06032056e05ca67 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1521,10 +1521,11 @@ impl BlockSnapshot { } pub(super) fn line_len(&self, row: BlockRow) -> u32 { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&BlockRow(row.0), Bias::Right); - if let Some(transform) = cursor.item() { - let Dimensions(output_start, input_start, _) = cursor.start(); + let (start, _, item) = + self.transforms + .find::, _>((), &row, Bias::Right); + if let Some(transform) = item { + let Dimensions(output_start, input_start, _) = start; let overshoot = row.0 - output_start.0; if transform.block.is_some() { 0 @@ -1539,15 +1540,13 @@ impl BlockSnapshot { } pub(super) fn is_block_line(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&row, Bias::Right); - cursor.item().is_some_and(|t| t.block.is_some()) + let (_, _, item) = self.transforms.find::((), &row, Bias::Right); + item.is_some_and(|t| t.block.is_some()) } pub(super) fn is_folded_buffer_header(&self, row: BlockRow) -> bool { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&row, Bias::Right); - let Some(transform) = cursor.item() else { + let (_, _, item) = self.transforms.find::((), &row, Bias::Right); + let Some(transform) = item else { return false; }; matches!(transform.block, Some(Block::FoldedBuffer { .. })) @@ -1557,9 +1556,10 @@ impl BlockSnapshot { let wrap_point = self .wrap_snapshot .make_wrap_point(Point::new(row.0, 0), Bias::Left); - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); - cursor.item().is_some_and(|transform| { + let (_, _, item) = + self.transforms + .find::((), &WrapRow(wrap_point.row()), Bias::Right); + item.is_some_and(|transform| { transform .block .as_ref() @@ -1627,13 +1627,16 @@ impl BlockSnapshot { } pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); - if let Some(transform) = cursor.item() { + let (start, _, item) = self.transforms.find::, _>( + (), + &WrapRow(wrap_point.row()), + Bias::Right, + ); + if let Some(transform) = item { if transform.block.is_some() { - BlockPoint::new(cursor.start().1.0, 0) + BlockPoint::new(start.1.0, 0) } else { - let Dimensions(input_start_row, output_start_row, _) = cursor.start(); + let Dimensions(input_start_row, output_start_row, _) = start; let input_start = Point::new(input_start_row.0, 0); let output_start = Point::new(output_start_row.0, 0); let input_overshoot = wrap_point.0 - input_start; @@ -1645,26 +1648,29 @@ impl BlockSnapshot { } pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&BlockRow(block_point.row), Bias::Right); - if let Some(transform) = cursor.item() { + let (start, end, item) = self.transforms.find::, _>( + (), + &BlockRow(block_point.row), + Bias::Right, + ); + if let Some(transform) = item { match transform.block.as_ref() { Some(block) => { if block.place_below() { - let wrap_row = cursor.start().1.0 - 1; + let wrap_row = start.1.0 - 1; WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) } else if block.place_above() { - WrapPoint::new(cursor.start().1.0, 0) + WrapPoint::new(start.1.0, 0) } else if bias == Bias::Left { - WrapPoint::new(cursor.start().1.0, 0) + WrapPoint::new(start.1.0, 0) } else { - let wrap_row = cursor.end().1.0 - 1; + let wrap_row = end.1.0 - 1; WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) } } None => { - let overshoot = block_point.row - cursor.start().0.0; - let wrap_row = cursor.start().1.0 + overshoot; + let overshoot = block_point.row - start.0.0; + let wrap_row = start.1.0 + overshoot; WrapPoint::new(wrap_row, block_point.column) } } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index e5d82f8f70a9b5e29622b1302c1eaaf2070b0387..d3bc7acfd303d7952cc46001306067dabb5b089f 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -98,28 +98,26 @@ impl FoldPoint { } pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { - let mut cursor = snapshot + let (start, _, _) = snapshot .transforms - .cursor::>(()); - cursor.seek(&self, Bias::Right); - let overshoot = self.0 - cursor.start().0.0; - InlayPoint(cursor.start().1.0 + overshoot) + .find::, _>((), &self, Bias::Right); + let overshoot = self.0 - start.0.0; + InlayPoint(start.1.0 + overshoot) } pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { - let mut cursor = snapshot + let (start, _, item) = snapshot .transforms - .cursor::>(()); - cursor.seek(&self, Bias::Right); - let overshoot = self.0 - cursor.start().1.output.lines; - let mut offset = cursor.start().1.output.len; + .find::, _>((), &self, Bias::Right); + let overshoot = self.0 - start.1.output.lines; + let mut offset = start.1.output.len; if !overshoot.is_zero() { - let transform = cursor.item().expect("display point out of range"); + let transform = item.expect("display point out of range"); assert!(transform.placeholder.is_none()); let end_inlay_offset = snapshot .inlay_snapshot - .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot)); - offset += end_inlay_offset.0 - cursor.start().1.input.len; + .to_offset(InlayPoint(start.1.input.lines + overshoot)); + offset += end_inlay_offset.0 - start.1.input.len; } FoldOffset(offset) } @@ -706,19 +704,18 @@ impl FoldSnapshot { } pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { - let mut cursor = self + let (start, end, item) = self .transforms - .cursor::>(()); - cursor.seek(&point, Bias::Right); - if cursor.item().is_some_and(|t| t.is_fold()) { - if bias == Bias::Left || point == cursor.start().0 { - cursor.start().1 + .find::, _>((), &point, Bias::Right); + if item.is_some_and(|t| t.is_fold()) { + if bias == Bias::Left || point == start.0 { + start.1 } else { - cursor.end().1 + end.1 } } else { - let overshoot = point.0 - cursor.start().0.0; - FoldPoint(cmp::min(cursor.start().1.0 + overshoot, cursor.end().1.0)) + let overshoot = point.0 - start.0.0; + FoldPoint(cmp::min(start.1.0 + overshoot, end.1.0)) } } @@ -787,9 +784,10 @@ impl FoldSnapshot { { let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer); let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); - let mut cursor = self.transforms.cursor::(()); - cursor.seek(&inlay_offset, Bias::Right); - cursor.item().is_some_and(|t| t.placeholder.is_some()) + let (_, _, item) = self + .transforms + .find::((), &inlay_offset, Bias::Right); + item.is_some_and(|t| t.placeholder.is_some()) } pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool { @@ -891,23 +889,22 @@ impl FoldSnapshot { } pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { - let mut cursor = self + let (start, end, item) = self .transforms - .cursor::>(()); - cursor.seek(&point, Bias::Right); - if let Some(transform) = cursor.item() { - let transform_start = cursor.start().0.0; + .find::, _>((), &point, Bias::Right); + if let Some(transform) = item { + let transform_start = start.0.0; if transform.placeholder.is_some() { if point.0 == transform_start || matches!(bias, Bias::Left) { FoldPoint(transform_start) } else { - FoldPoint(cursor.end().0.0) + FoldPoint(end.0.0) } } else { let overshoot = InlayPoint(point.0 - transform_start); - let inlay_point = cursor.start().1 + overshoot; + let inlay_point = start.1 + overshoot; let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias); - FoldPoint(cursor.start().0.0 + (clipped_inlay_point - cursor.start().1).0) + FoldPoint(start.0.0 + (clipped_inlay_point - start.1).0) } } else { FoldPoint(self.transforms.summary().output.lines) @@ -1480,28 +1477,26 @@ pub struct FoldOffset(pub usize); impl FoldOffset { pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint { - let mut cursor = snapshot + let (start, _, item) = snapshot .transforms - .cursor::>(()); - cursor.seek(&self, Bias::Right); - let overshoot = if cursor.item().is_none_or(|t| t.is_fold()) { - Point::new(0, (self.0 - cursor.start().0.0) as u32) + .find::, _>((), &self, Bias::Right); + let overshoot = if item.is_none_or(|t| t.is_fold()) { + Point::new(0, (self.0 - start.0.0) as u32) } else { - let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0.0; + let inlay_offset = start.1.input.len + self.0 - start.0.0; let inlay_point = snapshot.inlay_snapshot.to_point(InlayOffset(inlay_offset)); - inlay_point.0 - cursor.start().1.input.lines + inlay_point.0 - start.1.input.lines }; - FoldPoint(cursor.start().1.output.lines + overshoot) + FoldPoint(start.1.output.lines + overshoot) } #[cfg(test)] pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { - let mut cursor = snapshot + let (start, _, _) = snapshot .transforms - .cursor::>(()); - cursor.seek(&self, Bias::Right); - let overshoot = self.0 - cursor.start().0.0; - InlayOffset(cursor.start().1.0 + overshoot) + .find::, _>((), &self, Bias::Right); + let overshoot = self.0 - start.0.0; + InlayOffset(start.1.0 + overshoot) } } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 7aeb14fe0eef687ed375e28c6a726799e3876b12..8bda58da173ad45c524c5062bed6daf8309d114d 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -825,22 +825,21 @@ impl InlayMap { impl InlaySnapshot { pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { - let mut cursor = self + let (start, _, item) = self .transforms - .cursor::>(()); - cursor.seek(&offset, Bias::Right); - let overshoot = offset.0 - cursor.start().0.0; - match cursor.item() { + .find::, _>((), &offset, Bias::Right); + let overshoot = offset.0 - start.0.0; + match item { Some(Transform::Isomorphic(_)) => { - let buffer_offset_start = cursor.start().2; + let buffer_offset_start = start.2; let buffer_offset_end = buffer_offset_start + overshoot; let buffer_start = self.buffer.offset_to_point(buffer_offset_start); let buffer_end = self.buffer.offset_to_point(buffer_offset_end); - InlayPoint(cursor.start().1.0 + (buffer_end - buffer_start)) + InlayPoint(start.1.0 + (buffer_end - buffer_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text().offset_to_point(overshoot); - InlayPoint(cursor.start().1.0 + overshoot) + InlayPoint(start.1.0 + overshoot) } None => self.max_point(), } @@ -855,47 +854,48 @@ impl InlaySnapshot { } pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { - let mut cursor = self + let (start, _, item) = self .transforms - .cursor::>(()); - cursor.seek(&point, Bias::Right); - let overshoot = point.0 - cursor.start().0.0; - match cursor.item() { + .find::, _>((), &point, Bias::Right); + let overshoot = point.0 - start.0.0; + match item { Some(Transform::Isomorphic(_)) => { - let buffer_point_start = cursor.start().2; + let buffer_point_start = start.2; let buffer_point_end = buffer_point_start + overshoot; let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); - InlayOffset(cursor.start().1.0 + (buffer_offset_end - buffer_offset_start)) + InlayOffset(start.1.0 + (buffer_offset_end - buffer_offset_start)) } Some(Transform::Inlay(inlay)) => { let overshoot = inlay.text().point_to_offset(overshoot); - InlayOffset(cursor.start().1.0 + overshoot) + InlayOffset(start.1.0 + overshoot) } None => self.len(), } } pub fn to_buffer_point(&self, point: InlayPoint) -> Point { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&point, Bias::Right); - match cursor.item() { + let (start, _, item) = + self.transforms + .find::, _>((), &point, Bias::Right); + match item { Some(Transform::Isomorphic(_)) => { - let overshoot = point.0 - cursor.start().0.0; - cursor.start().1 + overshoot + let overshoot = point.0 - start.0.0; + start.1 + overshoot } - Some(Transform::Inlay(_)) => cursor.start().1, + Some(Transform::Inlay(_)) => start.1, None => self.buffer.max_point(), } } pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { - let mut cursor = self.transforms.cursor::>(()); - cursor.seek(&offset, Bias::Right); - match cursor.item() { + let (start, _, item) = + self.transforms + .find::, _>((), &offset, Bias::Right); + match item { Some(Transform::Isomorphic(_)) => { - let overshoot = offset - cursor.start().0; - cursor.start().1 + overshoot.0 + let overshoot = offset - start.0; + start.1 + overshoot.0 } - Some(Transform::Inlay(_)) => cursor.start().1, + Some(Transform::Inlay(_)) => start.1, None => self.buffer.len(), } } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 39c247cb4e105155e77c8fd5c84e5f185d726af1..6c1ee61de09327d06c17ed977c5f236c2d7232e8 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -568,14 +568,17 @@ impl WrapSnapshot { let mut old_start = old_cursor.start().output.lines; old_start += tab_edit.old.start.0 - old_cursor.start().input.lines; + // todo(lw): Should these be seek_forward? old_cursor.seek(&tab_edit.old.end, Bias::Right); let mut old_end = old_cursor.start().output.lines; old_end += tab_edit.old.end.0 - old_cursor.start().input.lines; + // todo(lw): Should these be seek_forward? new_cursor.seek(&tab_edit.new.start, Bias::Right); let mut new_start = new_cursor.start().output.lines; new_start += tab_edit.new.start.0 - new_cursor.start().input.lines; + // todo(lw): Should these be seek_forward? new_cursor.seek(&tab_edit.new.end, Bias::Right); let mut new_end = new_cursor.start().output.lines; new_end += tab_edit.new.end.0 - new_cursor.start().input.lines; @@ -628,24 +631,22 @@ impl WrapSnapshot { } pub fn line_len(&self, row: u32) -> u32 { - let mut cursor = self - .transforms - .cursor::>(()); - cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); - if cursor - .item() - .is_some_and(|transform| transform.is_isomorphic()) - { - let overshoot = row - cursor.start().0.row(); - let tab_row = cursor.start().1.row() + overshoot; + let (start, _, item) = self.transforms.find::, _>( + (), + &WrapPoint::new(row + 1, 0), + Bias::Left, + ); + if item.is_some_and(|transform| transform.is_isomorphic()) { + let overshoot = row - start.0.row(); + let tab_row = start.1.row() + overshoot; let tab_line_len = self.tab_snapshot.line_len(tab_row); if overshoot == 0 { - cursor.start().0.column() + (tab_line_len - cursor.start().1.column()) + start.0.column() + (tab_line_len - start.1.column()) } else { tab_line_len } } else { - cursor.start().0.column() + start.0.column() } } @@ -711,9 +712,10 @@ impl WrapSnapshot { } pub fn soft_wrap_indent(&self, row: u32) -> Option { - let mut cursor = self.transforms.cursor::(()); - cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right); - cursor.item().and_then(|transform| { + let (.., item) = + self.transforms + .find::((), &WrapPoint::new(row + 1, 0), Bias::Right); + item.and_then(|transform| { if transform.is_isomorphic() { None } else { @@ -749,13 +751,12 @@ impl WrapSnapshot { } pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { - let mut cursor = self - .transforms - .cursor::>(()); - cursor.seek(&point, Bias::Right); - let mut tab_point = cursor.start().1.0; - if cursor.item().is_some_and(|t| t.is_isomorphic()) { - tab_point += point.0 - cursor.start().0.0; + let (start, _, item) = + self.transforms + .find::, _>((), &point, Bias::Right); + let mut tab_point = start.1.0; + if item.is_some_and(|t| t.is_isomorphic()) { + tab_point += point.0 - start.0.0; } TabPoint(tab_point) } @@ -769,19 +770,19 @@ impl WrapSnapshot { } pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { - let mut cursor = self - .transforms - .cursor::>(()); - cursor.seek(&point, Bias::Right); - WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0)) + let (start, ..) = + self.transforms + .find::, _>((), &point, Bias::Right); + WrapPoint(start.1.0 + (point.0 - start.0.0)) } pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint { if bias == Bias::Left { - let mut cursor = self.transforms.cursor::(()); - cursor.seek(&point, Bias::Right); - if cursor.item().is_some_and(|t| !t.is_isomorphic()) { - point = *cursor.start(); + let (start, _, item) = self + .transforms + .find::((), &point, Bias::Right); + if item.is_some_and(|t| !t.is_isomorphic()) { + point = start; *point.column_mut() -= 1; } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 0d2b9971d8074a3051b31f44faf91f9c734f3064..78566208c89a7d6bf73804f611b45aa70e4933ec 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -509,10 +509,11 @@ impl StateInner { if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { self.logical_scroll_top = None; } else { - let mut cursor = self.items.cursor::(()); - cursor.seek(&Height(new_scroll_top), Bias::Right); - let item_ix = cursor.start().count; - let offset_in_item = new_scroll_top - cursor.start().height; + let (start, ..) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); + let item_ix = start.count; + let offset_in_item = new_scroll_top - start.height; self.logical_scroll_top = Some(ListOffset { item_ix, offset_in_item, @@ -550,9 +551,12 @@ impl StateInner { } fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels { - let mut cursor = self.items.cursor::(()); - cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right); - cursor.start().height + logical_scroll_top.offset_in_item + let (start, ..) = self.items.find::( + (), + &Count(logical_scroll_top.item_ix), + Bias::Right, + ); + start.height + logical_scroll_top.offset_in_item } fn layout_all_items( @@ -882,11 +886,12 @@ impl StateInner { if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { self.logical_scroll_top = None; } else { - let mut cursor = self.items.cursor::(()); - cursor.seek(&Height(new_scroll_top), Bias::Right); + let (start, _, _) = + self.items + .find::((), &Height(new_scroll_top), Bias::Right); - let item_ix = cursor.start().count; - let offset_in_item = new_scroll_top - cursor.start().height; + let item_ix = start.count; + let offset_in_item = new_scroll_top - start.height; self.logical_scroll_top = Some(ListOffset { item_ix, offset_in_item, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 94966708ac0e49fc01c7e1617fdd41149fc0a4e9..db15a0774c5411b1ddfb8a3f9772a0ca9789c36c 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6140,9 +6140,8 @@ impl MultiBufferSnapshot { } else if id == ExcerptId::max() { Locator::max_ref() } else { - let mut cursor = self.excerpt_ids.cursor::(()); - cursor.seek(&id, Bias::Left); - if let Some(entry) = cursor.item() + let (_, _, item) = self.excerpt_ids.find::((), &id, Bias::Left); + if let Some(entry) = item && entry.id == id { return &entry.locator; diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index 0964a648b0bead5d46fe7d63113d6bc966673116..7cae74a7293694ebedd603ded656af00201c7366 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -123,14 +123,16 @@ impl NotificationStore { return None; } let ix = count - 1 - ix; - let mut cursor = self.notifications.cursor::(()); - cursor.seek(&Count(ix), Bias::Right); - cursor.item() + let (.., item) = self + .notifications + .find::((), &Count(ix), Bias::Right); + item } pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { - let mut cursor = self.notifications.cursor::(()); - cursor.seek(&NotificationId(id), Bias::Left); - if let Some(item) = cursor.item() + let (.., item) = + self.notifications + .find::((), &NotificationId(id), Bias::Left); + if let Some(item) = item && item.id == id { return Some(item); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index b0b34c95120acddb26081b9265346c80a5afc99c..44e0676bae79bf3e3dee92e4a16652f404c95273 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -41,12 +41,9 @@ impl Rope { if self.chunks.is_empty() { return offset == 0; } - let mut cursor = self.chunks.cursor::(()); - cursor.seek(&offset, Bias::Left); - let chunk_offset = offset - cursor.start(); - cursor - .item() - .map(|chunk| chunk.text.is_char_boundary(chunk_offset)) + let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); + let chunk_offset = offset - start; + item.map(|chunk| chunk.text.is_char_boundary(chunk_offset)) .unwrap_or(false) } @@ -60,10 +57,9 @@ impl Rope { (u8 as i8) >= -0x40 } - let mut cursor = self.chunks.cursor::(()); - cursor.seek(&index, Bias::Left); - let chunk_offset = index - cursor.start(); - let lower_idx = cursor.item().map(|chunk| { + let (start, _, item) = self.chunks.find::((), &index, Bias::Left); + let chunk_offset = index - start; + let lower_idx = item.map(|chunk| { let lower_bound = chunk_offset.saturating_sub(3); chunk .text @@ -78,7 +74,7 @@ impl Rope { }) .unwrap_or(chunk.text.len()) }); - lower_idx.map_or_else(|| self.len(), |idx| cursor.start() + idx) + lower_idx.map_or_else(|| self.len(), |idx| start + idx) } } @@ -92,10 +88,9 @@ impl Rope { (u8 as i8) >= -0x40 } - let mut cursor = self.chunks.cursor::(()); - cursor.seek(&index, Bias::Left); - let chunk_offset = index - cursor.start(); - let upper_idx = cursor.item().map(|chunk| { + let (start, _, item) = self.chunks.find::((), &index, Bias::Left); + let chunk_offset = index - start; + let upper_idx = item.map(|chunk| { let upper_bound = Ord::min(chunk_offset + 4, chunk.text.len()); chunk.text.as_bytes()[chunk_offset..upper_bound] .iter() @@ -103,7 +98,7 @@ impl Rope { .map_or(upper_bound, |pos| pos + chunk_offset) }); - upper_idx.map_or_else(|| self.len(), |idx| cursor.start() + idx) + upper_idx.map_or_else(|| self.len(), |idx| start + idx) } } @@ -356,11 +351,12 @@ impl Rope { if offset >= self.summary().len { return self.summary().len_utf16; } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&offset, Bias::Left); - let overshoot = offset - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(Default::default(), |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &offset, Bias::Left); + let overshoot = offset - start.0; + start.1 + + item.map_or(Default::default(), |chunk| { chunk.as_slice().offset_to_offset_utf16(overshoot) }) } diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 6df1d3da41556dc7eb93f7460b960ccddbe52de6..7418224c86f51a52a8a621da0f2a0c53dcfcf404 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -388,6 +388,7 @@ where T: Item, D: Dimension<'a, T::Summary>, { + /// Returns whether we found the item you were seeking for. #[track_caller] pub fn seek(&mut self, pos: &Target, bias: Bias) -> bool where @@ -397,6 +398,7 @@ where self.seek_internal(pos, bias, &mut ()) } + /// Returns whether we found the item you were seeking for. #[track_caller] pub fn seek_forward(&mut self, pos: &Target, bias: Bias) -> bool where @@ -437,7 +439,7 @@ where summary.0 } - /// Returns whether we found the item you were seeking for + /// Returns whether we found the item you were seeking for. #[track_caller] fn seek_internal( &mut self, diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index bfd2423c9230ea4246509e164d7a03f4890cbf4a..ab0e9d03c4594b89159893c7a671e8a9e3928b3f 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -82,6 +82,11 @@ pub trait Dimension<'a, S: Summary>: Clone { fn zero(cx: S::Context<'_>) -> Self; fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>); + #[must_use] + fn with_added_summary(mut self, summary: &'a S, cx: S::Context<'_>) -> Self { + self.add_summary(summary, cx); + self + } fn from_summary(summary: &'a S, cx: S::Context<'_>) -> Self { let mut dimension = Self::zero(cx); @@ -371,12 +376,122 @@ impl SumTree { Iter::new(self) } - pub fn cursor<'a, 'b, S>( + /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()`. + /// + /// Only returns the item that exactly has the target match. + pub fn find_exact<'a, 'slf, D, Target>( + &'slf self, + cx: ::Context<'a>, + target: &Target, + bias: Bias, + ) -> (D, D, Option<&'slf T>) + where + D: Dimension<'slf, T::Summary>, + Target: SeekTarget<'slf, T::Summary, D>, + { + let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); + let comparison = target.cmp(&tree_end, cx); + if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) + { + return (tree_end.clone(), tree_end, None); + } + + let mut pos = D::zero(cx); + return match Self::find_recurse::<_, _, true>(cx, target, bias, &mut pos, self) { + Some((item, end)) => (pos, end, Some(item)), + None => (pos.clone(), pos, None), + }; + } + + /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()` + pub fn find<'a, 'slf, D, Target>( + &'slf self, + cx: ::Context<'a>, + target: &Target, + bias: Bias, + ) -> (D, D, Option<&'slf T>) + where + D: Dimension<'slf, T::Summary>, + Target: SeekTarget<'slf, T::Summary, D>, + { + let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); + let comparison = target.cmp(&tree_end, cx); + if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) + { + return (tree_end.clone(), tree_end, None); + } + + let mut pos = D::zero(cx); + return match Self::find_recurse::<_, _, false>(cx, target, bias, &mut pos, self) { + Some((item, end)) => (pos, end, Some(item)), + None => (pos.clone(), pos, None), + }; + } + + fn find_recurse<'tree, 'a, D, Target, const EXACT: bool>( + cx: ::Context<'a>, + target: &Target, + bias: Bias, + position: &mut D, + this: &'tree SumTree, + ) -> Option<(&'tree T, D)> + where + D: Dimension<'tree, T::Summary>, + Target: SeekTarget<'tree, T::Summary, D>, + { + match &*this.0 { + Node::Internal { + child_summaries, + child_trees, + .. + } => { + for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) { + let child_end = position.clone().with_added_summary(child_summary, cx); + + let comparison = target.cmp(&child_end, cx); + let target_in_child = comparison == Ordering::Less + || (comparison == Ordering::Equal && bias == Bias::Left); + if target_in_child { + return Self::find_recurse::( + cx, target, bias, position, child_tree, + ); + } + *position = child_end; + } + } + Node::Leaf { + items, + item_summaries, + .. + } => { + for (item, item_summary) in items.iter().zip(item_summaries) { + let mut child_end = position.clone(); + child_end.add_summary(item_summary, cx); + + let comparison = target.cmp(&child_end, cx); + let entry_found = if EXACT { + comparison == Ordering::Equal + } else { + comparison == Ordering::Less + || (comparison == Ordering::Equal && bias == Bias::Left) + }; + if entry_found { + return Some((item, child_end)); + } + + *position = child_end; + } + } + } + None + } + + pub fn cursor<'a, 'b, D>( &'a self, cx: ::Context<'b>, - ) -> Cursor<'a, 'b, T, S> + ) -> Cursor<'a, 'b, T, D> where - S: Dimension<'a, T::Summary>, + D: Dimension<'a, T::Summary>, { Cursor::new(self, cx) } @@ -787,9 +902,8 @@ impl SumTree { key: &T::Key, cx: ::Context<'a>, ) -> Option<&'a T> { - let mut cursor = self.cursor::(cx); - if cursor.seek(key, Bias::Left) { - cursor.item() + if let (_, _, Some(item)) = self.find_exact::(cx, key, Bias::Left) { + Some(item) } else { None } diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 62630407083fc34d74fef0ebf5e17a44ce40f68e..3e56194dddd9910f819e91c209f6701b55efdd02 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -54,9 +54,10 @@ impl TreeMap { } pub fn get(&self, key: &K) -> Option<&V> { - let mut cursor = self.0.cursor::>(()); - cursor.seek(&MapKeyRef(Some(key)), Bias::Left); - if let Some(item) = cursor.item() { + let (.., item) = self + .0 + .find::, _>((), &MapKeyRef(Some(key)), Bias::Left); + if let Some(item) = item { if Some(key) == item.key().0.as_ref() { Some(&item.value) } else { diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 6b0db2f9352997cd5cef4544edc388bc9c5cd209..cf2febdfc505b426fd8d224a2dc29f18d22cd1a8 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -99,13 +99,14 @@ impl Anchor { let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else { return false; }; - let mut fragment_cursor = buffer + let (.., item) = buffer .fragments - .cursor::, usize>>(&None); - fragment_cursor.seek(&Some(fragment_id), Bias::Left); - fragment_cursor - .item() - .is_some_and(|fragment| fragment.visible) + .find::, usize>, _>( + &None, + &Some(fragment_id), + Bias::Left, + ); + item.is_some_and(|fragment| fragment.visible) } } } diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 0516bf21c949db266aad025500f51aab9cec0958..42d9a48430590302f96a1476bdbc3b876ed8f2f6 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2323,12 +2323,15 @@ impl BufferSnapshot { ); }; - let mut fragment_cursor = self + let (start, _, item) = self .fragments - .cursor::, usize>>(&None); - fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left); - let fragment = fragment_cursor.item().unwrap(); - let mut fragment_offset = fragment_cursor.start().1; + .find::, usize>, _>( + &None, + &Some(&insertion.fragment_id), + Bias::Left, + ); + let fragment = item.unwrap(); + let mut fragment_offset = start.1; if fragment.visible { fragment_offset += anchor.offset - insertion.split_offset; } @@ -2410,10 +2413,9 @@ impl BufferSnapshot { offset, ch, char_range, ); } - let mut fragment_cursor = self.fragments.cursor::(&None); - fragment_cursor.seek(&offset, bias); - let fragment = fragment_cursor.item().unwrap(); - let overshoot = offset - *fragment_cursor.start(); + let (start, _, item) = self.fragments.find::(&None, &offset, bias); + let fragment = item.unwrap(); + let overshoot = offset - start; Anchor { timestamp: fragment.timestamp, offset: fragment.insertion_offset + overshoot, @@ -2494,15 +2496,17 @@ impl BufferSnapshot { cursor.next(); Some(cursor) }; - let mut cursor = self - .fragments - .cursor::, FragmentTextSummary>>(&None); - let start_fragment_id = self.fragment_id_for_anchor(&range.start); - cursor.seek(&Some(start_fragment_id), Bias::Left); - let mut visible_start = cursor.start().1.visible; - let mut deleted_start = cursor.start().1.deleted; - if let Some(fragment) = cursor.item() { + let (start, _, item) = self + .fragments + .find::, FragmentTextSummary>, _>( + &None, + &Some(start_fragment_id), + Bias::Left, + ); + let mut visible_start = start.1.visible; + let mut deleted_start = start.1.deleted; + if let Some(fragment) = item { let overshoot = range.start.offset - fragment.insertion_offset; if fragment.visible { visible_start += overshoot; From 78bfda5045a0c65796c7d1b5de3beb166167f331 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 20:01:46 +0200 Subject: [PATCH 066/202] gpui: Improve some `log_err` calls in windows backend (#40717) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/platform/windows/platform.rs | 19 ++++++++++++++++--- crates/gpui/src/platform/windows/window.rs | 19 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5219d8c8177f7c3dc76a7afab258e2c58a0ce6f8..361d8e114308323da8629fae93d257cc38147dba 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -951,17 +951,30 @@ fn file_save_dialog( ) -> Result> { let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? }; if !directory.to_string_lossy().is_empty() - && let Some(full_path) = directory.canonicalize().log_err() + && let Some(full_path) = directory + .canonicalize() + .context("failed to canonicalize directory") + .log_err() { let full_path = SanitizedPath::new(&full_path); let full_path_string = full_path.to_string(); let path_item: IShellItem = unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_string), None)? }; - unsafe { dialog.SetFolder(&path_item).log_err() }; + unsafe { + dialog + .SetFolder(&path_item) + .context("failed to set dialog folder") + .log_err() + }; } if let Some(suggested_name) = suggested_name { - unsafe { dialog.SetFileName(&HSTRING::from(suggested_name)).log_err() }; + unsafe { + dialog + .SetFileName(&HSTRING::from(suggested_name)) + .context("failed to set file name") + .log_err() + }; } unsafe { diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index be038508880bb629c86b10b42d68907f1c60b0ae..e765fa1a22d54a645d094f0df3250f75c94387af 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -169,7 +169,9 @@ impl WindowsWindowState { length: std::mem::size_of::() as u32, ..Default::default() }; - GetWindowPlacement(self.hwnd, &mut placement).log_err(); + GetWindowPlacement(self.hwnd, &mut placement) + .context("failed to get window placement") + .log_err(); placement }; ( @@ -254,7 +256,9 @@ impl WindowsWindowInner { lock.fullscreen_restore_bounds = window_bounds; let style = WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _); let mut rc = RECT::default(); - unsafe { GetWindowRect(this.hwnd, &mut rc) }.log_err(); + unsafe { GetWindowRect(this.hwnd, &mut rc) } + .context("failed to get window rect") + .log_err(); let _ = lock.fullscreen.insert(StyleAndBounds { style, x: rc.left, @@ -301,15 +305,20 @@ impl WindowsWindowInner { }; match open_status.state { WindowOpenState::Maximized => unsafe { - SetWindowPlacement(self.hwnd, &open_status.placement)?; + SetWindowPlacement(self.hwnd, &open_status.placement) + .context("failed to set window placement")?; ShowWindowAsync(self.hwnd, SW_MAXIMIZE).ok()?; }, WindowOpenState::Fullscreen => { - unsafe { SetWindowPlacement(self.hwnd, &open_status.placement)? }; + unsafe { + SetWindowPlacement(self.hwnd, &open_status.placement) + .context("failed to set window placement")? + }; self.toggle_fullscreen(); } WindowOpenState::Windowed => unsafe { - SetWindowPlacement(self.hwnd, &open_status.placement)?; + SetWindowPlacement(self.hwnd, &open_status.placement) + .context("failed to set window placement")?; }, } Ok(()) From 13fe9938c2b3fe9bd1a3f9e0b018a1085d5e8fa3 Mon Sep 17 00:00:00 2001 From: Josh Piasecki <138541977+FloppyDisco@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:18:01 -0500 Subject: [PATCH 067/202] CollapseAllDiffHunks action for editor (#40668) This PR adds a new action `editor::CollapseAllDiffHunks` which will allow the user to choose any keybinding for hiding the Expanded Diff Hunks. --- crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 11 +++++++++++ crates/editor/src/element.rs | 1 + 3 files changed, 14 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 530a547bb9d28d9dadeb95f5e9e98425eb967a55..810b84efcd40de6e507dfe12b1a1a7f89d2ec4cf 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -458,6 +458,8 @@ actions!( /// Expands all diff hunks in the editor. #[action(deprecated_aliases = ["editor::ExpandAllHunkDiffs"])] ExpandAllDiffHunks, + /// Collapses all diff hunks in the editor. + CollapseAllDiffHunks, /// Expands macros recursively at cursor position. ExpandMacroRecursively, /// Finds all references to the symbol at cursor. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 18fede90dd344d29dfb2407bce6a06ec875c3a35..20ebff811d544d0261e90c43336e569e9a5700ef 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -18792,6 +18792,17 @@ impl Editor { }); } + pub fn collapse_all_diff_hunks( + &mut self, + _: &CollapseAllDiffHunks, + _window: &mut Window, + cx: &mut Context, + ) { + self.buffer.update(cx, |buffer, cx| { + buffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) + }); + } + pub fn toggle_selected_diff_hunks( &mut self, _: &ToggleSelectedDiffHunks, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 52e73bd01c1712371805610f2aff2de6d7aa2d4b..b80953ad842b5c35dbfbbd8872f329760bd9b7a0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -493,6 +493,7 @@ impl EditorElement { register_action(editor, window, Editor::stage_and_next); register_action(editor, window, Editor::unstage_and_next); register_action(editor, window, Editor::expand_all_diff_hunks); + register_action(editor, window, Editor::collapse_all_diff_hunks); register_action(editor, window, Editor::go_to_previous_change); register_action(editor, window, Editor::go_to_next_change); From 057c3c1206dd8d7452c81bc9bef0e1b0650c81ab Mon Sep 17 00:00:00 2001 From: Josh Piasecki <138541977+FloppyDisco@users.noreply.github.com> Date: Mon, 20 Oct 2025 13:23:46 -0500 Subject: [PATCH 068/202] Add dock state to workspace context (#40454) I love keybindings. I spend way to much time thinking about them. I also REALLY like working in Zed. so far, however, I have found the key context system in Zed to be less flexible than in VSCode. the HUGE context that is available in VSCode helps you create keybindings for very specific targeted scenarios. the tree like structure of the Zed key context means you loose some information as focus moves throughout the application. For example, it is not currently possible to create a keybinding in the editor that will only work when one of the Docks is open, or if a specific dock is open. this would be useful in implementing solutions to ideas like #24222 we already have an action for moving focus to the dock, and we have an action for opening/closing the dock, but to my knowledge (very limited lol) we cannot determine if that dock *is open* unless we are focused on it. I think it is possible to create a more flexible key binding system by adding more context information to the higher up context ancestors. while: ``` Workspace right_dock=GitPanel Dock GitPanel Editor ``` may seem redundant, it actually communicates fundamentally different information than: ``` Workspace right_dock=GitPanel Pane Editor ``` the first says "the GitPanel is in the right hand dock AND IT IS FOCUSED", while the second means "Focus is on the Editor, and the GitPanel just happens to be open in the right hand dock" This change adds a new set of identifiers to the `Workspace` key_context that will indicate which docks are open and what is the specific panel that is currently visible in that dock. examples: - `left_dock=ProjectPanel` - `bottom_dock=TerminalPanel` - `right_dock=GitPanel` in my testing the following types of keybindings seem to be supported with this change: ```jsonc // match for any value of the identifier "context": "Workspace && bottom_dock" "context": "Workspace && !bottom_dock" // match only a specific value to an identifier "context": "Workspace && bottom_dock=TerminalPanel" // match only in a child context if the ancestor workspace has the correct identifier "context": "Workspace && !bottom_dock=DebugPanel > Editor" ``` some screen shots of the context matching in different circumstances: Screenshot 2025-10-16 at 23 20 34 Screenshot 2025-10-16 at 23 20 57 Screenshot 2025-10-16 at 23 21 37 Screenshot 2025-10-16 at 23 21 52 Screenshot 2025-10-16 at 23 22 38 the persistent_name values for `ProjectPanel` and `OutlinePanel` needed to be updated to not have a space in them in order to pass the `Identifier` check. all the other Panels already had names that did not include spaces, so it just makes these conform with the other ones. I think this is a great place to start with adding more context identifiers and i think this type of additional information will make it possible to create really dynamic keybindings! Release Notes: - Workspace key context now includes the state of the 3 docks --- crates/agent_ui/src/agent_panel.rs | 4 ++++ crates/collab_ui/src/collab_panel.rs | 4 ++++ crates/collab_ui/src/notification_panel.rs | 4 ++++ crates/debugger_ui/src/debugger_panel.rs | 6 ++++++ crates/git_ui/src/git_panel.rs | 4 ++++ crates/outline_panel/src/outline_panel.rs | 4 ++++ crates/project_panel/src/project_panel.rs | 4 ++++ crates/terminal_view/src/terminal_panel.rs | 4 ++++ crates/workspace/src/dock.rs | 10 ++++++++++ crates/workspace/src/workspace.rs | 18 ++++++++++++++++++ 10 files changed, 62 insertions(+) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2def41c74dd715637f269e572342d04b944505b5..444ee22fd9098deb83614fdfc7dbd26d90783c34 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1395,6 +1395,10 @@ impl Panel for AgentPanel { "AgentPanel" } + fn panel_key() -> &'static str { + AGENT_PANEL_KEY + } + fn position(&self, _window: &Window, cx: &App) -> DockPosition { agent_panel_dock_position(cx) } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index e9c9c4b0fae49fc97c30bdc7697460f82043750e..bfbf9721fab6df79ddd97810fa5b1d70ee701866 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -3037,6 +3037,10 @@ impl Panel for CollabPanel { "CollabPanel" } + fn panel_key() -> &'static str { + COLLABORATION_PANEL_KEY + } + fn activation_priority(&self) -> u32 { 6 } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index 3d988c4634ded9bd2c94d8a75886cf452e64eacb..bab4234f14ed3bba6b408efcc0170f7e15efaf50 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -612,6 +612,10 @@ impl Panel for NotificationPanel { "NotificationPanel" } + fn panel_key() -> &'static str { + NOTIFICATION_PANEL_KEY + } + fn position(&self, _: &Window, cx: &App) -> DockPosition { NotificationPanelSettings::get_global(cx).dock } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 093cef3630e3ae3627a2999b9deb81be3b0aeb8d..11d8683209eeac56c7f5a156c367a627e27ad459 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -43,6 +43,8 @@ use workspace::{ }; use zed_actions::ToggleFocus; +const DEBUG_PANEL_KEY: &str = "DebugPanel"; + pub struct DebugPanel { size: Pixels, active_session: Option>, @@ -1414,6 +1416,10 @@ impl Panel for DebugPanel { "DebugPanel" } + fn panel_key() -> &'static str { + DEBUG_PANEL_KEY + } + fn position(&self, _window: &Window, cx: &App) -> DockPosition { DebuggerSettings::get_global(cx).dock.into() } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 2e34c060f61c5dd9ed6718d379e16019c8e17b16..2678d96041b4fb1123388bbd61db904a924fc6c8 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4420,6 +4420,10 @@ impl Panel for GitPanel { "GitPanel" } + fn panel_key() -> &'static str { + GIT_PANEL_KEY + } + fn position(&self, _: &Window, cx: &App) -> DockPosition { GitPanelSettings::get_global(cx).dock } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 4a4990b40a5f3f7ad2f182e007593e62a8bcd015..ebc5946acf97b763d7ec06d264aeaa7169d7c68b 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4838,6 +4838,10 @@ impl Panel for OutlinePanel { "Outline Panel" } + fn panel_key() -> &'static str { + OUTLINE_PANEL_KEY + } + fn position(&self, _: &Window, cx: &App) -> DockPosition { match OutlinePanelSettings::get_global(cx).dock { DockSide::Left => DockPosition::Left, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d9decd954b8f15abd3d5126b3e8f475013a9b895..98ff619588d42f80ec53e9e91133d47e63cfcbee 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6016,6 +6016,10 @@ impl Panel for ProjectPanel { "Project Panel" } + fn panel_key() -> &'static str { + PROJECT_PANEL_KEY + } + fn starts_open(&self, _: &Window, cx: &App) -> bool { if !ProjectPanelSettings::get_global(cx).starts_open { return false; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 3f0ea0e274edce225d8fd4754043b49d76bf05b4..cb80d58c13128fad19b647e060001e5cf63f052b 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1664,6 +1664,10 @@ impl Panel for TerminalPanel { "TerminalPanel" } + fn panel_key() -> &'static str { + TERMINAL_PANEL_KEY + } + fn icon(&self, _window: &Window, cx: &App) -> Option { if (self.is_enabled(cx) || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index d67d3c81a9fbf67518a49c6c75a842a50ca78684..5958ba210f2dc984c3a8d698013a69548bbb3fcf 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -27,6 +27,7 @@ pub use proto::PanelId; pub trait Panel: Focusable + EventEmitter + Render + Sized { fn persistent_name() -> &'static str; + fn panel_key() -> &'static str; fn position(&self, window: &Window, cx: &App) -> DockPosition; fn position_is_valid(&self, position: DockPosition) -> bool; fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context); @@ -61,6 +62,7 @@ pub trait Panel: Focusable + EventEmitter + Render + Sized { pub trait PanelHandle: Send + Sync { fn panel_id(&self) -> EntityId; fn persistent_name(&self) -> &'static str; + fn panel_key(&self) -> &'static str; fn position(&self, window: &Window, cx: &App) -> DockPosition; fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool; fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App); @@ -108,6 +110,10 @@ where T::persistent_name() } + fn panel_key(&self) -> &'static str { + T::panel_key() + } + fn position(&self, window: &Window, cx: &App) -> DockPosition { self.read(cx).position(window, cx) } @@ -1016,6 +1022,10 @@ pub mod test { "TestPanel" } + fn panel_key() -> &'static str { + "TestPanel" + } + fn position(&self, _window: &Window, _: &App) -> super::DockPosition { self.position } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 85a17c244bebe5d3c33d1bda1fe4ae5758038e56..53f416ae805e692db48b6676a3484e6f839feb99 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6373,6 +6373,24 @@ impl Render for Workspace { } } + if self.left_dock.read(cx).is_open() { + if let Some(active_panel) = self.left_dock.read(cx).active_panel() { + context.set("left_dock", active_panel.panel_key()); + } + } + + if self.right_dock.read(cx).is_open() { + if let Some(active_panel) = self.right_dock.read(cx).active_panel() { + context.set("right_dock", active_panel.panel_key()); + } + } + + if self.bottom_dock.read(cx).is_open() { + if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() { + context.set("bottom_dock", active_panel.panel_key()); + } + } + let centered_layout = self.centered_layout && self.center.panes().len() == 1 && self.active_item(cx).is_some(); From 79ada634aca3a03cfb9276eb5b74128d1d420549 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 20 Oct 2025 20:49:05 +0200 Subject: [PATCH 069/202] acp: Fix following for agents that only provide locations (#40710) We were dropping the entities once we created the buffers, so the weak entities could never be upgraded. This treats new locations we see the same as we would for a read/write call and stores the entity so that we can follow like we normally would. Release Notes: - acp: Fix following not working with certain tool calls. --- crates/acp_thread/src/acp_thread.rs | 72 +++++++++++++++++++---------- 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index f041c9120a52e4b9c23acf121110f00ee157641e..ed2e01f0b37197d6878dee699fba43ed3410066f 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -328,7 +328,7 @@ impl ToolCall { location: acp::ToolCallLocation, project: WeakEntity, cx: &mut AsyncApp, - ) -> Option { + ) -> Option { let buffer = project .update(cx, |project, cx| { project @@ -350,17 +350,14 @@ impl ToolCall { }) .ok()?; - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }) + Some(ResolvedLocation { buffer, position }) } fn resolve_locations( &self, project: Entity, cx: &mut App, - ) -> Task>> { + ) -> Task>> { let locations = self.locations.clone(); project.update(cx, |_, cx| { cx.spawn(async move |project, cx| { @@ -374,6 +371,23 @@ impl ToolCall { } } +// Separate so we can hold a strong reference to the buffer +// for saving on the thread +#[derive(Clone, Debug, PartialEq, Eq)] +struct ResolvedLocation { + buffer: Entity, + position: Anchor, +} + +impl From<&ResolvedLocation> for AgentLocation { + fn from(value: &ResolvedLocation) -> Self { + Self { + buffer: value.buffer.downgrade(), + position: value.position, + } + } +} + #[derive(Debug)] pub enum ToolCallStatus { /// The tool call hasn't started running yet, but we start showing it to @@ -1393,35 +1407,43 @@ impl AcpThread { let task = tool_call.resolve_locations(project, cx); cx.spawn(async move |this, cx| { let resolved_locations = task.await; + this.update(cx, |this, cx| { let project = this.project.clone(); + + for location in resolved_locations.iter().flatten() { + this.shared_buffers + .insert(location.buffer.clone(), location.buffer.read(cx).snapshot()); + } let Some((ix, tool_call)) = this.tool_call_mut(&id) else { return; }; + if let Some(Some(location)) = resolved_locations.last() { project.update(cx, |project, cx| { - if let Some(agent_location) = project.agent_location() { - let should_ignore = agent_location.buffer == location.buffer - && location - .buffer - .update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let old_position = - agent_location.position.to_point(&snapshot); - let new_position = location.position.to_point(&snapshot); - // ignore this so that when we get updates from the edit tool - // the position doesn't reset to the startof line - old_position.row == new_position.row - && old_position.column > new_position.column - }) - .ok() - .unwrap_or_default(); - if !should_ignore { - project.set_agent_location(Some(location.clone()), cx); - } + let should_ignore = if let Some(agent_location) = project.agent_location() { + let snapshot = location.buffer.read(cx).snapshot(); + let old_position = agent_location.position.to_point(&snapshot); + let new_position = location.position.to_point(&snapshot); + agent_location.buffer == location.buffer + // ignore this so that when we get updates from the edit tool + // the position doesn't reset to the startof line + && (old_position.row == new_position.row + && old_position.column > new_position.column) + } else { + false + }; + if !should_ignore { + project.set_agent_location(Some(location.into()), cx); } }); } + + let resolved_locations = resolved_locations + .iter() + .map(|l| l.as_ref().map(|l| AgentLocation::from(l))) + .collect::>(); + if tool_call.resolved_locations != resolved_locations { tool_call.resolved_locations = resolved_locations; cx.emit(AcpThreadEvent::EntryUpdated(ix)); From 853d7c3f92801e46b66733de67b6c151cc43da58 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 20 Oct 2025 20:57:00 +0200 Subject: [PATCH 070/202] gpui: Fix uniform list scrolling with vertical padding present (#40719) Closes #40267 Release Notes: - Fixed a rare issue where the extension page would stutter while scrolling. --- crates/gpui/src/elements/uniform_list.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 949d4339e616cd9f49b3783f46da0f80424c474f..3b721d4238785a12e54b79400324807270501e6c 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -364,17 +364,7 @@ impl Element for UniformList { content_size, window, cx, - |style, mut scroll_offset, hitbox, window, cx| { - let border = style.border_widths.to_pixels(window.rem_size()); - let padding = style - .padding - .to_pixels(bounds.size.into(), window.rem_size()); - - let padded_bounds = Bounds::from_corners( - bounds.origin + point(border.left + padding.left, border.top), - bounds.bottom_right() - point(border.right + padding.right, border.bottom), - ); - + |_style, mut scroll_offset, hitbox, window, cx| { let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { let scroll_state = scroll_handle.0.borrow(); scroll_state.y_flipped From 33bc586ed16995f9c31e8f4d10d4f3a9d96172fb Mon Sep 17 00:00:00 2001 From: Jose Garcia <47431411+ruxwez@users.noreply.github.com> Date: Mon, 20 Oct 2025 22:23:24 +0200 Subject: [PATCH 071/202] theme: Change the icon used for JSONC files (#40726) Closes #40683 Release Notes: - Changed jsonc files' icon --- crates/theme/src/icon_theme.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 513dedfe428e68ff708302e5a23ff64ad6d66d0a..c3e7f3cfbc25cc04f05cd939f74154a732f16f58 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -152,7 +152,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ ), ("java", &["java"]), ("javascript", &["cjs", "js", "mjs"]), - ("json", &["json"]), + ("json", &["json", "jsonc"]), ("julia", &["jl"]), ("kdl", &["kdl"]), ("kotlin", &["kt"]), @@ -199,9 +199,9 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ ( "storage", &[ - "accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "jsonc", - "ldf", "mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql", - "sqlite", "tsv", + "accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "ldf", + "mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql", "sqlite", + "tsv", ], ), ( From ebaefa8cbcaa0439d8bb74646d6140a05dd45db7 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 20 Oct 2025 15:25:20 -0500 Subject: [PATCH 072/202] settings_ui: Add maybe settings (#40724) Closes #ISSUE Adds a `Maybe` type to `settings_content`, that makes the distinction between `null` and omitted settings values explicit. This unlocks a few more settings in the settings UI Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/settings/default.json | 4 +- crates/settings/src/settings_content.rs | 215 ++++++++++++++++++ .../settings/src/settings_content/project.rs | 10 +- .../settings/src/settings_content/terminal.rs | 14 +- crates/settings/src/vscode_import.rs | 2 +- crates/settings_ui/src/page_data.rs | 215 ++++++++++++++++-- crates/settings_ui/src/settings_ui.rs | 3 + crates/worktree/src/worktree_settings.rs | 2 +- 8 files changed, 434 insertions(+), 31 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 3afa21be2bb4e1e542b2610c70a7672158d274de..327b35c4b197818c09a065e2b6203430a6150665 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1,8 +1,8 @@ { "$schema": "zed://schemas/settings", - /// The displayed name of this project. If not set or empty, the root directory name + /// The displayed name of this project. If not set or null, the root directory name /// will be displayed. - "project_name": "", + "project_name": null, // The name of the Zed theme to use for the UI. // // `mode` is one of: diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 526bb1b5c7ab8134bc53b79d1c37092cd49144b3..94bde9e4e403a090a32e145e532114e7a3b65681 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -1034,3 +1034,218 @@ impl std::fmt::Display for DelayMs { write!(f, "{}ms", self.0) } } + +/// A wrapper type that distinguishes between an explicitly set value (including null) and an unset value. +/// +/// This is useful for configuration where you need to differentiate between: +/// - A field that is not present in the configuration file (`Maybe::Unset`) +/// - A field that is explicitly set to `null` (`Maybe::Set(None)`) +/// - A field that is explicitly set to a value (`Maybe::Set(Some(value))`) +/// +/// # Examples +/// +/// In JSON: +/// - `{}` (field missing) deserializes to `Maybe::Unset` +/// - `{"field": null}` deserializes to `Maybe::Set(None)` +/// - `{"field": "value"}` deserializes to `Maybe::Set(Some("value"))` +/// +/// WARN: This type should not be wrapped in an option inside of settings, otherwise the default `serde_json` behavior +/// of treating `null` and missing as the `Option::None` will be used +#[derive(Debug, Clone, PartialEq, Eq, strum::EnumDiscriminants, Default)] +#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] +pub enum Maybe { + /// An explicitly set value, which may be `None` (representing JSON `null`) or `Some(value)`. + Set(Option), + /// A value that was not present in the configuration. + #[default] + Unset, +} + +impl merge_from::MergeFrom for Maybe { + fn merge_from(&mut self, other: &Self) { + if self.is_unset() { + *self = other.clone(); + } + } +} + +impl From>> for Maybe { + fn from(value: Option>) -> Self { + match value { + Some(value) => Maybe::Set(value), + None => Maybe::Unset, + } + } +} + +impl Maybe { + pub fn is_set(&self) -> bool { + matches!(self, Maybe::Set(_)) + } + + pub fn is_unset(&self) -> bool { + matches!(self, Maybe::Unset) + } + + pub fn into_inner(self) -> Option { + match self { + Maybe::Set(value) => value, + Maybe::Unset => None, + } + } + + pub fn as_ref(&self) -> Option<&Option> { + match self { + Maybe::Set(value) => Some(value), + Maybe::Unset => None, + } + } +} + +impl serde::Serialize for Maybe { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Maybe::Set(value) => value.serialize(serializer), + Maybe::Unset => serializer.serialize_none(), + } + } +} + +impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Maybe { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Option::::deserialize(deserializer).map(Maybe::Set) + } +} + +impl JsonSchema for Maybe { + fn schema_name() -> std::borrow::Cow<'static, str> { + format!("Nullable<{}>", T::schema_name()).into() + } + + fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema { + let mut schema = generator.subschema_for::>(); + // Add description explaining that null is an explicit value + let description = if let Some(existing_desc) = + schema.get("description").and_then(|desc| desc.as_str()) + { + format!( + "{}. Note: `null` is treated as an explicit value, different from omitting the field entirely.", + existing_desc + ) + } else { + "This field supports explicit `null` values. Omitting the field is different from setting it to `null`.".to_string() + }; + + schema.insert("description".to_string(), description.into()); + + schema + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json; + + #[test] + fn test_maybe() { + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct TestStruct { + #[serde(default)] + #[serde(skip_serializing_if = "Maybe::is_unset")] + field: Maybe, + } + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct NumericTest { + #[serde(default)] + value: Maybe, + } + + let json = "{}"; + let result: TestStruct = serde_json::from_str(json).unwrap(); + assert!(result.field.is_unset()); + assert_eq!(result.field, Maybe::Unset); + + let json = r#"{"field": null}"#; + let result: TestStruct = serde_json::from_str(json).unwrap(); + assert!(result.field.is_set()); + assert_eq!(result.field, Maybe::Set(None)); + + let json = r#"{"field": "hello"}"#; + let result: TestStruct = serde_json::from_str(json).unwrap(); + assert!(result.field.is_set()); + assert_eq!(result.field, Maybe::Set(Some("hello".to_string()))); + + let test = TestStruct { + field: Maybe::Unset, + }; + let json = serde_json::to_string(&test).unwrap(); + assert_eq!(json, "{}"); + + let test = TestStruct { + field: Maybe::Set(None), + }; + let json = serde_json::to_string(&test).unwrap(); + assert_eq!(json, r#"{"field":null}"#); + + let test = TestStruct { + field: Maybe::Set(Some("world".to_string())), + }; + let json = serde_json::to_string(&test).unwrap(); + assert_eq!(json, r#"{"field":"world"}"#); + + let default_maybe: Maybe = Maybe::default(); + assert!(default_maybe.is_unset()); + + let unset: Maybe = Maybe::Unset; + assert!(unset.is_unset()); + assert!(!unset.is_set()); + + let set_none: Maybe = Maybe::Set(None); + assert!(set_none.is_set()); + assert!(!set_none.is_unset()); + + let set_some: Maybe = Maybe::Set(Some("value".to_string())); + assert!(set_some.is_set()); + assert!(!set_some.is_unset()); + + let original = TestStruct { + field: Maybe::Set(Some("test".to_string())), + }; + let json = serde_json::to_string(&original).unwrap(); + let deserialized: TestStruct = serde_json::from_str(&json).unwrap(); + assert_eq!(original, deserialized); + + let json = r#"{"value": 42}"#; + let result: NumericTest = serde_json::from_str(json).unwrap(); + assert_eq!(result.value, Maybe::Set(Some(42))); + + let json = r#"{"value": null}"#; + let result: NumericTest = serde_json::from_str(json).unwrap(); + assert_eq!(result.value, Maybe::Set(None)); + + let json = "{}"; + let result: NumericTest = serde_json::from_str(json).unwrap(); + assert_eq!(result.value, Maybe::Unset); + + // Test JsonSchema implementation + use schemars::schema_for; + let schema = schema_for!(Maybe); + let schema_json = serde_json::to_value(&schema).unwrap(); + + // Verify the description mentions that null is an explicit value + let description = schema_json["description"].as_str().unwrap(); + assert!( + description.contains("null") && description.contains("explicit"), + "Schema description should mention that null is an explicit value. Got: {}", + description + ); + } +} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index a26a72e7aa32d513dd5afe3c7aa5078f3e3201a5..6a77b815fa547d41e6f38541fe1d681c82b3347b 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -8,7 +8,7 @@ use settings_macros::MergeFrom; use util::serde::default_true; use crate::{ - AllLanguageSettingsContent, DelayMs, ExtendingVec, ProjectTerminalSettingsContent, + AllLanguageSettingsContent, DelayMs, ExtendingVec, Maybe, ProjectTerminalSettingsContent, SlashCommandSettings, }; @@ -56,11 +56,13 @@ pub struct ProjectSettingsContent { #[skip_serializing_none] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] pub struct WorktreeSettingsContent { - /// The displayed name of this project. If not set or empty, the root directory name + /// The displayed name of this project. If not set or null, the root directory name /// will be displayed. /// - /// Default: "" - pub project_name: Option, + /// Default: null + #[serde(default)] + #[serde(skip_serializing_if = "Maybe::is_unset")] + pub project_name: Maybe, /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides /// `file_scan_inclusions`. diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs index aba2d5209a071b1d8aa50a7feea2965d9cafb948..e1b101e5ae72a260ec90c01a63315b2b1c3f2c11 100644 --- a/crates/settings/src/settings_content/terminal.rs +++ b/crates/settings/src/settings_content/terminal.rs @@ -134,7 +134,19 @@ pub struct TerminalSettingsContent { } /// Shell configuration to open the terminal with. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] +#[derive( + Clone, + Debug, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + JsonSchema, + MergeFrom, + strum::EnumDiscriminants, +)] +#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] #[serde(rename_all = "snake_case")] pub enum Shell { /// Use the system's default terminal configuration in /etc/passwd diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index c07f75a9e3b96bea689f5d6dda22d0742800d321..586f5685a8962aef1f6a13c29c19f0bcf3e557b4 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -853,7 +853,7 @@ impl VsCodeSettings { fn worktree_settings_content(&self) -> WorktreeSettingsContent { WorktreeSettingsContent { - project_name: None, + project_name: crate::Maybe::Unset, file_scan_exclusions: self .read_value("files.watcherExclude") .and_then(|v| v.as_array()) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 19a495cf770cddd4dd3252e23ef5be8a6c1308eb..7ced4000349612b7a9629e33a6934ce801645b86 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -9,6 +9,16 @@ use crate::{ SettingsPageItem, SubPageLink, USER, all_language_names, sub_page_stack, }; +const DEFAULT_STRING: String = String::new(); +/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe` +/// to avoid the "NO DEFAULT" case. +const DEFAULT_EMPTY_STRING: Option<&String> = Some(&DEFAULT_STRING); + +const DEFAULT_SHARED_STRING: SharedString = SharedString::new_static(""); +/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe` +/// to avoid the "NO DEFAULT" case. +const DEFAULT_EMPTY_SHARED_STRING: Option<&SharedString> = Some(&DEFAULT_SHARED_STRING); + pub(crate) fn settings_data(cx: &App) -> Vec { vec![ SettingsPage { @@ -16,16 +26,20 @@ pub(crate) fn settings_data(cx: &App) -> Vec { items: vec![ SettingsPageItem::SectionHeader("General Settings"), SettingsPageItem::SettingItem(SettingItem { - title: "Confirm Quit", - description: "Confirm before quitting Zed", - field: Box::new(SettingField { - pick: |settings_content| settings_content.workspace.confirm_quit.as_ref(), - write: |settings_content, value| { - settings_content.workspace.confirm_quit = value; - }, - }), - metadata: None, - files: USER, + files: LOCAL, + title: "Project Name", + description: "The Displayed Name Of This Project. If Left Empty, The Root Directory Name Will Be Displayed", + field: Box::new( + SettingField { + pick: |settings_content| { + settings_content.project.worktree.project_name.as_ref()?.as_ref().or(DEFAULT_EMPTY_STRING) + }, + write: |settings_content, value| { + settings_content.project.worktree.project_name = settings::Maybe::Set(value.filter(|name| !name.is_empty())); + }, + } + ), + metadata: Some(Box::new(SettingsFieldMetadata { placeholder: Some("Project Name"), ..Default::default() })), }), SettingsPageItem::SettingItem(SettingItem { title: "When Closing With No Tabs", @@ -4205,26 +4219,183 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Terminal", items: vec![ SettingsPageItem::SectionHeader("Environment"), - SettingsPageItem::SettingItem(SettingItem { - title: "Shell", - description: "What shell to use when opening a terminal", - field: Box::new( - SettingField { + SettingsPageItem::DynamicItem(DynamicItem { + discriminant: SettingItem { + files: USER | LOCAL, + title: "Shell", + description: "What shell to use when opening a terminal", + field: Box::new(SettingField { pick: |settings_content| { - settings_content.terminal.as_ref()?.project.shell.as_ref() + Some(&dynamic_variants::()[ + settings_content + .terminal + .as_ref()? + .project + .shell + .as_ref()? + .discriminant() as usize]) }, write: |settings_content, value| { - settings_content + let Some(value) = value else { + return; + }; + let settings_value = settings_content .terminal .get_or_insert_default() .project - .shell = value; + .shell + .get_or_insert_with(|| settings::Shell::default()); + *settings_value = match value { + settings::ShellDiscriminants::System => { + settings::Shell::System + }, + settings::ShellDiscriminants::Program => { + let program = match settings_value { + settings::Shell::Program(p) => p.clone(), + settings::Shell::WithArguments { program, .. } => program.clone(), + _ => String::from("sh"), + }; + settings::Shell::Program(program) + }, + settings::ShellDiscriminants::WithArguments => { + let (program, args, title_override) = match settings_value { + settings::Shell::Program(p) => (p.clone(), vec![], None), + settings::Shell::WithArguments { program, args, title_override } => { + (program.clone(), args.clone(), title_override.clone()) + }, + _ => (String::from("sh"), vec![], None), + }; + settings::Shell::WithArguments { + program, + args, + title_override, + } + }, + }; }, + }), + metadata: None, + }, + pick_discriminant: |settings_content| { + Some(settings_content.terminal.as_ref()?.project.shell.as_ref()?.discriminant() as usize) + }, + fields: dynamic_variants::().into_iter().map(|variant| { + match variant { + settings::ShellDiscriminants::System => vec![], + settings::ShellDiscriminants::Program => vec![ + SettingItem { + files: USER | LOCAL, + title: "Program", + description: "The shell program to use", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.terminal.as_ref()?.project.shell.as_ref() { + Some(settings::Shell::Program(program)) => Some(program), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .terminal + .get_or_insert_default() + .project + .shell.as_mut() { + Some(settings::Shell::Program(program)) => *program = value, + _ => return + } + }, + }), + metadata: None, + } + ], + settings::ShellDiscriminants::WithArguments => vec![ + SettingItem { + files: USER | LOCAL, + title: "Program", + description: "The shell program to run", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.terminal.as_ref()?.project.shell.as_ref() { + Some(settings::Shell::WithArguments { program, .. }) => Some(program), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .terminal + .get_or_insert_default() + .project + .shell.as_mut() { + Some(settings::Shell::WithArguments { program, .. }) => *program = value, + _ => return + } + }, + }), + metadata: None, + }, + SettingItem { + files: USER | LOCAL, + title: "Arguments", + description: "The arguments to pass to the shell program", + field: Box::new( + SettingField { + pick: |settings_content| { + match settings_content.terminal.as_ref()?.project.shell.as_ref() { + Some(settings::Shell::WithArguments { args, .. }) => Some(args), + _ => None + } + }, + write: |settings_content, value| { + let Some(value) = value else { + return; + }; + match settings_content + .terminal + .get_or_insert_default() + .project + .shell.as_mut() { + Some(settings::Shell::WithArguments { args, .. }) => *args = value, + _ => return + } + }, + } + .unimplemented(), + ), + metadata: None, + }, + SettingItem { + files: USER | LOCAL, + title: "Title Override", + description: "An optional string to override the title of the terminal tab", + field: Box::new(SettingField { + pick: |settings_content| { + match settings_content.terminal.as_ref()?.project.shell.as_ref() { + Some(settings::Shell::WithArguments { title_override, .. }) => title_override.as_ref().or(DEFAULT_EMPTY_SHARED_STRING), + _ => None + } + }, + write: |settings_content, value| { + match settings_content + .terminal + .get_or_insert_default() + .project + .shell.as_mut() { + Some(settings::Shell::WithArguments { title_override, .. }) => *title_override = value.filter(|s| !s.is_empty()), + _ => return + } + }, + }), + metadata: None, + } + ], } - .unimplemented(), - ), - metadata: None, - files: USER | LOCAL, + }).collect(), }), SettingsPageItem::DynamicItem(DynamicItem { discriminant: SettingItem { diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index d430f719c9476e4216e5fedd42269b13b916f4fe..c9b1b7ac9e7632bb9741cab396ee5f2d6252603c 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -370,6 +370,7 @@ fn init_renderers(cx: &mut App) { }) .add_basic_renderer::(render_toggle_button) .add_basic_renderer::(render_text_field) + .add_basic_renderer::(render_text_field) .add_basic_renderer::(render_toggle_button) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) @@ -444,8 +445,10 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) + .add_basic_renderer::(render_dropdown) // please semicolon stay on next line ; } diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 8e432f8affbbfa9e7eb53ec474970c43ec0e8a94..9eddef8eaf43cecca949ea6f595c75795698ab38 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -50,7 +50,7 @@ impl Settings for WorktreeSettings { .collect(); Self { - project_name: worktree.project_name.filter(|p| !p.is_empty()), + project_name: worktree.project_name.into_inner(), file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions") .log_err() .unwrap_or_default(), From 1c639da8a837f5cac4dda4fbfa07d99a07fd9e7f Mon Sep 17 00:00:00 2001 From: Coenen Benjamin Date: Mon, 20 Oct 2025 22:54:56 +0200 Subject: [PATCH 073/202] file_finder: Include worktree root name in multi-worktrees workspace (#40415) Closes #39865 Release Notes: - Fixed file finder display when searching for files in history if you had several worktrees opened in a workspace. It now displays the worktree root name to avoid confusion if you have several files with same name in different worktrees. --------- Signed-off-by: Benjamin <5719034+bnjjj@users.noreply.github.com> --- crates/file_finder/src/file_finder.rs | 45 ++++++- crates/file_finder/src/file_finder_tests.rs | 141 ++++++++++++++++++++ crates/fuzzy/src/paths.rs | 31 ++++- 3 files changed, 209 insertions(+), 8 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 9e813bcb782c2e2f8e86e1e6a019d890c203b1b5..6ea815d5663f40bb66ca764533a6e79b53c6f712 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -21,7 +21,9 @@ use gpui::{ }; use open_path_prompt::OpenPathPrompt; use picker::{Picker, PickerDelegate}; -use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use project::{ + PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore, +}; use search::ToggleIncludeIgnored; use settings::Settings; use std::{ @@ -538,11 +540,14 @@ impl Matches { fn push_new_matches<'a>( &'a mut self, + worktree_store: Entity, + cx: &'a App, history_items: impl IntoIterator + Clone, currently_opened: Option<&'a FoundPath>, query: Option<&FileSearchQuery>, new_search_matches: impl Iterator, extend_old_matches: bool, + path_style: PathStyle, ) { let Some(query) = query else { // assuming that if there's no query, then there's no search matches. @@ -556,8 +561,25 @@ impl Matches { .extend(history_items.into_iter().map(path_to_entry)); return; }; - - let new_history_matches = matching_history_items(history_items, currently_opened, query); + // If several worktress are open we have to set the worktree root names in path prefix + let several_worktrees = worktree_store.read(cx).worktrees().count() > 1; + let worktree_name_by_id = several_worktrees.then(|| { + worktree_store + .read(cx) + .worktrees() + .map(|worktree| { + let snapshot = worktree.read(cx).snapshot(); + (snapshot.id(), snapshot.root_name().into()) + }) + .collect() + }); + let new_history_matches = matching_history_items( + history_items, + currently_opened, + worktree_name_by_id, + query, + path_style, + ); let new_search_matches: Vec = new_search_matches .filter(|path_match| { !new_history_matches.contains_key(&ProjectPath { @@ -694,7 +716,9 @@ impl Matches { fn matching_history_items<'a>( history_items: impl IntoIterator, currently_opened: Option<&'a FoundPath>, + worktree_name_by_id: Option>>, query: &FileSearchQuery, + path_style: PathStyle, ) -> HashMap { let mut candidates_paths = HashMap::default(); @@ -734,13 +758,18 @@ fn matching_history_items<'a>( let mut matching_history_paths = HashMap::default(); for (worktree, candidates) in history_items_by_worktrees { let max_results = candidates.len() + 1; + let worktree_root_name = worktree_name_by_id + .as_ref() + .and_then(|w| w.get(&worktree).cloned()); matching_history_paths.extend( fuzzy::match_fixed_path_set( candidates, worktree.to_usize(), + worktree_root_name, query.path_query(), false, max_results, + path_style, ) .into_iter() .filter_map(|path_match| { @@ -937,15 +966,18 @@ impl FileFinderDelegate { self.matches.get(self.selected_index).cloned() }; + let path_style = self.project.read(cx).path_style(cx); self.matches.push_new_matches( + self.project.read(cx).worktree_store(), + cx, &self.history_items, self.currently_opened_path.as_ref(), Some(&query), matches.into_iter(), extend_old_matches, + path_style, ); - let path_style = self.project.read(cx).path_style(cx); let query_path = query.raw_query.as_str(); if let Ok(mut query_path) = RelPath::new(Path::new(query_path), path_style) { let available_worktree = self @@ -1365,7 +1397,11 @@ impl PickerDelegate for FileFinderDelegate { separate_history: self.separate_history, ..Matches::default() }; + let path_style = self.project.read(cx).path_style(cx); + self.matches.push_new_matches( + project.worktree_store(), + cx, self.history_items.iter().filter(|history_item| { project .worktree_for_id(history_item.project.worktree_id, cx) @@ -1377,6 +1413,7 @@ impl PickerDelegate for FileFinderDelegate { None, None.into_iter(), false, + path_style, ); self.first_update = false; diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 50cba6ce5fd8c6af0fcbbc10855ff92caa532f22..9670de072a5d7c10c2a82c2e384bd7bc4adcd848 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -2503,6 +2503,147 @@ async fn test_search_results_refreshed_on_adding_and_removing_worktrees( }); } +#[gpui::test] +async fn test_history_items_uniqueness_for_multiple_worktree_open_all_files( + cx: &mut TestAppContext, +) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + path!("/repo1"), + json!({ + "package.json": r#"{"name": "repo1"}"#, + "src": { + "index.js": "// Repo 1 index", + } + }), + ) + .await; + + app_state + .fs + .as_fake() + .insert_tree( + path!("/repo2"), + json!({ + "package.json": r#"{"name": "repo2"}"#, + "src": { + "index.js": "// Repo 2 index", + } + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [path!("/repo1").as_ref(), path!("/repo2").as_ref()], + cx, + ) + .await; + + let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); + let (worktree_id1, worktree_id2) = cx.read(|cx| { + let worktrees = workspace.read(cx).worktrees(cx).collect::>(); + (worktrees[0].read(cx).id(), worktrees[1].read(cx).id()) + }); + + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + ProjectPath { + worktree_id: worktree_id1, + path: rel_path("package.json").into(), + }, + None, + true, + window, + cx, + ) + }) + .await + .unwrap(); + + cx.dispatch_action(workspace::CloseActiveItem { + save_intent: None, + close_pinned: false, + }); + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + ProjectPath { + worktree_id: worktree_id2, + path: rel_path("package.json").into(), + }, + None, + true, + window, + cx, + ) + }) + .await + .unwrap(); + + cx.dispatch_action(workspace::CloseActiveItem { + save_intent: None, + close_pinned: false, + }); + + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("package.json"); + + picker.update(cx, |finder, _| { + let matches = &finder.delegate.matches.matches; + + assert_eq!( + matches.len(), + 2, + "Expected 1 history match + 1 search matches, but got {} matches: {:?}", + matches.len(), + matches + ); + + assert_matches!(matches[0], Match::History { .. }); + + let search_matches = collect_search_matches(finder); + assert_eq!( + search_matches.history.len(), + 2, + "Should have exactly 2 history match" + ); + assert_eq!( + search_matches.search.len(), + 0, + "Should have exactly 0 search match (because we already opened the 2 package.json)" + ); + + if let Match::History { path, panel_match } = &matches[0] { + assert_eq!(path.project.worktree_id, worktree_id2); + assert_eq!(path.project.path.as_ref(), rel_path("package.json")); + let panel_match = panel_match.as_ref().unwrap(); + assert_eq!(panel_match.0.path_prefix, rel_path("repo2").into()); + assert_eq!(panel_match.0.path, rel_path("package.json").into()); + assert_eq!( + panel_match.0.positions, + vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] + ); + } + + if let Match::History { path, panel_match } = &matches[1] { + assert_eq!(path.project.worktree_id, worktree_id1); + assert_eq!(path.project.path.as_ref(), rel_path("package.json")); + let panel_match = panel_match.as_ref().unwrap(); + assert_eq!(panel_match.0.path_prefix, rel_path("repo1").into()); + assert_eq!(panel_match.0.path, rel_path("package.json").into()); + assert_eq!( + panel_match.0.positions, + vec![6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] + ); + } + }); +} + #[gpui::test] async fn test_selected_match_stays_selected_after_matches_refreshed(cx: &mut gpui::TestAppContext) { let app_state = init_test(cx); diff --git a/crates/fuzzy/src/paths.rs b/crates/fuzzy/src/paths.rs index 6fc52361e37750400aa308733865fc6fee435134..b35f0c1ce6cec73995838eb82bf782d00f0129af 100644 --- a/crates/fuzzy/src/paths.rs +++ b/crates/fuzzy/src/paths.rs @@ -88,9 +88,11 @@ impl Ord for PathMatch { pub fn match_fixed_path_set( candidates: Vec, worktree_id: usize, + worktree_root_name: Option>, query: &str, smart_case: bool, max_results: usize, + path_style: PathStyle, ) -> Vec { let lowercase_query = query.to_lowercase().chars().collect::>(); let query = query.chars().collect::>(); @@ -98,10 +100,31 @@ pub fn match_fixed_path_set( let mut matcher = Matcher::new(&query, &lowercase_query, query_char_bag, smart_case, true); - let mut results = Vec::new(); + let mut results = Vec::with_capacity(candidates.len()); + let (path_prefix, path_prefix_chars, lowercase_prefix) = match worktree_root_name { + Some(worktree_root_name) => { + let mut path_prefix_chars = worktree_root_name + .display(path_style) + .chars() + .collect::>(); + path_prefix_chars.extend(path_style.separator().chars()); + let lowercase_pfx = path_prefix_chars + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect::>(); + + (worktree_root_name, path_prefix_chars, lowercase_pfx) + } + None => ( + RelPath::empty().into(), + Default::default(), + Default::default(), + ), + }; + matcher.match_candidates( - &[], - &[], + &path_prefix_chars, + &lowercase_prefix, candidates.into_iter(), &mut results, &AtomicBool::new(false), @@ -111,7 +134,7 @@ pub fn match_fixed_path_set( positions: positions.clone(), is_dir: candidate.is_dir, path: candidate.path.into(), - path_prefix: RelPath::empty().into(), + path_prefix: path_prefix.clone(), distance_to_relative_ancestor: usize::MAX, }, ); From ec0efc9360fb6bc4aee17b0888b8ff467eb3bfd1 Mon Sep 17 00:00:00 2001 From: Alex Miller Date: Mon, 20 Oct 2025 22:55:44 +0200 Subject: [PATCH 074/202] git_ui: Close branch selector as soon as branch is selected (#39725) I noticed that branch picker doesn't close until the checkout operation is completed. While normally it's not an issue, it becomes obvious if there are longer running post checkout hooks. In that case selecting a branch makes it feel like nothing has happened (there's a small indicator in the footer) so it's possible to click it multiple times. Closing the modal before the operation completes leads to the error modal saying `Failed to change branch. entity released. Please try again.` even though the checkout was successful. The new behavior is to close the branch picker as soon as the branch is selected. This also aligns with the existing behavior in `create_branch` where `cx.emit(DismissEvent);` is called without waiting for `repo.update`. And as I mentioned before there an indicator in the footer saying `git switch ` with a spinner thingy. I also added a check in the picker's `open` function where it first checks if there's currently an active job and does not show the picker in that case. If this generally makes sense I can add the tests as well if needed. P.S I checked how it works in VSCode and yes it also closes the branch picker as soon as the branch is selected. The only difference is that they show the loading indicator right next to the branch name (with a new branch) but in our case the current branch and activity indicator are located in different places.
Before https://github.com/user-attachments/assets/adf08967-d908-45fa-b3f6-96f73d321262
After https://github.com/user-attachments/assets/88c7ca41-7b39-42d6-a98b-3ad19da9317c
Release Notes: - The branch picker now closes immediately after a branch is selected, instead of waiting for the branch switch to complete. --- crates/git_ui/src/branch_picker.rs | 41 +++++++++--------------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index b9a8dfea9ea167bf7ee807ee2b459444f4fa4f4d..7a046266ac863f78c1e449c7c1b58f0834834b2c 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -137,13 +137,13 @@ impl BranchList { }) .await; - this.update_in(cx, |this, window, cx| { + let _ = this.update_in(cx, |this, window, cx| { this.picker.update(cx, |picker, cx| { picker.delegate.default_branch = default_branch; picker.delegate.all_branches = Some(all_branches); picker.refresh(window, cx); }) - })?; + }); anyhow::Ok(()) }) @@ -410,37 +410,20 @@ impl PickerDelegate for BranchListDelegate { return; } - cx.spawn_in(window, { - let branch = entry.branch.clone(); - async move |picker, cx| { - let branch_change_task = picker.update(cx, |this, cx| { - let repo = this - .delegate - .repo - .as_ref() - .context("No active repository")? - .clone(); - - let mut cx = cx.to_async(); - - anyhow::Ok(async move { - repo.update(&mut cx, |repo, _| { - repo.change_branch(branch.name().to_string()) - })? - .await? - }) - })??; - - branch_change_task.await?; + let Some(repo) = self.repo.clone() else { + return; + }; - picker.update(cx, |_, cx| { - cx.emit(DismissEvent); + let branch = entry.branch.clone(); + cx.spawn(async move |_, cx| { + repo.update(cx, |repo, _| repo.change_branch(branch.name().to_string()))? + .await??; - anyhow::Ok(()) - }) - } + anyhow::Ok(()) }) .detach_and_prompt_err("Failed to change branch", window, cx, |_, _, _| None); + + cx.emit(DismissEvent); } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { From ed822330301a1895df50efa66ce3a6493c5059b6 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Mon, 20 Oct 2025 23:03:28 +0200 Subject: [PATCH 075/202] gpui: Box `Window` instances (#40733) We very frequently move this in and out of the windows slot map on `update_window_id` calls (and we call this a lot!). This alone showed up as `memmove`s at roughly 1% perf in Instruments when scrolling a buffer which makes sense, `Window` itself is 4kb in size. The fix is simple, just box the `Window` instances, moving a pointer is cheap in comparison. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/app.rs | 10 +++++----- crates/gpui/src/app/test_context.rs | 2 +- crates/gpui/src/window.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a5f53c2dad1019ad5ccd3427696a4ceb964c1e61..d4bd7798187a5b7a358106965d9e41fd85efeffe 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -553,7 +553,7 @@ pub struct App { pub(crate) entities: EntityMap, pub(crate) window_update_stack: Vec, pub(crate) new_entity_observers: SubscriberSet, - pub(crate) windows: SlotMap>, + pub(crate) windows: SlotMap>>, pub(crate) window_handles: FxHashMap, pub(crate) focus_handles: Arc, pub(crate) keymap: Rc>, @@ -964,7 +964,7 @@ impl App { clear.clear(); cx.window_handles.insert(id, window.handle); - cx.windows.get_mut(id).unwrap().replace(window); + cx.windows.get_mut(id).unwrap().replace(Box::new(window)); Ok(handle) } Err(e) => { @@ -1239,7 +1239,7 @@ impl App { .windows .values() .filter_map(|window| { - let window = window.as_ref()?; + let window = window.as_deref()?; window.invalidator.is_dirty().then_some(window.handle) }) .collect::>() @@ -1320,7 +1320,7 @@ impl App { fn apply_refresh_effect(&mut self) { for window in self.windows.values_mut() { - if let Some(window) = window.as_mut() { + if let Some(window) = window.as_deref_mut() { window.refreshing = true; window.invalidator.set_dirty(true); } @@ -2199,7 +2199,7 @@ impl AppContext for App { .windows .get(window.id) .context("window not found")? - .as_ref() + .as_deref() .expect("attempted to read a window that is already on the stack"); let root_view = window.root.clone().unwrap(); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 2e24c3ba195f9fc8f72c43cd3da09778e8b2b3e9..cba6de6e31901a43b7c80cff8460af6e0e6a09cc 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -455,7 +455,7 @@ impl TestAppContext { .windows .get_mut(window.id) .unwrap() - .as_mut() + .as_deref_mut() .unwrap() .platform_window .as_test() diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 5534a4a8ddf98c2bf5d26637b7af3a499dd63ae1..6855874bae25db1b0990541902333e4e72a283b3 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4718,7 +4718,7 @@ impl WindowHandle { .get(self.id) .and_then(|window| { window - .as_ref() + .as_deref() .and_then(|window| window.root.clone()) .map(|root_view| root_view.downcast::()) }) From f7a0971d2b9c366338dabad13fb494fe0a2c83b1 Mon Sep 17 00:00:00 2001 From: kitt <11167504+kitt-cat@users.noreply.github.com> Date: Mon, 20 Oct 2025 14:24:41 -0700 Subject: [PATCH 076/202] Add line endings indicator in status bar (#39609) Closes #5294 This PR adds a line ending indicator to the status bar, hidden by default as discussed in https://github.com/zed-industries/zed/issues/5294. ### Changes - 8b063a22d8700bed9c93989b9e0f6a064b2e86cf add the indicator and `status_bar.line_endings_button` setting. - ~~9926237b709dd4e25ce58d558fd385d63b405f3b changes `status_bar.line_endings_button` from a boolean to an enum:~~
show details - `always` Always show line endings indicator. - `non_native` Indicate when line endings do not match the current platform. - `lf_only` Indicate when using unix-style (LF) line endings only. - `crlf_only` Indicate when using windows-style (CRLF) line endings only. - `never` Do not show line endings indicator. I know this many options might be overdoing it, but I was torn between the pleasant default of `non_native` and the simplicity of `lf_only` / `crlf_only`. My thinking was if one is developing on a project which exclusively uses one line-ending style or the other, it would be nice to be able to configure no-indicator-in-the-happy-case behavior regardless of the platform zed is running on. But I'm not really familiar with any projects that use exclusively CRLF line endings in practice. Is this a scenario worth supporting or just something I dreamed up?
- 01174191e4cf337069e7a31b0f0432ae94c52515 rename the action context for `line ending: Toggle` -> `line ending selector: Toggle`. When running the action in the command palette with the old name I felt surprised to be greeted with an additional menu, with the new name it feels more predictable (plus now it matches `language_selector::Toggle`!) ### Future work Hidden status bar items still get padding, creating inconsistent spacing (and it kind of stands out where I placed the line-endings button): the gap after the indicator is larger than for other buttons I started a new follow-up PR to address that: https://github.com/zed-industries/zed/pull/39992 Release Notes: - Added line ending indicator to the status bar (disabled by default; enabled by setting `status_bar.line_endings_button` to `true`) --- assets/settings/default.json | 4 +- .../src/line_ending_indicator.rs | 70 +++++++++++++++++++ .../src/line_ending_selector.rs | 10 +-- .../src/settings_content/workspace.rs | 4 ++ crates/text/src/text.rs | 7 ++ crates/workspace/src/workspace_settings.rs | 2 + crates/zed/src/zed.rs | 5 +- docs/src/configuring-zed.md | 3 +- docs/src/visual-customization.md | 4 ++ 9 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 crates/line_ending_selector/src/line_ending_indicator.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 327b35c4b197818c09a065e2b6203430a6150665..c110b169b7cd18098de9945b43f72ac1fa0cff19 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1350,7 +1350,9 @@ // Whether to show the active language button in the status bar. "active_language_button": true, // Whether to show the cursor position button in the status bar. - "cursor_position_button": true + "cursor_position_button": true, + // Whether to show active line endings button in the status bar. + "line_endings_button": false }, // Settings specific to the terminal "terminal": { diff --git a/crates/line_ending_selector/src/line_ending_indicator.rs b/crates/line_ending_selector/src/line_ending_indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..042630056a4cad93497e7b35cab7c82c1ea643e3 --- /dev/null +++ b/crates/line_ending_selector/src/line_ending_indicator.rs @@ -0,0 +1,70 @@ +use editor::Editor; +use gpui::{Entity, Subscription, WeakEntity}; +use language::LineEnding; +use ui::{Tooltip, prelude::*}; +use workspace::{StatusBarSettings, StatusItemView, item::ItemHandle, item::Settings}; + +use crate::{LineEndingSelector, Toggle}; + +#[derive(Default)] +pub struct LineEndingIndicator { + line_ending: Option, + active_editor: Option>, + _observe_active_editor: Option, +} + +impl LineEndingIndicator { + fn update(&mut self, editor: Entity, _: &mut Window, cx: &mut Context) { + self.line_ending = None; + self.active_editor = None; + + if let Some((_, buffer, _)) = editor.read(cx).active_excerpt(cx) { + let line_ending = buffer.read(cx).line_ending(); + self.line_ending = Some(line_ending); + self.active_editor = Some(editor.downgrade()); + } + + cx.notify(); + } +} + +impl Render for LineEndingIndicator { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if !StatusBarSettings::get_global(cx).line_endings_button { + return div(); + } + + div().when_some(self.line_ending.as_ref(), |el, line_ending| { + el.child( + Button::new("change-line-ending", line_ending.label()) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, window, cx| { + if let Some(editor) = this.active_editor.as_ref() { + LineEndingSelector::toggle(editor, window, cx); + } + })) + .tooltip(|window, cx| { + Tooltip::for_action("Select Line Ending", &Toggle, window, cx) + }), + ) + }) + } +} + +impl StatusItemView for LineEndingIndicator { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { + self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update)); + self.update(editor, window, cx); + } else { + self.line_ending = None; + self._observe_active_editor = None; + } + cx.notify(); + } +} diff --git a/crates/line_ending_selector/src/line_ending_selector.rs b/crates/line_ending_selector/src/line_ending_selector.rs index 7f75a1ebe3550595c8fa78643ef5446ab2fa3a44..504c327a349c97214e801f6bd375d61c7847f2be 100644 --- a/crates/line_ending_selector/src/line_ending_selector.rs +++ b/crates/line_ending_selector/src/line_ending_selector.rs @@ -1,6 +1,9 @@ +mod line_ending_indicator; + use editor::Editor; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions}; use language::{Buffer, LineEnding}; +pub use line_ending_indicator::LineEndingIndicator; use picker::{Picker, PickerDelegate}; use project::Project; use std::sync::Arc; @@ -9,7 +12,7 @@ use util::ResultExt; use workspace::ModalView; actions!( - line_ending, + line_ending_selector, [ /// Toggles the line ending selector modal. Toggle @@ -172,10 +175,7 @@ impl PickerDelegate for LineEndingSelectorDelegate { _: &mut Context>, ) -> Option { let line_ending = self.matches.get(ix)?; - let label = match line_ending { - LineEnding::Unix => "LF", - LineEnding::Windows => "CRLF", - }; + let label = line_ending.label(); let mut list_item = ListItem::new(ix) .inset(true) diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index 577f8fa4f996b2a808bdc785c56210e766dab2fb..a4e36ef8358487dbd4f3e5696eb52c5da9d28eb9 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -380,6 +380,10 @@ pub struct StatusBarSettingsContent { /// /// Default: true pub cursor_position_button: Option, + /// Whether to show active line endings button in the status bar. + /// + /// Default: false + pub line_endings_button: Option, } #[derive( diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 42d9a48430590302f96a1476bdbc3b876ed8f2f6..d78dfbea7dc0d3ac65005855eaffadee37fda584 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -3267,6 +3267,13 @@ impl LineEnding { } } + pub fn label(&self) -> &'static str { + match self { + LineEnding::Unix => "LF", + LineEnding::Windows => "CRLF", + } + } + pub fn detect(text: &str) -> Self { let mut max_ix = cmp::min(text.len(), 1000); while !text.is_char_boundary(max_ix) { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 6ed2aeb5cb85ae5728f683b3c3d7dfbdf86f0c6a..f061227f2cb264b1be1234364ca1e8de7a462e86 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -126,6 +126,7 @@ pub struct StatusBarSettings { pub show: bool, pub active_language_button: bool, pub cursor_position_button: bool, + pub line_endings_button: bool, } impl Settings for StatusBarSettings { @@ -135,6 +136,7 @@ impl Settings for StatusBarSettings { show: status_bar.show.unwrap(), active_language_button: status_bar.active_language_button.unwrap(), cursor_position_button: status_bar.cursor_position_button.unwrap(), + line_endings_button: status_bar.line_endings_button.unwrap(), } } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a9b28229de20b8d24e481482e1441482e5d36212..1416a74e1697324213c11e1bbc51fd2d8a6bf91b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -417,6 +417,8 @@ pub fn initialize_workspace( let cursor_position = cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); + let line_ending_indicator = + cx.new(|_| line_ending_selector::LineEndingIndicator::default()); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(search_button, window, cx); status_bar.add_left_item(lsp_button, window, cx); @@ -425,6 +427,7 @@ pub fn initialize_workspace( status_bar.add_right_item(edit_prediction_button, window, cx); status_bar.add_right_item(active_buffer_language, window, cx); status_bar.add_right_item(active_toolchain_language, window, cx); + status_bar.add_right_item(line_ending_indicator, window, cx); status_bar.add_right_item(vim_mode_indicator, window, cx); status_bar.add_right_item(cursor_position, window, cx); status_bar.add_right_item(image_info, window, cx); @@ -4669,7 +4672,7 @@ mod tests { "keymap_editor", "keystroke_input", "language_selector", - "line_ending", + "line_ending_selector", "lsp_tool", "markdown", "menu", diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 4fb65d14118d637d7123998e53da9aa40dc7a84c..efc4538c0e5286a053a89916c90548796ba619d0 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1498,7 +1498,8 @@ Positive `integer` value between 1 and 32. Values outside of this range will be ```json [settings] "status_bar": { "active_language_button": true, - "cursor_position_button": true + "cursor_position_button": true, + "line_endings_button": false }, ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 89cb3ec1929e6c71f8f832818ef6c54c0219bff2..a31f4428cd9d554ce366e182da605a71eefe6eec 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -319,6 +319,10 @@ TBD: Centered layout related settings // Clicking the button brings up an input for jumping to a line and column. // Defaults to true. "cursor_position_button": true, + // Show/hide a button that displays the buffer's line-ending mode. + // Clicking the button brings up the line-ending selector. + // Defaults to false. + "line_endings_button": false }, "global_lsp_settings": { // Show/hide the LSP button in the status bar. From 32a442d5229f58204df365da6c33796b03179474 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 21 Oct 2025 00:43:33 +0300 Subject: [PATCH 077/202] Fix inlay hint cleanup on excerpts removal (#40738) A cherry-pick of https://github.com/zed-industries/zed/pull/40183/commits/f5188d55fbcbb3856038967bce8e824ddb42bdba This fixes a hard-to-reproduce crash caused excerpts removal not updating previous snapshot data after corresponding inlay data was removed. Same branch has a test: https://github.com/zed-industries/zed/pull/40183/commits/8783a9eb4fbc60e3fbe0654c2d330bddfaa7ef0a that does not fail on `main` due to different way inlays are queried, it will be merged later. Release Notes: - N/A --- crates/editor/src/display_map.rs | 8 ++++++-- crates/editor/src/editor.rs | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index b3d642f60ea591fcf8b1987c723b4ba025dc110a..aad62e0debd366a968a34e5d7b937b75f6272c0d 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -594,7 +594,11 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } - pub fn remove_inlays_for_excerpts(&mut self, excerpts_removed: &[ExcerptId]) { + pub fn remove_inlays_for_excerpts( + &mut self, + excerpts_removed: &[ExcerptId], + cx: &mut Context, + ) { let to_remove = self .inlay_map .current_inlays() @@ -606,7 +610,7 @@ impl DisplayMap { } }) .collect::>(); - self.inlay_map.splice(&to_remove, Vec::new()); + self.splice_inlays(&to_remove, Vec::new(), cx); } fn tab_size(buffer: &Entity, cx: &App) -> NonZeroU32 { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 20ebff811d544d0261e90c43336e569e9a5700ef..6923e5ca5bcbc02a1e2f857942afe97a650dd5cf 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5298,8 +5298,8 @@ impl Editor { { self.splice_inlays(&to_remove, to_insert, cx); } - self.display_map.update(cx, |display_map, _| { - display_map.remove_inlays_for_excerpts(&excerpts_removed) + self.display_map.update(cx, |display_map, cx| { + display_map.remove_inlays_for_excerpts(&excerpts_removed, cx) }); return; } From 4f5f29926520c6bf268b96720c594bd4375a2512 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:53:47 -0400 Subject: [PATCH 078/202] settings ui: Autoscroll content during keyboard navigation (#40734) Closes #40608 This fixes tabbing in both the settings ui nav bar and page content going off screen instead of scrolling the focused element into a visible view. The bug occurred because `gpui::list` and `gpui::uniform_list` only render visible elements, preventing non visible elements in a view from having their focus handle added to the element tree. Thus making the tab stop map skip over those elements because they weren't present. The fix for this is scrolling to reveal non visible elements and then focus the selected element on the next frame. Release Notes: - settings ui: Auto scroll to reveal items in navigation bar and window when tabbing --------- Co-authored-by: Ben Kunkle --- assets/keymaps/default-linux.json | 2 + assets/keymaps/default-macos.json | 2 + assets/keymaps/default-windows.json | 2 + crates/gpui/src/window.rs | 10 +- crates/settings_ui/src/settings_ui.rs | 164 +++++++++++++++++++++----- 5 files changed, 144 insertions(+), 36 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index d2f70b825b4efa3544e916589846e7944a91771a..fff7469199f88b88bb02fcf2d595d5ee76628315 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1280,7 +1280,9 @@ "use_key_equivalents": true, "bindings": { "up": "settings_editor::FocusPreviousNavEntry", + "shift-tab": "settings_editor::FocusPreviousNavEntry", "down": "settings_editor::FocusNextNavEntry", + "tab": "settings_editor::FocusNextNavEntry", "right": "settings_editor::ExpandNavEntry", "left": "settings_editor::CollapseNavEntry", "pageup": "settings_editor::FocusPreviousRootNavEntry", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 75dd84f7ae269f0883809c8ec1b2516b25f024c9..0b4119e95e4bf33d1f19a538fa231cc13ff79419 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1386,7 +1386,9 @@ "use_key_equivalents": true, "bindings": { "up": "settings_editor::FocusPreviousNavEntry", + "shift-tab": "settings_editor::FocusPreviousNavEntry", "down": "settings_editor::FocusNextNavEntry", + "tab": "settings_editor::FocusNextNavEntry", "right": "settings_editor::ExpandNavEntry", "left": "settings_editor::CollapseNavEntry", "pageup": "settings_editor::FocusPreviousRootNavEntry", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index e38bca90e4394ec415df253a7d9668cadefceac1..39c1b672a4105e9565bbdaded7229402831c702d 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1309,7 +1309,9 @@ "use_key_equivalents": true, "bindings": { "up": "settings_editor::FocusPreviousNavEntry", + "shift-tab": "settings_editor::FocusPreviousNavEntry", "down": "settings_editor::FocusNextNavEntry", + "tab": "settings_editor::FocusNextNavEntry", "right": "settings_editor::ExpandNavEntry", "left": "settings_editor::CollapseNavEntry", "pageup": "settings_editor::FocusPreviousRootNavEntry", diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 6855874bae25db1b0990541902333e4e72a283b3..6d74a0e11f7a7ecde003f48b084f4720bd03230e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4313,14 +4313,14 @@ impl Window { } /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. - pub fn handler_for) + 'static>( + pub fn handler_for) + 'static>( &self, - view: &Entity, + entity: &Entity, f: Callback, - ) -> impl Fn(&mut Window, &mut App) + use { - let view = view.downgrade(); + ) -> impl Fn(&mut Window, &mut App) + 'static { + let entity = entity.downgrade(); move |window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, window, cx)).ok(); + entity.update(cx, |entity, cx| f(entity, window, cx)).ok(); } } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index c9b1b7ac9e7632bb9741cab396ee5f2d6252603c..ef3049ea20953c032714f4855a1b3da8a60a5434 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1052,7 +1052,7 @@ impl SettingsWindow { }; // high overdraw value so the list scrollbar len doesn't change too much - let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(100.0)).measure_all(); + let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(0.0)).measure_all(); list_state.set_scroll_handler(|_, _, _| {}); let mut this = Self { @@ -1184,7 +1184,7 @@ impl SettingsWindow { move |this: &mut SettingsWindow, window: &mut Window, cx: &mut Context| { - this.open_and_scroll_to_navbar_entry(entry_index, window, cx, false); + this.open_and_scroll_to_navbar_entry(entry_index, None, false, window, cx); }, ); focus_subscriptions.push(subscription); @@ -1622,7 +1622,7 @@ impl SettingsWindow { .visible_navbar_entries() .any(|(index, _)| index == self.navbar_entry) { - self.open_and_scroll_to_navbar_entry(self.navbar_entry, window, cx, true); + self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx); } else { self.open_first_nav_page(); }; @@ -1921,7 +1921,13 @@ impl SettingsWindow { let Some(next_entry_index) = next_index else { return; }; - this.open_and_scroll_to_navbar_entry(next_entry_index, window, cx, false); + this.open_and_scroll_to_navbar_entry( + next_entry_index, + Some(gpui::ScrollStrategy::Bottom), + false, + window, + cx, + ); })) .on_action(cx.listener(|this, _: &FocusPreviousNavEntry, window, cx| { let entry_index = this @@ -1937,7 +1943,13 @@ impl SettingsWindow { let Some(prev_entry_index) = prev_index else { return; }; - this.open_and_scroll_to_navbar_entry(prev_entry_index, window, cx, false); + this.open_and_scroll_to_navbar_entry( + prev_entry_index, + Some(gpui::ScrollStrategy::Top), + false, + window, + cx, + ); })) .border_color(cx.theme().colors().border) .bg(cx.theme().colors().panel_background) @@ -1957,21 +1969,22 @@ impl SettingsWindow { this.visible_navbar_entries() .skip(range.start.saturating_sub(1)) .take(range.len()) - .map(|(ix, entry)| { + .map(|(entry_index, entry)| { TreeViewItem::new( - ("settings-ui-navbar-entry", ix), + ("settings-ui-navbar-entry", entry_index), entry.title, ) .track_focus(&entry.focus_handle) .root_item(entry.is_root) - .toggle_state(this.is_navbar_entry_selected(ix)) + .toggle_state(this.is_navbar_entry_selected(entry_index)) .when(entry.is_root, |item| { item.expanded(entry.expanded || this.has_query) .on_toggle(cx.listener( move |this, _, window, cx| { - this.toggle_navbar_entry(ix); + this.toggle_navbar_entry(entry_index); window.focus( - &this.navbar_entries[ix].focus_handle, + &this.navbar_entries[entry_index] + .focus_handle, ); cx.notify(); }, @@ -1980,7 +1993,11 @@ impl SettingsWindow { .on_click( cx.listener(move |this, _, window, cx| { this.open_and_scroll_to_navbar_entry( - ix, window, cx, true, + entry_index, + None, + true, + window, + cx, ); }), ) @@ -2017,13 +2034,16 @@ impl SettingsWindow { fn open_and_scroll_to_navbar_entry( &mut self, navbar_entry_index: usize, + scroll_strategy: Option, + focus_content: bool, window: &mut Window, cx: &mut Context, - focus_content: bool, ) { self.open_navbar_entry_page(navbar_entry_index); cx.notify(); + let mut handle_to_focus = None; + if self.navbar_entries[navbar_entry_index].is_root || !self.is_nav_entry_visible(navbar_entry_index) { @@ -2035,9 +2055,17 @@ impl SettingsWindow { else { return; }; - self.focus_content_element(first_item_index, window, cx); + handle_to_focus = Some(self.focus_handle_for_content_element(first_item_index, cx)); + } else if !self.is_nav_entry_visible(navbar_entry_index) { + let Some(first_visible_nav_entry_index) = + self.visible_navbar_entries().next().map(|(index, _)| index) + else { + return; + }; + self.focus_and_scroll_to_nav_entry(first_visible_nav_entry_index, window, cx); } else { - window.focus(&self.navbar_entries[navbar_entry_index].focus_handle); + handle_to_focus = + Some(self.navbar_entries[navbar_entry_index].focus_handle.clone()); } } else { let entry_item_index = self.navbar_entries[navbar_entry_index] @@ -2055,18 +2083,33 @@ impl SettingsWindow { offset_in_item: px(0.), }); if focus_content { - self.focus_content_element(entry_item_index, window, cx); + handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx)); } else { - window.focus(&self.navbar_entries[navbar_entry_index].focus_handle); + handle_to_focus = + Some(self.navbar_entries[navbar_entry_index].focus_handle.clone()); } } + if let Some(scroll_strategy) = scroll_strategy + && let Some(logical_entry_index) = self + .visible_navbar_entries() + .into_iter() + .position(|(index, _)| index == navbar_entry_index) + { + self.navbar_scroll_handle + .scroll_to_item(logical_entry_index + 1, scroll_strategy); + } + // Page scroll handle updates the active item index // in it's next paint call after using scroll_handle.scroll_to_top_of_item // The call after that updates the offset of the scroll handle. So to // ensure the scroll handle doesn't lag behind we need to render three frames // back to back. - cx.on_next_frame(window, |_, window, cx| { + cx.on_next_frame(window, move |_, window, cx| { + if let Some(handle) = handle_to_focus.as_ref() { + window.focus(handle); + } + cx.on_next_frame(window, |_, _, cx| { cx.notify(); }); @@ -2133,7 +2176,7 @@ impl SettingsWindow { fn render_page_items( &mut self, - page_index: Option, + page_index: usize, _window: &mut Window, cx: &mut Context, ) -> impl IntoElement { @@ -2197,16 +2240,14 @@ impl SettingsWindow { .unwrap_or(false); let is_last = Some(actual_item_index) == last_non_header_index; + let item_focus_handle = + this.content_handles[page_index][actual_item_index].focus_handle(cx); + v_flex() .id(("settings-page-item", actual_item_index)) .w_full() .min_w_0() - .when_some(page_index, |element, page_index| { - element.track_focus( - &this.content_handles[page_index][actual_item_index] - .focus_handle(cx), - ) - }) + .track_focus(&item_focus_handle) .child(item.render( this, actual_item_index, @@ -2327,7 +2368,7 @@ impl SettingsWindow { page_header = self.render_files_header(window, cx).into_any_element(); page_content = self - .render_page_items(Some(self.current_page_index()), window, cx) + .render_page_items(self.current_page_index(), window, cx) .into_any_element(); } else { page_header = h_flex() @@ -2356,6 +2397,65 @@ impl SettingsWindow { .pb_8() .px_8() .bg(cx.theme().colors().editor_background) + .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { + if !sub_page_stack().is_empty() { + window.focus_next(); + return; + } + for (logical_index, (actual_index, _)) in this.visible_page_items().enumerate() { + let handle = this.content_handles[this.current_page_index()][actual_index] + .focus_handle(cx); + let mut offset = 1; // for page header + + if let Some((_, next_item)) = this.visible_page_items().nth(logical_index + 1) + && matches!(next_item, SettingsPageItem::SectionHeader(_)) + { + offset += 1; + } + if handle.contains_focused(window, cx) { + let next_logical_index = logical_index + offset + 1; + this.list_state.scroll_to_reveal_item(next_logical_index); + // We need to render the next item to ensure it's focus handle is in the element tree + cx.on_next_frame(window, |_, window, cx| { + window.focus_next(); + cx.notify(); + }); + cx.notify(); + return; + } + } + window.focus_next(); + })) + .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| { + if !sub_page_stack().is_empty() { + window.focus_prev(); + return; + } + let mut prev_was_header = false; + for (logical_index, (actual_index, item)) in this.visible_page_items().enumerate() { + let is_header = matches!(item, SettingsPageItem::SectionHeader(_)); + let handle = this.content_handles[this.current_page_index()][actual_index] + .focus_handle(cx); + let mut offset = 1; // for page header + + if prev_was_header { + offset -= 1; + } + if handle.contains_focused(window, cx) { + let next_logical_index = logical_index + offset - 1; + this.list_state.scroll_to_reveal_item(next_logical_index); + // We need to render the next item to ensure it's focus handle is in the element tree + cx.on_next_frame(window, |_, window, cx| { + window.focus_prev(); + cx.notify(); + }); + cx.notify(); + return; + } + prev_was_header = is_header; + } + window.focus_prev(); + })) .child(page_header) .when(sub_page_stack().is_empty(), |this| { this.vertical_scrollbar_for(self.list_state.clone(), window, cx) @@ -2543,12 +2643,13 @@ impl SettingsWindow { 0 } - fn focus_content_element(&self, item_index: usize, window: &mut Window, cx: &mut App) { - if !sub_page_stack().is_empty() { - return; - } + fn focus_handle_for_content_element( + &self, + actual_item_index: usize, + cx: &Context, + ) -> FocusHandle { let page_index = self.current_page_index(); - window.focus(&self.content_handles[page_index][item_index].focus_handle(cx)); + self.content_handles[page_index][actual_item_index].focus_handle(cx) } fn focused_nav_entry(&self, window: &Window, cx: &App) -> Option { @@ -2609,9 +2710,10 @@ impl Render for SettingsWindow { { this.open_and_scroll_to_navbar_entry( this.navbar_entry, + None, + true, window, cx, - true, ); } else { this.focus_and_scroll_to_nav_entry(this.navbar_entry, window, cx); From de6750d3f4674da3f82f3d15b5c2029cfe4bbee6 Mon Sep 17 00:00:00 2001 From: Jose Garcia <47431411+ruxwez@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:31:04 +0200 Subject: [PATCH 079/202] Add `line_endings_button` in VSCode settings to fix semantic merge conflict (#40745) I have partially solved a problem caused by a structure in commit: f7a0971d2b9c366338dabad13fb494fe0a2c83b1 Release Notes: - N/A Before: image After: image --- crates/settings/src/vscode_import.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 586f5685a8962aef1f6a13c29c19f0bcf3e557b4..fd9b343ad9cf6b0fd93ac31bf2dd2e1f2f6023bf 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -635,6 +635,7 @@ impl VsCodeSettings { show: self.read_bool("workbench.statusBar.visible"), active_language_button: None, cursor_position_button: None, + line_endings_button: None, }) } From 684f4dced95804334d233f51d0b601850d493df7 Mon Sep 17 00:00:00 2001 From: ofetch <164693572+ofetch@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:04:00 +0100 Subject: [PATCH 080/202] settings_ui: Fix typo (#40743) Fixes #40742 Release Notes: - N/A *or* Added/Fixed/Improved ... Co-authored-by: Danilo Leal --- crates/settings_ui/src/page_data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 7ced4000349612b7a9629e33a6934ce801645b86..7bcddfbfbf2d3638140c0ad35dcc9e1130e42d31 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -4903,7 +4903,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { items: vec![ SettingsPageItem::SectionHeader("Git Gutter"), SettingsPageItem::SettingItem(SettingItem { - title: "Visibilility", + title: "Visibility", description: "Control whether git status is shown in the editor's gutter", field: Box::new(SettingField { pick: |settings_content| settings_content.git.as_ref()?.git_gutter.as_ref(), From b479d1ef49120c5b7d8a319d918ac18c398d1d3b Mon Sep 17 00:00:00 2001 From: Akira Sousa Date: Mon, 20 Oct 2025 21:00:56 -0300 Subject: [PATCH 081/202] title_bar: Add configurable window controls position (#38834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🎯 Description Adds configurable window control buttons (minimize, maximize, close) positioning for Linux, allowing users to choose between macOS-style (left side) or Windows-style (right side) placement. ## ✨ Features - New `title_bar.window_controls_position` setting with `"left"` and `"right"` options - Left positioning: macOS style (Close → Minimize → Maximize) - Right positioning: Windows style (Minimize → Maximize → Close) - Fixed transparent background issues for window controls - Maintains consistent styling with Zed's theme ## 🔧 Technical Changes ### Settings System - Added `WindowControlsPosition` enum in `settings_content.rs` - Extended `TitleBarSettingsContent` with `window_controls_position` field - Updated `TitleBarSettings` to include the new configuration ### Title Bar Layout - Modified `platform_title_bar.rs` to use setting for layout positioning - Added conditional logic for `justify_start()` vs `justify_between()` based on position - Fixed transparent container background by adding `bg(titlebar_color)` ### Window Controls - Updated `platform_linux.rs` to reorder buttons based on position setting - Changed button background from `ghost_element_background` to `title_bar_background` - Implemented proper button sequencing for both positions ## 🧪 How to Test 1. Add to your Zed settings: ```json { "title_bar": { "window_controls_position": "left" } } ``` or ```json { "title_bar": { "window_controls_position": "right" } } ``` 2. Restart Zed 3. Verify buttons are positioned correctly 4. Check that background is not transparent 5. Test button functionality (minimize, maximize, close) ## �� Expected Behavior - **Left position**: Buttons appear on the left side of the title bar in Close → Minimize → Maximize order - **Right position**: Buttons appear on the right side of the title bar in Minimize → Maximize → Close order - **Background**: Solid background matching Zed's theme (no transparency) ## 🔍 Files Changed - `crates/settings/src/settings_content.rs` - Added enum and setting - `crates/title_bar/src/title_bar_settings.rs` - Updated settings struct - `crates/title_bar/src/platform_title_bar.rs` - Modified layout logic - `crates/title_bar/src/platforms/platform_linux.rs` - Updated button ordering and styling ## 🎨 Design Rationale This feature provides Linux users with the flexibility to choose their preferred window control button layout, improving the user experience by allowing them to match their desktop environment's conventions or personal preferences. ## ✅ Checklist - [x] Code compiles without errors - [x] Settings are properly serialized/deserialized - [x] Background transparency issues resolved - [x] Button ordering works correctly for both positions - [x] Layout adapts properly based on configuration - [x] No breaking changes to existing functionality ## 🔗 Related This addresses the need for customizable window control positioning on Linux, providing consistency with user expectations from different desktop environments. ![demo2](https://github.com/user-attachments/assets/7333db34-d54e-427c-ac52-140925363f91) --- crates/settings/src/settings_content.rs | 30 +++++ crates/title_bar/src/platform_title_bar.rs | 103 +++++++++++++----- .../title_bar/src/platforms/platform_linux.rs | 78 +++++++++---- crates/title_bar/src/title_bar_settings.rs | 4 +- 4 files changed, 162 insertions(+), 53 deletions(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 94bde9e4e403a090a32e145e532114e7a3b65681..9eec9ac3d56b2ac2fa7d435d658de3f1c2123a1a 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -260,6 +260,32 @@ impl strum::VariantNames for BaseKeymapContent { ]; } +/// Position of window control buttons on Linux. +/// +/// Valid values: "left" (macOS style) or "right" (Windows/Linux style) +#[derive( + Copy, + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + MergeFrom, + PartialEq, + Eq, + Default, + strum::VariantArray, + strum::VariantNames, +)] +#[serde(rename_all = "snake_case")] +pub enum WindowControlsPosition { + /// Window controls on the left side (macOS style) + Left, + /// Window controls on the right side (Windows style) + #[default] + Right, +} + #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct TitleBarSettingsContent { @@ -291,6 +317,10 @@ pub struct TitleBarSettingsContent { /// /// Default: false pub show_menus: Option, + /// Position of window control buttons (minimize, maximize, close) on Linux. + /// + /// Default: right + pub window_controls_position: Option, } /// Configuration of audio in Zed. diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index fd03e764629454411c9726ef7dcf055d54582d7e..f078777c2c05bb10afbcd400736c6a428473eeed 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -2,6 +2,7 @@ use gpui::{ AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, }; +use settings::{Settings, WindowControlsPosition}; use smallvec::SmallVec; use std::mem; use ui::prelude::*; @@ -9,6 +10,7 @@ use ui::prelude::*; use crate::{ platforms::{platform_linux, platform_mac, platform_windows}, system_window_tabs::SystemWindowTabs, + title_bar_settings::TitleBarSettings, }; pub struct PlatformTitleBar { @@ -134,35 +136,78 @@ impl Render for PlatformTitleBar { PlatformStyle::Mac => title_bar, PlatformStyle::Linux => { if matches!(decorations, Decorations::Client { .. }) { - title_bar - .child(platform_linux::LinuxWindowControls::new(close_action)) - .when(supported_controls.window_menu, |titlebar| { - titlebar - .on_mouse_down(MouseButton::Right, move |ev, window, _| { - window.show_window_menu(ev.position) - }) - }) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.should_move { - this.should_move = false; - window.start_window_move(); - } - })) - .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - })) - .on_mouse_up( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = true; - }), - ) + let title_bar_settings = TitleBarSettings::get(None, cx); + match title_bar_settings.window_controls_position { + WindowControlsPosition::Left => h_flex() + .w_full() + .bg(titlebar_color) + .child(platform_linux::LinuxWindowControls::new(close_action)) + .child(title_bar) + .when(supported_controls.window_menu, |titlebar| { + titlebar.on_mouse_down( + MouseButton::Right, + move |ev, window, _| { + window.show_window_menu(ev.position) + }, + ) + }) + .on_mouse_move(cx.listener(move |this, _ev, window, _| { + if this.should_move { + this.should_move = false; + window.start_window_move(); + } + })) + .on_mouse_down_out(cx.listener( + move |this, _ev, _window, _cx| { + this.should_move = false; + }, + )) + .on_mouse_up( + MouseButton::Left, + cx.listener(move |this, _ev, _window, _cx| { + this.should_move = false; + }), + ) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _ev, _window, _cx| { + this.should_move = true; + }), + ), + WindowControlsPosition::Right => title_bar + .child(platform_linux::LinuxWindowControls::new(close_action)) + .when(supported_controls.window_menu, |titlebar| { + titlebar.on_mouse_down( + MouseButton::Right, + move |ev, window, _| { + window.show_window_menu(ev.position) + }, + ) + }) + .on_mouse_move(cx.listener(move |this, _ev, window, _| { + if this.should_move { + this.should_move = false; + window.start_window_move(); + } + })) + .on_mouse_down_out(cx.listener( + move |this, _ev, _window, _cx| { + this.should_move = false; + }, + )) + .on_mouse_up( + MouseButton::Left, + cx.listener(move |this, _ev, _window, _cx| { + this.should_move = false; + }), + ) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _ev, _window, _cx| { + this.should_move = true; + }), + ), + } } else { title_bar } diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs index 0e7af80f80e8dcbea03a3b3375f1e4dfd7ca2f37..306d689a7c57f618cdc318c73d4f6bc962dc5a0f 100644 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ b/crates/title_bar/src/platforms/platform_linux.rs @@ -1,4 +1,6 @@ +use crate::title_bar_settings::TitleBarSettings; use gpui::{Action, Hsla, MouseButton, prelude::*, svg}; +use settings::{Settings, WindowControlsPosition}; use ui::prelude::*; #[derive(IntoElement)] @@ -14,33 +16,62 @@ impl LinuxWindowControls { } } +impl LinuxWindowControls { + /// Builds the window controls based on the position setting. + fn build_controls( + position: WindowControlsPosition, + window: &Window, + close_action: Box, + cx: &mut App, + ) -> Vec { + let maximize_type = if window.is_maximized() { + WindowControlType::Restore + } else { + WindowControlType::Maximize + }; + + match position { + WindowControlsPosition::Left => { + // Left side: Close, Minimize, Maximize (left to right) + vec![ + WindowControl::new_close("close", WindowControlType::Close, close_action, cx), + WindowControl::new("minimize", WindowControlType::Minimize, cx), + WindowControl::new("maximize-or-restore", maximize_type, cx), + ] + } + WindowControlsPosition::Right => { + // Right side: Minimize, Maximize, Close (left to right) + vec![ + WindowControl::new("minimize", WindowControlType::Minimize, cx), + WindowControl::new("maximize-or-restore", maximize_type, cx), + WindowControl::new_close("close", WindowControlType::Close, close_action, cx), + ] + } + } + } +} + impl RenderOnce for LinuxWindowControls { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() + let title_bar_settings = TitleBarSettings::get(None, cx); + let controls = Self::build_controls( + title_bar_settings.window_controls_position, + window, + self.close_window_action, + cx, + ); + + let mut element = h_flex() .id("generic-window-controls") .px_3() .gap_3() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .child(WindowControl::new( - "minimize", - WindowControlType::Minimize, - cx, - )) - .child(WindowControl::new( - "maximize-or-restore", - if window.is_maximized() { - WindowControlType::Restore - } else { - WindowControlType::Maximize - }, - cx, - )) - .child(WindowControl::new_close( - "close", - WindowControlType::Close, - self.close_window_action, - cx, - )) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()); + + for control in controls { + element = element.child(control); + } + + element } } @@ -80,7 +111,7 @@ impl WindowControlStyle { let colors = cx.theme().colors(); Self { - background: colors.ghost_element_background, + background: colors.title_bar_background, background_hover: colors.ghost_element_hover, icon: colors.icon, icon_hover: colors.icon_muted, @@ -185,6 +216,7 @@ impl RenderOnce for WindowControl { .rounded_2xl() .w_5() .h_5() + .bg(self.style.background) .hover(|this| this.bg(self.style.background_hover)) .active(|this| this.bg(self.style.background_hover)) .child(icon) diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index bc9b1acbaa06cf60396e61ff68470c8a544e3f5d..a02f80fbd6e5a6f8a54d0599ccb8e04369a6b76f 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,4 +1,4 @@ -use settings::{Settings, SettingsContent}; +use settings::{Settings, SettingsContent, WindowControlsPosition}; #[derive(Copy, Clone, Debug)] pub struct TitleBarSettings { @@ -9,6 +9,7 @@ pub struct TitleBarSettings { pub show_project_items: bool, pub show_sign_in: bool, pub show_menus: bool, + pub window_controls_position: WindowControlsPosition, } impl Settings for TitleBarSettings { @@ -22,6 +23,7 @@ impl Settings for TitleBarSettings { show_project_items: content.show_project_items.unwrap(), show_sign_in: content.show_sign_in.unwrap(), show_menus: content.show_menus.unwrap(), + window_controls_position: content.window_controls_position.unwrap_or_default(), } } } From 62516e8f1fd96850e54817d5984c36033640fcf9 Mon Sep 17 00:00:00 2001 From: Dmitry Nefedov <113844030+dangooddd@users.noreply.github.com> Date: Tue, 21 Oct 2025 03:05:54 +0300 Subject: [PATCH 082/202] themes: Improve Gruvbox scrollbar colors (#38145) Changes that I made: - add "scrollbar.thumb.active_background" to all themes - for dark themes: scrollbar.thumb.background is darker than hover (fg4 from palette for background and fg0 for hover) - for light themes: scrollbar.thumb.background is lighter than hover (fg4 for background and fg0 for hover like in dark theme case) Those changes is consistent with VSCode gruvbox theme and other applications. For active_background I chose orange color, but we can use cyan color to match vscode theme. UPDATE: decided to use blue for active scrollbar as this color is used as accent in other parts of gruvbox themes Release Notes: - Improved scrollbar colors for Gruvbox theme --------- Co-authored-by: Danilo Leal --- assets/themes/gruvbox/gruvbox.json | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/assets/themes/gruvbox/gruvbox.json b/assets/themes/gruvbox/gruvbox.json index 4e6f8334b269e3e5090b0f91d995834906c09083..402d190b34bb3c730e01b9817d815da53cff288d 100644 --- a/assets/themes/gruvbox/gruvbox.json +++ b/assets/themes/gruvbox/gruvbox.json @@ -49,8 +49,9 @@ "panel.background": "#3a3735ff", "panel.focused_border": "#83a598ff", "pane.focused_border": null, - "scrollbar.thumb.background": "#fbf1c74c", - "scrollbar.thumb.hover_background": "#494340ff", + "scrollbar.thumb.active_background": "#83a598ac", + "scrollbar.thumb.hover_background": "#fbf1c74c", + "scrollbar.thumb.background": "#a899844c", "scrollbar.thumb.border": "#494340ff", "scrollbar.track.background": "#00000000", "scrollbar.track.border": "#373432ff", @@ -454,8 +455,9 @@ "panel.background": "#393634ff", "panel.focused_border": "#83a598ff", "pane.focused_border": null, - "scrollbar.thumb.background": "#fbf1c74c", - "scrollbar.thumb.hover_background": "#494340ff", + "scrollbar.thumb.active_background": "#83a598ac", + "scrollbar.thumb.hover_background": "#fbf1c74c", + "scrollbar.thumb.background": "#a899844c", "scrollbar.thumb.border": "#494340ff", "scrollbar.track.background": "#00000000", "scrollbar.track.border": "#343130ff", @@ -859,8 +861,9 @@ "panel.background": "#3b3735ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar.thumb.background": "#fbf1c74c", - "scrollbar.thumb.hover_background": "#494340ff", + "scrollbar.thumb.active_background": "#83a598ac", + "scrollbar.thumb.hover_background": "#fbf1c74c", + "scrollbar.thumb.background": "#a899844c", "scrollbar.thumb.border": "#494340ff", "scrollbar.track.background": "#00000000", "scrollbar.track.border": "#393634ff", @@ -1264,8 +1267,9 @@ "panel.background": "#ecddb4ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar.thumb.background": "#2828284c", - "scrollbar.thumb.hover_background": "#ddcca7ff", + "scrollbar.thumb.active_background": "#458588ac", + "scrollbar.thumb.hover_background": "#2828284c", + "scrollbar.thumb.background": "#7c6f644c", "scrollbar.thumb.border": "#ddcca7ff", "scrollbar.track.background": "#00000000", "scrollbar.track.border": "#eee0b7ff", @@ -1669,8 +1673,9 @@ "panel.background": "#ecddb5ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar.thumb.background": "#2828284c", - "scrollbar.thumb.hover_background": "#ddcca7ff", + "scrollbar.thumb.active_background": "#458588ac", + "scrollbar.thumb.hover_background": "#2828284c", + "scrollbar.thumb.background": "#7c6f644c", "scrollbar.thumb.border": "#ddcca7ff", "scrollbar.track.background": "#00000000", "scrollbar.track.border": "#eee1bbff", @@ -2074,8 +2079,9 @@ "panel.background": "#ecdcb3ff", "panel.focused_border": null, "pane.focused_border": null, - "scrollbar.thumb.background": "#2828284c", - "scrollbar.thumb.hover_background": "#ddcca7ff", + "scrollbar.thumb.active_background": "#458588ac", + "scrollbar.thumb.hover_background": "#2828284c", + "scrollbar.thumb.background": "#7c6f644c", "scrollbar.thumb.border": "#ddcca7ff", "scrollbar.track.background": "#00000000", "scrollbar.track.border": "#eddeb5ff", From 267052f891a8c5f4c5ef9b8b70130983ed05589f Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Mon, 20 Oct 2025 17:08:51 -0700 Subject: [PATCH 083/202] Editor end of input context (#40735) This is needed for #38914 and seems generally useful to have for contextual keybindings. Release Notes: - N/A --------- Co-authored-by: David Kleingeld --- crates/editor/src/editor.rs | 27 +++++++++++++++++++-------- crates/editor/src/editor_tests.rs | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6923e5ca5bcbc02a1e2f857942afe97a650dd5cf..7955aac5e52d3a7fce7299b2d811636a9db2b085 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2466,15 +2466,15 @@ impl Editor { }) } - pub fn key_context(&self, window: &Window, cx: &App) -> KeyContext { + pub fn key_context(&self, window: &mut Window, cx: &mut App) -> KeyContext { self.key_context_internal(self.has_active_edit_prediction(), window, cx) } fn key_context_internal( &self, has_active_edit_prediction: bool, - window: &Window, - cx: &App, + window: &mut Window, + cx: &mut App, ) -> KeyContext { let mut key_context = KeyContext::new_with_defaults(); key_context.add("Editor"); @@ -2551,6 +2551,17 @@ impl Editor { key_context.add("selection_mode"); } + let disjoint = self.selections.disjoint_anchors(); + let snapshot = self.snapshot(window, cx); + let snapshot = snapshot.buffer_snapshot(); + if self.mode == EditorMode::SingleLine + && let [selection] = disjoint + && selection.start == selection.end + && selection.end.to_offset(snapshot) == snapshot.len() + { + key_context.add("end_of_input"); + } + key_context } @@ -2604,8 +2615,8 @@ impl Editor { pub fn accept_edit_prediction_keybind( &self, accept_partial: bool, - window: &Window, - cx: &App, + window: &mut Window, + cx: &mut App, ) -> AcceptEditPredictionBinding { let key_context = self.key_context_internal(true, window, cx); let in_conflict = self.edit_prediction_in_conflict(); @@ -2747,7 +2758,7 @@ impl Editor { self.buffer().read(cx).title(cx) } - pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot { + pub fn snapshot(&self, window: &Window, cx: &mut App) -> EditorSnapshot { let git_blame_gutter_max_author_length = self .render_git_blame_gutter(cx) .then(|| { @@ -9285,7 +9296,7 @@ impl Editor { fn render_edit_prediction_accept_keybind( &self, window: &mut Window, - cx: &App, + cx: &mut App, ) -> Option { let accept_binding = self.accept_edit_prediction_keybind(false, window, cx); let accept_keystroke = accept_binding.keystroke()?; @@ -9331,7 +9342,7 @@ impl Editor { label: impl Into, icon: Option, window: &mut Window, - cx: &App, + cx: &mut App, ) -> Stateful
{ let padding_right = if icon.is_some() { px(4.) } else { px(8.) }; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 239dc2969196d400a84824eabc0ca7e300851a35..3e520f4e2901ff378c64f405d082ad460349ec82 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -26838,3 +26838,24 @@ async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) { cx.assert_editor_state("line1\nline2\nˇ"); } + +#[gpui::test] +async fn test_end_of_editor_context(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state("line1\nline2ˇ"); + cx.update_editor(|e, window, cx| { + e.set_mode(EditorMode::SingleLine); + assert!(e.key_context(window, cx).contains("end_of_input")); + }); + cx.set_state("ˇline1\nline2"); + cx.update_editor(|e, window, cx| { + assert!(!e.key_context(window, cx).contains("end_of_input")); + }); + cx.set_state("line1ˇ\nline2"); + cx.update_editor(|e, window, cx| { + assert!(!e.key_context(window, cx).contains("end_of_input")); + }); +} From a2c42813c45ca474e8d946725e2ff8ce209f1a5c Mon Sep 17 00:00:00 2001 From: Bartosz Kaszubowski Date: Tue, 21 Oct 2025 02:38:47 +0200 Subject: [PATCH 084/202] markdown_preview: Apply few appearance tweaks for tables (#39190) # Why Refs: * https://github.com/zed-industries/zed/pull/39101#issuecomment-3350557981 # How Apply suggested appearance changes in the comment mentioned above. I have also retained the different background for header rows, since it feels to me that it is something that GitHub styling lacks. I have also attempted to shrink the table table element, to fit the content width (so it does not span for the full width of preview), but I have failed on those attempts. Tried to use many various GPUI attributes, but only thing that worked was setting the exact width on table container, also tried to reuse `max_lengths` values, but those are counting characters, not the rendered width. I would like to explore this a bit more, and try to follow up on those changes in a separate PR. Release Notes: - Improved table elements styling in Markdown Preview # Preview Screenshot 2025-09-30 at 12 04 30 Screenshot 2025-09-30 at 12 04 23 Screenshot 2025-09-30 at 12 04 15 Screenshot 2025-09-30 at 12 04 42 Screenshot 2025-09-30 at 12 04 34 --------- Co-authored-by: Danilo Leal --- .../markdown_preview/src/markdown_renderer.rs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index 6f794b1358a1869779b01f6af3069bf8be735e7e..d3768ca99449e820f6c7b457ce93eb886511340f 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -19,10 +19,8 @@ use std::{ }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ - ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems, - StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover, - h_flex, relative, tooltip_container, v_flex, + Clickable, FluentBuilder, LinkPreview, StatefulInteractiveElement, StyledExt, StyledImage, + ToggleState, Tooltip, VisibleOnHover, prelude::*, tooltip_container, }; use workspace::{OpenOptions, OpenVisible, Workspace}; @@ -51,7 +49,8 @@ pub struct RenderContext { buffer_text_style: TextStyle, text_style: TextStyle, border_color: Hsla, - element_background_color: Hsla, + title_bar_background_color: Hsla, + panel_background_color: Hsla, text_color: Hsla, link_color: Hsla, window_rem_size: Pixels, @@ -87,7 +86,8 @@ impl RenderContext { text_style: window.text_style(), syntax_theme: theme.syntax().clone(), border_color: theme.colors().border, - element_background_color: theme.colors().element_background, + title_bar_background_color: theme.colors().title_bar_background, + panel_background_color: theme.colors().panel_background, text_color: theme.colors().text, link_color: theme.colors().text_accent, window_rem_size: window.rem_size(), @@ -511,28 +511,27 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) - &parsed.column_alignments, &max_column_widths, true, + 0, cx, ); let body: Vec = parsed .body .iter() - .map(|row| { + .enumerate() + .map(|(index, row)| { render_markdown_table_row( row, &parsed.column_alignments, &max_column_widths, false, + index, cx, ) }) .collect(); - cx.with_common_p(v_flex()) - .w_full() - .child(header) - .children(body) - .into_any() + div().child(header).children(body).into_any() } fn render_markdown_table_row( @@ -540,6 +539,7 @@ fn render_markdown_table_row( alignments: &Vec, max_column_widths: &Vec, is_header: bool, + row_index: usize, cx: &mut RenderContext, ) -> AnyElement { let mut items = Vec::with_capacity(parsed.children.len()); @@ -574,7 +574,7 @@ fn render_markdown_table_row( } if is_header { - cell = cell.bg(cx.element_background_color) + cell = cell.bg(cx.title_bar_background_color).opacity(0.6) } items.push(cell); @@ -588,6 +588,10 @@ fn render_markdown_table_row( row = row.border_b_1(); } + if row_index % 2 == 1 { + row = row.bg(cx.panel_background_color) + } + row.children(items).into_any_element() } From 36c006828e403bc4ba6a41a6670509e336a5ad08 Mon Sep 17 00:00:00 2001 From: vipex <101529155+vipexv@users.noreply.github.com> Date: Tue, 21 Oct 2025 03:19:40 +0200 Subject: [PATCH 085/202] pane: Ignore max tabs on terminal pane (#40740) Closes #39901 I'm unsure as to which direction the team wants to go with this, but this is the behavior of VSCode which is what this feature is based off so i'm going with this. Changes: 1. Introduced a new argument to the `new` method on the Pane called `ignore_max_tabs` that forces the `max_tabs` to None if it's true. 2. Added a new test `test_bypass_max_tabs_limit`. Release Notes: - Fixed: `max_tabs` Setting affecting the terminal pane. --------- Co-authored-by: Joseph T. Lyons --- crates/debugger_ui/src/session/running.rs | 1 + crates/terminal_view/src/terminal_panel.rs | 51 ++++++++++++++++++++++ crates/workspace/src/pane.rs | 12 ++++- crates/workspace/src/workspace.rs | 2 + 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 8f25ee7fa4cf1210ff72b577765481e1ab109cc0..7340f2591623fcf8b61916fc3aea3337bcad3149 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -386,6 +386,7 @@ pub(crate) fn new_debugger_pane( Default::default(), None, NoAction.boxed_clone(), + true, window, cx, ); diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index cb80d58c13128fad19b647e060001e5cf63f052b..de66bb1ed64851a1101d434e3a7b54a8ae725cfb 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1103,6 +1103,7 @@ pub fn new_terminal_pane( Default::default(), None, NewTerminal.boxed_clone(), + false, window, cx, ); @@ -1752,6 +1753,8 @@ impl Render for InlineAssistTabBarButton { #[cfg(test)] mod tests { + use std::num::NonZero; + use super::*; use gpui::{TestAppContext, UpdateGlobal as _}; use pretty_assertions::assert_eq; @@ -1808,6 +1811,46 @@ mod tests { .unwrap(); } + #[gpui::test] + async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); + + let (window_handle, terminal_panel) = workspace + .update(cx, |workspace, window, cx| { + let window_handle = window.window_handle(); + let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); + (window_handle, terminal_panel) + }) + .unwrap(); + + set_max_tabs(cx, Some(3)); + + for _ in 0..5 { + let task = window_handle + .update(cx, |_, window, cx| { + terminal_panel.update(cx, |panel, cx| { + panel.add_terminal_shell(None, RevealStrategy::Always, window, cx) + }) + }) + .unwrap(); + task.await.unwrap(); + } + + cx.run_until_parked(); + + let item_count = + terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len()); + + assert_eq!( + item_count, 5, + "Terminal panel should bypass max_tabs limit and have all 5 terminals" + ); + } + // A complex Unix command won't be properly parsed by the Windows terminal hence omit the test there. #[cfg(unix)] #[gpui::test] @@ -1922,6 +1965,14 @@ mod tests { .unwrap(); } + fn set_max_tabs(cx: &mut TestAppContext, value: Option) { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings(cx, |settings| { + settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap()) + }); + }); + } + pub fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let store = SettingsStore::test(cx); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 0784f30739be9ef6bf6c65f38e2f7e52c73390e8..3dcba4aa4ebc38bc7c0006c8402e38d4e12ac016 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -376,6 +376,7 @@ pub struct Pane { render_tab_bar: Rc) -> AnyElement>, show_tab_bar_buttons: bool, max_tabs: Option, + use_max_tabs: bool, _subscriptions: Vec, tab_bar_scroll_handle: ScrollHandle, /// This is set to true if a user scroll has occurred more recently than a system scroll @@ -473,10 +474,16 @@ impl Pane { next_timestamp: Arc, can_drop_predicate: Option bool + 'static>>, double_click_dispatch_action: Box, + use_max_tabs: bool, window: &mut Window, cx: &mut Context, ) -> Self { let focus_handle = cx.focus_handle(); + let max_tabs = if use_max_tabs { + WorkspaceSettings::get_global(cx).max_tabs + } else { + None + }; let subscriptions = vec![ cx.on_focus(&focus_handle, window, Pane::focus_in), @@ -498,7 +505,8 @@ impl Pane { zoomed: false, active_item_index: 0, preview_item_id: None, - max_tabs: WorkspaceSettings::get_global(cx).max_tabs, + max_tabs, + use_max_tabs, last_focus_handle_by_item: Default::default(), nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState { mode: NavigationMode::Normal, @@ -706,7 +714,7 @@ impl Pane { self.preview_item_id = None; } - if new_max_tabs != self.max_tabs { + if self.use_max_tabs && new_max_tabs != self.max_tabs { self.max_tabs = new_max_tabs; self.close_items_on_settings_change(window, cx); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 53f416ae805e692db48b6676a3484e6f839feb99..3c8c94ce0e932dc6773c8f6c168c95563b60c879 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1331,6 +1331,7 @@ impl Workspace { pane_history_timestamp.clone(), None, NewFile.boxed_clone(), + true, window, cx, ); @@ -3235,6 +3236,7 @@ impl Workspace { self.pane_history_timestamp.clone(), None, NewFile.boxed_clone(), + true, window, cx, ); From 917f22f884ca28f2496e68200ee71012bd1134ae Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 20 Oct 2025 21:29:19 -0400 Subject: [PATCH 086/202] Don't auto-release preview (#40728) This feels a bit dangerous as long as we have the split releases problem Release Notes: - N/A --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8a587895aaf747f89fb4b93ece8e3b51deb076c..a56f028efc7a94c4b80e10db70d977fafe7c7638 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -847,7 +847,8 @@ jobs: auto-release-preview: name: Auto release preview if: | - startsWith(github.ref, 'refs/tags/v') + false + && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64] runs-on: From 71ea133d72eba0d8ab0216d78ae0ac05fd10c406 Mon Sep 17 00:00:00 2001 From: Willy Hetland <58665964+willeyh-git@users.noreply.github.com> Date: Tue, 21 Oct 2025 06:30:30 +0200 Subject: [PATCH 087/202] Theme-able Vim Mode wrapper (#39813) Closes [#14093](https://github.com/zed-industries/zed/issues/14093) Builds on [#32279](https://github.com/zed-industries/zed/pull/32279) by making it theme dependent. Discussion [#37816](https://github.com/zed-industries/zed/discussions/37816) Wraps the mode label indicator in a div and makes the wrapper and label theme-able. Label weight to medium Mode indicator will render like previously if not theme colors have been set. (i.e., they match zed default- and fallbacks) Really helps with visual confirmation of current mode. _Did not investigate further if there is a way to keep the leading and trailing -- if no theme var given._ Can be applied either by a theme itself or using `theme_overrides` in settings.json Theme colors applied via `theme_overrides` Screenshot 2025-10-08 at 23 01 08 Screenshot 2025-10-08 at 23 01 16 Screenshot 2025-10-08 at 23 01 23 No theme applied Screenshot 2025-10-08 at 23 01 31 Screenshot 2025-10-08 at 23 01 36 Screenshot 2025-10-08 at 23 01 40 https://github.com/user-attachments/assets/d0d71d4d-504f-4d18-bbd9-83d3a4b2adb7 Release Notes: - Vim make mode indicator themeable --------- Co-authored-by: willyHetland Co-authored-by: Conrad Irwin --- crates/settings/src/settings_content/theme.rs | 29 +++++++ crates/theme/src/default_colors.rs | 18 +++++ crates/theme/src/fallback_themes.rs | 10 +++ crates/theme/src/schema.rs | 36 +++++++++ crates/theme/src/styles/colors.rs | 19 +++++ crates/vim/src/mode_indicator.rs | 76 ++++++++++++++++--- 6 files changed, 176 insertions(+), 12 deletions(-) diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index 0228fcbfe832f56c8ad504fd289b273b0a1c0ec7..487ffad34e313f403d97f91af5df205294df61df 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -889,6 +889,35 @@ pub struct ThemeColorsContent { /// Deprecated in favor of `version_control_conflict_marker_theirs`. #[deprecated] pub version_control_conflict_theirs_background: Option, + + /// Background color for Vim Normal mode indicator. + #[serde(rename = "vim.normal.background")] + pub vim_normal_background: Option, + /// Background color for Vim Insert mode indicator. + #[serde(rename = "vim.insert.background")] + pub vim_insert_background: Option, + /// Background color for Vim Replace mode indicator. + #[serde(rename = "vim.replace.background")] + pub vim_replace_background: Option, + /// Background color for Vim Visual mode indicator. + #[serde(rename = "vim.visual.background")] + pub vim_visual_background: Option, + /// Background color for Vim Visual Line mode indicator. + #[serde(rename = "vim.visual_line.background")] + pub vim_visual_line_background: Option, + /// Background color for Vim Visual Block mode indicator. + #[serde(rename = "vim.visual_block.background")] + pub vim_visual_block_background: Option, + /// Background color for Vim Helix Normal mode indicator. + #[serde(rename = "vim.helix_normal.background")] + pub vim_helix_normal_background: Option, + /// Background color for Vim Helix Select mode indicator. + #[serde(rename = "vim.helix_select.background")] + pub vim_helix_select_background: Option, + + /// Text color for Vim mode indicator label. + #[serde(rename = "vim.mode.text")] + pub vim_mode_text: Option, } #[skip_serializing_none] diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 051b7acf102597b6f11581afdd45611b9a4b76e3..80ad845e989b244a5bcd5eb529720d10416ea7bc 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -154,6 +154,15 @@ impl ThemeColors { version_control_ignored: gray().light().step_12(), version_control_conflict_marker_ours: green().light().step_10().alpha(0.5), version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5), + vim_normal_background: system.transparent, + vim_insert_background: system.transparent, + vim_replace_background: system.transparent, + vim_visual_background: system.transparent, + vim_visual_line_background: system.transparent, + vim_visual_block_background: system.transparent, + vim_helix_normal_background: system.transparent, + vim_helix_select_background: system.transparent, + vim_mode_text: system.transparent, } } @@ -280,6 +289,15 @@ impl ThemeColors { version_control_ignored: gray().dark().step_12(), version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5), version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5), + vim_normal_background: system.transparent, + vim_insert_background: system.transparent, + vim_replace_background: system.transparent, + vim_visual_background: system.transparent, + vim_visual_line_background: system.transparent, + vim_visual_block_background: system.transparent, + vim_helix_normal_background: system.transparent, + vim_helix_select_background: system.transparent, + vim_mode_text: system.transparent, } } } diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index 4fb8069bc16d1967dfe10b2e6a577b990d942db7..ae120165f23095266cf92fd33a1cd1ccb88fe309 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -233,6 +233,16 @@ pub(crate) fn zed_default_dark() -> Theme { version_control_ignored: crate::gray().light().step_12(), version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5), version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5), + + vim_normal_background: SystemColors::default().transparent, + vim_insert_background: SystemColors::default().transparent, + vim_replace_background: SystemColors::default().transparent, + vim_visual_background: SystemColors::default().transparent, + vim_visual_line_background: SystemColors::default().transparent, + vim_visual_block_background: SystemColors::default().transparent, + vim_helix_normal_background: SystemColors::default().transparent, + vim_helix_select_background: SystemColors::default().transparent, + vim_mode_text: SystemColors::default().transparent, }, status: StatusColors { conflict: yellow, diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 2d7e1ff9d823eae0d48b375592c6d1f91318f472..c4ed624bf642e0820fd9187224f96e2acfa92018 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -756,6 +756,42 @@ pub fn theme_colors_refinement( .as_ref() .or(this.version_control_conflict_theirs_background.as_ref()) .and_then(|color| try_parse_color(color).ok()), + vim_normal_background: this + .vim_normal_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_insert_background: this + .vim_insert_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_replace_background: this + .vim_replace_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_visual_background: this + .vim_visual_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_visual_line_background: this + .vim_visual_line_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_visual_block_background: this + .vim_visual_block_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_helix_normal_background: this + .vim_helix_normal_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_helix_select_background: this + .vim_helix_select_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + vim_mode_text: this + .vim_mode_text + .as_ref() + .and_then(|color| try_parse_color(color).ok()), } } diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 198ad97adb5d964a1d8f62c5bde99d1d5be5adf7..179d02b91684410bb641893e87759bd30cc73b36 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -162,6 +162,25 @@ pub struct ThemeColors { /// The border color of the minimap thumb. pub minimap_thumb_border: Hsla, + /// Background color for Vim Normal mode indicator. + pub vim_normal_background: Hsla, + /// Background color for Vim Insert mode indicator. + pub vim_insert_background: Hsla, + /// Background color for Vim Replace mode indicator. + pub vim_replace_background: Hsla, + /// Background color for Vim Visual mode indicator. + pub vim_visual_background: Hsla, + /// Background color for Vim Visual Line mode indicator. + pub vim_visual_line_background: Hsla, + /// Background color for Vim Visual Block mode indicator. + pub vim_visual_block_background: Hsla, + /// Background color for Vim Helix Normal mode indicator. + pub vim_helix_normal_background: Hsla, + /// Background color for Vim Helix Select mode indicator. + pub vim_helix_select_background: Hsla, + /// Text color for Vim mode indicator label. + pub vim_mode_text: Hsla, + // === // Editor // === diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index ed182eb74d8e01cc9b8f543cbbc928f6864391e3..42d4915fc509e0f373c8d2c5a2a422b74cc84a8f 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -1,4 +1,4 @@ -use gpui::{Context, Entity, Render, Subscription, WeakEntity, Window, div}; +use gpui::{Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div}; use ui::text_for_keystrokes; use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*}; @@ -93,13 +93,33 @@ impl Render for ModeIndicator { }; let vim_readable = vim.read(cx); - let label = if let Some(label) = vim_readable.status_label.clone() { - label + let status_label = vim_readable.status_label.clone(); + let temp_mode = vim_readable.temp_mode; + let mode = vim_readable.mode; + + let theme = cx.theme(); + let colors = theme.colors(); + let system_transparent = gpui::hsla(0.0, 0.0, 0.0, 0.0); + let vim_mode_text = colors.vim_mode_text; + let bg_color = match mode { + crate::state::Mode::Normal => colors.vim_normal_background, + crate::state::Mode::Insert => colors.vim_insert_background, + crate::state::Mode::Replace => colors.vim_replace_background, + crate::state::Mode::Visual => colors.vim_visual_background, + crate::state::Mode::VisualLine => colors.vim_visual_line_background, + crate::state::Mode::VisualBlock => colors.vim_visual_block_background, + crate::state::Mode::HelixNormal => colors.vim_helix_normal_background, + crate::state::Mode::HelixSelect => colors.vim_helix_select_background, + }; + + let (label, mode): (SharedString, Option) = if let Some(label) = status_label + { + (label, None) } else { - let mode = if vim_readable.temp_mode { - format!("(insert) {}", vim_readable.mode) + let mode_str = if temp_mode { + format!("(insert) {}", mode) } else { - vim_readable.mode.to_string() + mode.to_string() }; let current_operators_description = self.current_operators_description(vim.clone(), cx); @@ -107,13 +127,45 @@ impl Render for ModeIndicator { .pending_keys .as_ref() .unwrap_or(¤t_operators_description); - format!("{} -- {} --", pending, mode).into() + let mode = if bg_color != system_transparent { + mode_str.into() + } else { + format!("-- {} --", mode_str).into() + }; + (pending.into(), Some(mode)) }; - - Label::new(label) - .size(LabelSize::Small) - .line_height_style(LineHeightStyle::UiLabel) - .into_any_element() + h_flex() + .gap_1() + .when(!label.is_empty(), |el| { + el.child( + Label::new(label) + .line_height_style(LineHeightStyle::UiLabel) + .weight(FontWeight::MEDIUM), + ) + }) + .when_some(mode, |el, mode| { + el.child( + v_flex() + .when(bg_color != system_transparent, |el| el.px_2()) + // match with other icons at the bottom that use default buttons + .h(ButtonSize::Default.rems()) + .justify_center() + .rounded_sm() + .bg(bg_color) + .child( + Label::new(mode) + .size(LabelSize::Small) + .line_height_style(LineHeightStyle::UiLabel) + .weight(FontWeight::MEDIUM) + .when( + bg_color != system_transparent + && vim_mode_text != system_transparent, + |el| el.color(Color::Custom(vim_mode_text)), + ), + ), + ) + }) + .into_any() } } From 4b489f4ce9ae3edbf28e11f4def0dc28099c33d9 Mon Sep 17 00:00:00 2001 From: Mateo Noel Rabines <39866695+mateonoel2@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:41:13 -0500 Subject: [PATCH 088/202] cli: Add `--reuse` flag for replacing workspace in existing window (#38131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #ISSUE it is was still in [discussion](https://github.com/zed-industries/zed/discussions/37983) Release Notes: - Added: `--reuse` (`-r`) CLI flag to replace the workspace in an existing window instead of opening a new one This PR adds a new `--reuse` (`-r`) CLI flag that allows users to replace the workspace in an existing Zed window instead of opening a new one or adding files to the current workspace. ### What it does The `--reuse` flag finds an available local workspace window and replaces its workspace with the newly specified paths. This provides a third workspace opening mode alongside the existing `--add` and `--new` flags. ### Implementation Details - **CLI Flag**: Added `--reuse` (`-r`) flag with proper mutual exclusion with `--add` and `--new` - **Window Replacement**: Uses the existing `replace_window` option in `workspace::OpenOptions` - **Window Selection**: Reuses the first available local workspace window - **Fallback Behavior**: When no existing windows are found, creates a new window - **Test Coverage**: Added comprehensive test for the reuse functionality ### Behavior - `zed -r file.txt` - Replaces the workspace in an available window with `file.txt` - If no windows are open, creates a new window (same as default behavior) - Mutually exclusive with `-a/--add` and `-n/--new` flags - Works with multiple files and directories ### Files Changed - `crates/cli/src/cli.rs` - Added `reuse` field to `CliRequest::Open` - `crates/cli/src/main.rs` - Added CLI argument definition and parsing - `crates/zed/src/zed/open_listener.rs` - Implemented reuse logic and added tests - `crates/zed/src/zed/windows_only_instance.rs` - Updated for Windows compatibility ### Testing - ✅ Unit tests pass - ✅ Manual testing confirms expected behavior: - Works when no windows are open - Replaces workspace in existing window - Maintains compatibility with existing `-a` and `-n` flags - Proper help text display ## Manual testing #### In this first video we do a couple of tests: * **1**: What happens if we use the -r flag when there are no windows open? - works as expected. It opens the files in a new window. * **2**: Does it work as expected if there is already a window open. Does it overrides the workspace? - yes it does. When opening a different file it overrides the current window instead of creating a new one. * **3**: Does the -n flag still works as expected? - yes, it creates the project in a new window * **4**: What about the -a flag? - yes, on the last accessed page * **5**: we do the replace command. It overrides the first opened window, do we want this behavior? - It is good enough that it overrides one of the opened windows with the new project. It still makes the user automatically go to the window with the specified files * **6**: we use the -r command again replacing the workspace with a new one. - this indeed worked as expected https://github.com/user-attachments/assets/f1cd7f4b-f4af-4da2-a755-c0be7ce96c0d #### In here the we check how the --help flag now displays the new command. (Description was later updated) https://github.com/user-attachments/assets/a8a7a288-d926-431b-a9f9-a8c3d909a2ec --- crates/cli/src/cli.rs | 1 + crates/cli/src/main.rs | 8 +- crates/zed/src/zed/open_listener.rs | 126 +++++++++++++++++++- crates/zed/src/zed/windows_only_instance.rs | 1 + 4 files changed, 133 insertions(+), 3 deletions(-) diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 79a10fa2b0936b44d9500fd9990ffa4c6ac62e85..fbd7e2693a74598f3840afa5f4a99c86e96f2357 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -17,6 +17,7 @@ pub enum CliRequest { wsl: Option, wait: bool, open_new_workspace: Option, + reuse: bool, env: Option>, user_data_dir: Option, }, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4c25cf1c9d701369c7ce18a1cb70b8073da161e5..64a342a332f2c1b896afe58dda0e7156304e8116 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -62,11 +62,14 @@ struct Args { #[arg(short, long)] wait: bool, /// Add files to the currently open workspace - #[arg(short, long, overrides_with = "new")] + #[arg(short, long, overrides_with_all = ["new", "reuse"])] add: bool, /// Create a new workspace - #[arg(short, long, overrides_with = "add")] + #[arg(short, long, overrides_with_all = ["add", "reuse"])] new: bool, + /// Reuse an existing window, replacing its workspace + #[arg(short, long, overrides_with_all = ["add", "new"])] + reuse: bool, /// Sets a custom directory for all user data (e.g., database, extensions, logs). /// This overrides the default platform-specific data directory location: #[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")] @@ -374,6 +377,7 @@ fn main() -> Result<()> { wsl, wait: args.wait, open_new_workspace, + reuse: args.reuse, env, user_data_dir: user_data_dir_for_thread, })?; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index a8a998b6580269de150280c432c329cf59c30c22..3e0250825860aa358bb43125267dd4be8299b736 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -328,6 +328,7 @@ pub async fn handle_cli_connection( wait, wsl, open_new_workspace, + reuse, env, user_data_dir: _, } => { @@ -363,6 +364,7 @@ pub async fn handle_cli_connection( paths, diff_paths, open_new_workspace, + reuse, &responses, wait, app_state.clone(), @@ -382,6 +384,7 @@ async fn open_workspaces( paths: Vec, diff_paths: Vec<[String; 2]>, open_new_workspace: Option, + reuse: bool, responses: &IpcSender, wait: bool, app_state: Arc, @@ -441,6 +444,7 @@ async fn open_workspaces( workspace_paths, diff_paths.clone(), open_new_workspace, + reuse, wait, responses, env.as_ref(), @@ -487,6 +491,7 @@ async fn open_local_workspace( workspace_paths: Vec, diff_paths: Vec<[String; 2]>, open_new_workspace: Option, + reuse: bool, wait: bool, responses: &IpcSender, env: Option<&HashMap>, @@ -497,12 +502,30 @@ async fn open_local_workspace( let paths_with_position = derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await; + + // Handle reuse flag by finding existing window to replace + let replace_window = if reuse { + cx.update(|cx| workspace::local_workspace_windows(cx).into_iter().next()) + .ok() + .flatten() + } else { + None + }; + + // For reuse, force new workspace creation but with replace_window set + let effective_open_new_workspace = if reuse { + Some(true) + } else { + open_new_workspace + }; + match open_paths_with_positions( &paths_with_position, &diff_paths, app_state.clone(), workspace::OpenOptions { - open_new_workspace, + open_new_workspace: effective_open_new_workspace, + replace_window, env: env.cloned(), ..Default::default() }, @@ -614,7 +637,9 @@ mod tests { }; use editor::Editor; use gpui::TestAppContext; + use language::LineEnding; use remote::SshConnectionOptions; + use rope::Rope; use serde_json::json; use std::sync::Arc; use util::path; @@ -780,6 +805,7 @@ mod tests { vec![], open_new_workspace, false, + false, &response_tx, None, &app_state, @@ -791,4 +817,102 @@ mod tests { assert!(!errored); } + + #[gpui::test] + async fn test_reuse_flag_functionality(cx: &mut TestAppContext) { + let app_state = init_test(cx); + + let root_dir = if cfg!(windows) { "C:\\root" } else { "/root" }; + let file1_path = if cfg!(windows) { + "C:\\root\\file1.txt" + } else { + "/root/file1.txt" + }; + let file2_path = if cfg!(windows) { + "C:\\root\\file2.txt" + } else { + "/root/file2.txt" + }; + + app_state.fs.create_dir(Path::new(root_dir)).await.unwrap(); + app_state + .fs + .create_file(Path::new(file1_path), Default::default()) + .await + .unwrap(); + app_state + .fs + .save( + Path::new(file1_path), + &Rope::from("content1"), + LineEnding::Unix, + ) + .await + .unwrap(); + app_state + .fs + .create_file(Path::new(file2_path), Default::default()) + .await + .unwrap(); + app_state + .fs + .save( + Path::new(file2_path), + &Rope::from("content2"), + LineEnding::Unix, + ) + .await + .unwrap(); + + // First, open a workspace normally + let (response_tx, _response_rx) = ipc::channel::().unwrap(); + let workspace_paths = vec![file1_path.to_string()]; + + let _errored = cx + .spawn({ + let app_state = app_state.clone(); + let response_tx = response_tx.clone(); + |mut cx| async move { + open_local_workspace( + workspace_paths, + vec![], + None, + false, + false, + &response_tx, + None, + &app_state, + &mut cx, + ) + .await + } + }) + .await; + + // Now test the reuse functionality - should replace the existing workspace + let workspace_paths_reuse = vec![file1_path.to_string()]; + + let errored_reuse = cx + .spawn({ + let app_state = app_state.clone(); + let response_tx = response_tx.clone(); + |mut cx| async move { + open_local_workspace( + workspace_paths_reuse, + vec![], + None, // open_new_workspace will be overridden by reuse logic + true, // reuse = true + false, + &response_tx, + None, + &app_state, + &mut cx, + ) + .await + } + }) + .await; + + assert!(!errored_reuse); + } } diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 45f3cd158bb38156a0981f01e5331dc0aead91c9..f3eab154415814d60e2b06f5823d47006b1c367c 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -158,6 +158,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { wait: false, wsl: args.wsl.clone(), open_new_workspace: None, + reuse: false, env: None, user_data_dir: args.user_data_dir.clone(), } From 04a45e3501e99bda9be79c87ecb40c70c38e90df Mon Sep 17 00:00:00 2001 From: Dario Griffo Date: Tue, 21 Oct 2025 07:50:57 +0100 Subject: [PATCH 089/202] Add debian community repository (#40698) I maintain this repository that contains several developer tools like - ghostty - zig - yazi all of them are updated usually the same day as upstream. Release Notes: - N/A --- docs/src/linux.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/src/linux.md b/docs/src/linux.md index f349c80d61640817938c33337c18180e02c043b7..433891a3e461f6c20d4281c72f7b9ae10a459c03 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -51,10 +51,21 @@ There are several third-party Zed packages for various Linux distributions and p See [Repology](https://repology.org/project/zed-editor/versions) for a list of Zed packages in various repositories. +### Community + When installing a third-party package please be aware that it may not be completely up to date and may be slightly different from the Zed we package (a common change is to rename the binary to `zedit` or `zeditor` to avoid conflicting with other packages). We'd love your help making Zed available for everyone. If Zed is not yet available for your package manager, and you would like to fix that, we have some notes on [how to do it](./development/linux.md#notes-for-packaging-zed). +The packages in this section provide binary installs for Zed but are not official packages within the associated distributions. These packages are maintained by community members and as such a higher level of caution should be taken when installing them. + +#### Debian + +Zed is available in [this community-maintained repository](https://debian.griffo.io/). + +Instructions for each version are available in the README of the repository where packages are built. +Build, packaging and instructions for each version are available in the README of the [repository](https://github.com/dariogriffo/zed-debian) + ### Downloading manually If you'd prefer, you can install Zed by downloading our pre-built .tar.gz. This is the same artifact that our install script uses, but you can customize the location of your installation by modifying the instructions below: From a56122e1448ce6022a8a1be08167f94c6498fc66 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 21 Oct 2025 08:53:45 +0200 Subject: [PATCH 090/202] ci: Do not use full debug info in CI builds (#40764) For good backtraces in tests 'limited' is all we need. Closes #ISSUE Release Notes: - N/A --- .cargo/ci-config.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.cargo/ci-config.toml b/.cargo/ci-config.toml index d5e312c2429ad8a4fa933d4080c8fcde217bd6eb..a18554fd6e119010c5692da0be62428d3ea2a342 100644 --- a/.cargo/ci-config.toml +++ b/.cargo/ci-config.toml @@ -11,6 +11,10 @@ [target.'cfg(all())'] rustflags = ["-D", "warnings"] +# We don't need fullest debug information for dev stuff (tests etc.) in CI. +[profile.dev] +debug = "limited" + # Use Mold on Linux, because it's faster than GNU ld and LLD. # # We no longer set this in the default `config.toml` so that developers can opt in to Wild, which From ea6e6dbda121f9d2b65bfa9b06846aa5c5ed5d4f Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Tue, 21 Oct 2025 00:17:26 -0700 Subject: [PATCH 091/202] Add log message on first render (#40749) Having this in our logs with a timestamp should help when users submit issues with logs about slow startup time. Release Notes: - N/A --- crates/workspace/src/workspace.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c8c94ce0e932dc6773c8f6c168c95563b60c879..51a4edbf2567f941595bcd7307095858b240db57 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -102,7 +102,10 @@ use std::{ path::{Path, PathBuf}, process::ExitStatus, rc::Rc, - sync::{Arc, LazyLock, Weak, atomic::AtomicUsize}, + sync::{ + Arc, LazyLock, Weak, + atomic::{AtomicBool, AtomicUsize}, + }, time::Duration, }; use task::{DebugScenario, SpawnInTerminal, TaskContext}; @@ -6358,6 +6361,10 @@ impl Render for DraggedDock { impl Render for Workspace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + static FIRST_PAINT: AtomicBool = AtomicBool::new(true); + if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) { + log::info!("Rendered first frame"); + } let mut context = KeyContext::new_with_defaults(); context.add("Workspace"); context.set("keyboard_layout", cx.keyboard_layout().name().to_string()); From 1b544b9e19edd72786b568a80e09c61e799f53e6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:58:26 +0200 Subject: [PATCH 092/202] ci: Run slow tests first (#40769) Tests are hand-picked based on yours truly's preference Release Notes: - N/A --- .config/nextest.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.config/nextest.toml b/.config/nextest.toml index b05d68911fb5f50afaa623649fd426f7eb1e7bbe..49fb4d01f794613e430953e4565923a784368836 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -4,3 +4,17 @@ sequential-db-tests = { max-threads = 1 } [[profile.default.overrides]] filter = 'package(db)' test-group = 'sequential-db-tests' + +# Run slowest tests first. +# +[[profile.default.overrides]] +filter = 'package(worktree) and test(test_random_worktree_changes)' +priority = 100 + +[[profile.default.overrides]] +filter = 'package(collab) and (test(random_project_collaboration_tests) or test(random_channel_buffer_tests) or test(test_contact_requests) or test(test_basic_following))' +priority = 99 + +[[profile.default.overrides]] +filter = 'package(extension_host) and test(test_extension_store_with_test_extension)' +priority = 99 From 0aa7b7c7738f07c85fd7e8fb38c144c242502050 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 21 Oct 2025 10:38:40 +0200 Subject: [PATCH 093/202] editor: Toggle diff hunk based on current mouse position (#40773) This fixes an issue where we would search for the hovered diff hunk based on the mouse hit test computed during (or prior) editor paint instead of the mouse hit test computed prior to the mouse event invocation. That in turn could lead to cases where moving the mouse from the editor to the project panel and then clicking a file shortly after would expand a diff hunk when actually nothing should happen in that case. Release Notes: - Fixed an issue where diff hunks would sometimes erroneously toggle upon mouse clicks. --- crates/editor/src/element.rs | 37 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b80953ad842b5c35dbfbbd8872f329760bd9b7a0..65c9d1dbc78d14ab1419118f1d56d2b10a494ba6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -652,7 +652,6 @@ impl EditorElement { fn mouse_left_down( editor: &mut Editor, event: &MouseDownEvent, - hovered_hunk: Option>, position_map: &PositionMap, line_numbers: &HashMap, window: &mut Window, @@ -668,7 +667,20 @@ impl EditorElement { let mut click_count = event.click_count; let mut modifiers = event.modifiers; - if let Some(hovered_hunk) = hovered_hunk { + if let Some(hovered_hunk) = + position_map + .display_hunks + .iter() + .find_map(|(hunk, hunk_hitbox)| match hunk { + DisplayDiffHunk::Folded { .. } => None, + DisplayDiffHunk::Unfolded { + multi_buffer_range, .. + } => hunk_hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.is_hovered(window)) + .then(|| multi_buffer_range.clone()), + }) + { editor.toggle_single_diff_hunk(hovered_hunk, cx); cx.notify(); return; @@ -7263,26 +7275,6 @@ impl EditorElement { window.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); - let diff_hunk_range = - layout - .display_hunks - .iter() - .find_map(|(hunk, hunk_hitbox)| match hunk { - DisplayDiffHunk::Folded { .. } => None, - DisplayDiffHunk::Unfolded { - multi_buffer_range, .. - } => { - if hunk_hitbox - .as_ref() - .map(|hitbox| hitbox.is_hovered(window)) - .unwrap_or(false) - { - Some(multi_buffer_range.clone()) - } else { - None - } - } - }); let line_numbers = layout.line_numbers.clone(); move |event: &MouseDownEvent, phase, window, cx| { @@ -7299,7 +7291,6 @@ impl EditorElement { Self::mouse_left_down( editor, event, - diff_hunk_range.clone(), &position_map, line_numbers.as_ref(), window, From 0721c7873a0b053f1d6f9133a25eb42b84f07f0b Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 21 Oct 2025 11:03:12 +0200 Subject: [PATCH 094/202] Make `kotlin-lsp` the default language server (#40776) Following a conversation with the maintainer/owner of kotlin-language-server, he recommended switching to the official language server, which is better in many aspects and also more actively maintained. Release Notes: - Made the official Kotlin Language Server the default language server for Kotlin. --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index c110b169b7cd18098de9945b43f72ac1fa0cff19..5107a7f64f3a57bfb612476797fb3c6659f79b4e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1741,7 +1741,7 @@ } }, "Kotlin": { - "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] + "language_servers": ["!kotlin-language-server", "kotlin-lsp", "..."] }, "LaTeX": { "formatter": "language_server", From 977887b65f1cbfa0cdf59bc6e42d0df40173f224 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 21 Oct 2025 11:24:22 +0200 Subject: [PATCH 095/202] ci: Bump max target directory size on Mac to 300GB (#40778) I did not bump it for Linux as some machines have smaller disks (~300GB or so); with Mac, we have at least 1TB on all of our boxes Release Notes: - N/A --- .github/actions/run_tests/action.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml index faf94017976f4b06fdaaa80a5db8083405a7950a..3bc28249f3b8b2a08a48be040177530c5ecfd407 100644 --- a/.github/actions/run_tests/action.yml +++ b/.github/actions/run_tests/action.yml @@ -15,8 +15,11 @@ runs: node-version: "18" - name: Limit target directory size + env: + MAX_SIZE: ${{ runner.os == 'macOS' && 300 || 100 }} shell: bash -euxo pipefail {0} - run: script/clear-target-dir-if-larger-than 100 + # Use the variable in the run command + run: script/clear-target-dir-if-larger-than ${{ env.MAX_SIZE }} - name: Run tests shell: bash -euxo pipefail {0} From b487d2cfe0dac402fd3cfd21d19354ab182f55ed Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 21 Oct 2025 07:30:21 -0300 Subject: [PATCH 096/202] zeta2 inspector: Feedback box (#40732) Adds a way to submit feedback about a zeta2 prediction from the inspector. The telemetry event includes: - project snapshot (git + unsaved buffer state) - the full request and response - user feedback kind and text Release Notes: - N/A --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 8 + assets/keymaps/default-macos.json | 8 + assets/keymaps/default-windows.json | 8 + crates/agent/src/agent.rs | 16 +- crates/agent/src/thread.rs | 99 +---- crates/project/src/project.rs | 1 + crates/project/src/telemetry_snapshot.rs | 125 ++++++ crates/zeta2/src/zeta2.rs | 33 +- crates/zeta2_tools/Cargo.toml | 1 + crates/zeta2_tools/src/zeta2_tools.rs | 459 ++++++++++++++++++----- 11 files changed, 540 insertions(+), 219 deletions(-) create mode 100644 crates/project/src/telemetry_snapshot.rs diff --git a/Cargo.lock b/Cargo.lock index 6ff72c08b4482a7b5ae5e4abeed90157f6bcd124..430a52d0f3d41523c737dbcd6ecf4e0e5a9424fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21533,6 +21533,7 @@ dependencies = [ "serde", "serde_json", "settings", + "telemetry", "text", "ui", "ui_input", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index fff7469199f88b88bb02fcf2d595d5ee76628315..51cf0b03a56a03aaaa9cc8ad6550d8debfda0df7 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1290,5 +1290,13 @@ "home": "settings_editor::FocusFirstNavEntry", "end": "settings_editor::FocusLastNavEntry" } + }, + { + "context": "Zeta2Feedback > Editor", + "bindings": { + "enter": "editor::Newline", + "ctrl-enter up": "dev::Zeta2RatePredictionPositive", + "ctrl-enter down": "dev::Zeta2RatePredictionNegative" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 0b4119e95e4bf33d1f19a538fa231cc13ff79419..97846a8edf63cae577eb17d49ee835b43295be35 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1396,5 +1396,13 @@ "home": "settings_editor::FocusFirstNavEntry", "end": "settings_editor::FocusLastNavEntry" } + }, + { + "context": "Zeta2Feedback > Editor", + "bindings": { + "enter": "editor::Newline", + "cmd-enter up": "dev::Zeta2RatePredictionPositive", + "cmd-enter down": "dev::Zeta2RatePredictionNegative" + } } ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 39c1b672a4105e9565bbdaded7229402831c702d..02bd2207c2805ef5f3eef1b06378f197595ad4a4 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1319,5 +1319,13 @@ "home": "settings_editor::FocusFirstNavEntry", "end": "settings_editor::FocusLastNavEntry" } + }, + { + "context": "Zeta2Feedback > Editor", + "bindings": { + "enter": "editor::Newline", + "ctrl-enter up": "dev::Zeta2RatePredictionPositive", + "ctrl-enter down": "dev::Zeta2RatePredictionNegative" + } } ] diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 32dec9f723a6776fd14def29be3be4eb21afa72d..65eb25e6ac9d005fc2e18901a56287e2938e5bb8 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -48,24 +48,10 @@ use util::rel_path::RelPath; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct ProjectSnapshot { - pub worktree_snapshots: Vec, + pub worktree_snapshots: Vec, pub timestamp: DateTime, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct WorktreeSnapshot { - pub worktree_path: String, - pub git_state: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct GitState { - pub remote_url: Option, - pub head_sha: Option, - pub current_branch: Option, - pub diff: Option, -} - const RULES_FILE_NAMES: [&str; 9] = [ ".rules", ".cursorrules", diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index ec9d50ff2f62c5602dd91e5da47593764ea01c85..c89ad1df241c3b9c6e07b9a5433dd964244ba2cb 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1,9 +1,8 @@ use crate::{ ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, - DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GitState, GrepTool, + DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool, SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, - WorktreeSnapshot, }; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; @@ -26,7 +25,6 @@ use futures::{ future::Shared, stream::FuturesUnordered, }; -use git::repository::DiffType; use gpui::{ App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, }; @@ -37,10 +35,7 @@ use language_model::{ LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID, }; -use project::{ - Project, - git_store::{GitStore, RepositoryState}, -}; +use project::Project; use prompt_store::ProjectContext; use schemars::{JsonSchema, Schema}; use serde::{Deserialize, Serialize}; @@ -880,101 +875,17 @@ impl Thread { project: Entity, cx: &mut Context, ) -> Task> { - let git_store = project.read(cx).git_store().clone(); - let worktree_snapshots: Vec<_> = project - .read(cx) - .visible_worktrees(cx) - .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx)) - .collect(); - + let task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx); cx.spawn(async move |_, _| { - let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; + let snapshot = task.await; Arc::new(ProjectSnapshot { - worktree_snapshots, + worktree_snapshots: snapshot.worktree_snapshots, timestamp: Utc::now(), }) }) } - fn worktree_snapshot( - worktree: Entity, - git_store: Entity, - cx: &App, - ) -> Task { - cx.spawn(async move |cx| { - // Get worktree path and snapshot - let worktree_info = cx.update(|app_cx| { - let worktree = worktree.read(app_cx); - let path = worktree.abs_path().to_string_lossy().into_owned(); - let snapshot = worktree.snapshot(); - (path, snapshot) - }); - - let Ok((worktree_path, _snapshot)) = worktree_info else { - return WorktreeSnapshot { - worktree_path: String::new(), - git_state: None, - }; - }; - - let git_state = git_store - .update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .find(|repo| { - repo.read(cx) - .abs_path_to_repo_path(&worktree.read(cx).abs_path()) - .is_some() - }) - .cloned() - }) - .ok() - .flatten() - .map(|repo| { - repo.update(cx, |repo, _| { - let current_branch = - repo.branch.as_ref().map(|branch| branch.name().to_owned()); - repo.send_job(None, |state, _| async move { - let RepositoryState::Local { backend, .. } = state else { - return GitState { - remote_url: None, - head_sha: None, - current_branch, - diff: None, - }; - }; - - let remote_url = backend.remote_url("origin"); - let head_sha = backend.head_sha().await; - let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); - - GitState { - remote_url, - head_sha, - current_branch, - diff, - } - }) - }) - }); - - let git_state = match git_state { - Some(git_state) => match git_state.ok() { - Some(git_state) => git_state.await.ok(), - None => None, - }, - None => None, - }; - - WorktreeSnapshot { - worktree_path, - git_state, - } - }) - } - pub fn project_context(&self) -> &Entity { &self.project_context } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 678607b53219992317e1762ff15b57500eb33d79..c0d853966694a68fad9d69ad160071c3d5fca9bf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -16,6 +16,7 @@ pub mod project_settings; pub mod search; mod task_inventory; pub mod task_store; +pub mod telemetry_snapshot; pub mod terminals; pub mod toolchain_store; pub mod worktree_store; diff --git a/crates/project/src/telemetry_snapshot.rs b/crates/project/src/telemetry_snapshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..79fe2bd8b3f21df03b4cf7a59f73df93b22f3a6c --- /dev/null +++ b/crates/project/src/telemetry_snapshot.rs @@ -0,0 +1,125 @@ +use git::repository::DiffType; +use gpui::{App, Entity, Task}; +use serde::{Deserialize, Serialize}; +use worktree::Worktree; + +use crate::{ + Project, + git_store::{GitStore, RepositoryState}, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TelemetrySnapshot { + pub worktree_snapshots: Vec, +} + +impl TelemetrySnapshot { + pub fn new(project: &Entity, cx: &mut App) -> Task { + let git_store = project.read(cx).git_store().clone(); + let worktree_snapshots: Vec<_> = project + .read(cx) + .visible_worktrees(cx) + .map(|worktree| TelemetryWorktreeSnapshot::new(worktree, git_store.clone(), cx)) + .collect(); + + cx.spawn(async move |_| { + let worktree_snapshots = futures::future::join_all(worktree_snapshots).await; + + Self { worktree_snapshots } + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TelemetryWorktreeSnapshot { + pub worktree_path: String, + pub git_state: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct GitState { + pub remote_url: Option, + pub head_sha: Option, + pub current_branch: Option, + pub diff: Option, +} + +impl TelemetryWorktreeSnapshot { + fn new( + worktree: Entity, + git_store: Entity, + cx: &App, + ) -> Task { + cx.spawn(async move |cx| { + // Get worktree path and snapshot + let worktree_info = cx.update(|app_cx| { + let worktree = worktree.read(app_cx); + let path = worktree.abs_path().to_string_lossy().into_owned(); + let snapshot = worktree.snapshot(); + (path, snapshot) + }); + + let Ok((worktree_path, _snapshot)) = worktree_info else { + return TelemetryWorktreeSnapshot { + worktree_path: String::new(), + git_state: None, + }; + }; + + let git_state = git_store + .update(cx, |git_store, cx| { + git_store + .repositories() + .values() + .find(|repo| { + repo.read(cx) + .abs_path_to_repo_path(&worktree.read(cx).abs_path()) + .is_some() + }) + .cloned() + }) + .ok() + .flatten() + .map(|repo| { + repo.update(cx, |repo, _| { + let current_branch = + repo.branch.as_ref().map(|branch| branch.name().to_owned()); + repo.send_job(None, |state, _| async move { + let RepositoryState::Local { backend, .. } = state else { + return GitState { + remote_url: None, + head_sha: None, + current_branch, + diff: None, + }; + }; + + let remote_url = backend.remote_url("origin"); + let head_sha = backend.head_sha().await; + let diff = backend.diff(DiffType::HeadToWorktree).await.ok(); + + GitState { + remote_url, + head_sha, + current_branch, + diff, + } + }) + }) + }); + + let git_state = match git_state { + Some(git_state) => match git_state.ok() { + Some(git_state) => git_state.await.ok(), + None => None, + }, + None => None, + }; + + TelemetryWorktreeSnapshot { + worktree_path, + git_state, + } + }) + } +} diff --git a/crates/zeta2/src/zeta2.rs b/crates/zeta2/src/zeta2.rs index 7c945d6ebbe9c994977adbcc72c6d8fc175930d4..42eb565502e6568491e820dfb5c0921e4d56039b 100644 --- a/crates/zeta2/src/zeta2.rs +++ b/crates/zeta2/src/zeta2.rs @@ -11,7 +11,7 @@ use edit_prediction_context::{ DeclarationId, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions, EditPredictionExcerptOptions, EditPredictionScoreOptions, SyntaxIndex, SyntaxIndexState, }; -use feature_flags::FeatureFlag; +use feature_flags::{FeatureFlag, FeatureFlagAppExt as _}; use futures::AsyncReadExt as _; use futures::channel::{mpsc, oneshot}; use gpui::http_client::{AsyncBody, Method}; @@ -32,7 +32,6 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use thiserror::Error; use util::rel_path::RelPathBuf; -use util::some_or_debug_panic; use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification}; mod prediction; @@ -103,12 +102,12 @@ pub struct ZetaOptions { } pub struct PredictionDebugInfo { - pub context: EditPredictionContext, + pub request: predict_edits_v3::PredictEditsRequest, pub retrieval_time: TimeDelta, pub buffer: WeakEntity, pub position: language::Anchor, pub local_prompt: Result, - pub response_rx: oneshot::Receiver>, + pub response_rx: oneshot::Receiver>, } pub type RequestDebugInfo = predict_edits_v3::DebugInfo; @@ -571,6 +570,9 @@ impl Zeta { if path.pop() { Some(path) } else { None } }); + // TODO data collection + let can_collect_data = cx.is_staff(); + let request_task = cx.background_spawn({ let snapshot = snapshot.clone(); let buffer = buffer.clone(); @@ -606,25 +608,22 @@ impl Zeta { options.max_diagnostic_bytes, ); - let debug_context = debug_tx.map(|tx| (tx, context.clone())); - let request = make_cloud_request( excerpt_path, context, events, - // TODO data collection - false, + can_collect_data, diagnostic_groups, diagnostic_groups_truncated, None, - debug_context.is_some(), + debug_tx.is_some(), &worktree_snapshots, index_state.as_deref(), Some(options.max_prompt_bytes), options.prompt_format, ); - let debug_response_tx = if let Some((debug_tx, context)) = debug_context { + let debug_response_tx = if let Some(debug_tx) = &debug_tx { let (response_tx, response_rx) = oneshot::channel(); let local_prompt = PlannedPrompt::populate(&request) @@ -633,7 +632,7 @@ impl Zeta { debug_tx .unbounded_send(PredictionDebugInfo { - context, + request: request.clone(), retrieval_time, buffer: buffer.downgrade(), local_prompt, @@ -660,12 +659,12 @@ impl Zeta { if let Some(debug_response_tx) = debug_response_tx { debug_response_tx - .send(response.as_ref().map_err(|err| err.to_string()).and_then( - |response| match some_or_debug_panic(response.0.debug_info.clone()) { - Some(debug_info) => Ok(debug_info), - None => Err("Missing debug info".to_string()), - }, - )) + .send( + response + .as_ref() + .map_err(|err| err.to_string()) + .map(|response| response.0.clone()), + ) .ok(); } diff --git a/crates/zeta2_tools/Cargo.toml b/crates/zeta2_tools/Cargo.toml index b56b806e783b7e6acc946a9dadb00703e4a7f2c1..edd1b1eb242c6c02001bec53120425f9a05e5d1d 100644 --- a/crates/zeta2_tools/Cargo.toml +++ b/crates/zeta2_tools/Cargo.toml @@ -27,6 +27,7 @@ multi_buffer.workspace = true ordered-float.workspace = true project.workspace = true serde.workspace = true +telemetry.workspace = true text.workspace = true ui.workspace = true ui_input.workspace = true diff --git a/crates/zeta2_tools/src/zeta2_tools.rs b/crates/zeta2_tools/src/zeta2_tools.rs index 0ac4fb2162ca632618df0c2b0d256b2fd7c30742..7b806e2b9a4ba7c7dbda41bb0f5750e5d2b9ff97 100644 --- a/crates/zeta2_tools/src/zeta2_tools.rs +++ b/crates/zeta2_tools/src/zeta2_tools.rs @@ -1,37 +1,38 @@ -use std::{ - cmp::Reverse, collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc, - time::Duration, -}; +use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; use chrono::TimeDelta; use client::{Client, UserStore}; -use cloud_llm_client::predict_edits_v3::{DeclarationScoreComponents, PromptFormat}; +use cloud_llm_client::predict_edits_v3::{ + self, DeclarationScoreComponents, PredictEditsRequest, PredictEditsResponse, PromptFormat, +}; use collections::HashMap; use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer}; use feature_flags::FeatureFlagAppExt as _; -use futures::{StreamExt as _, channel::oneshot}; +use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared}; use gpui::{ - CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, - actions, prelude::*, + CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, + WeakEntity, actions, prelude::*, }; use language::{Buffer, DiskState}; use ordered_float::OrderedFloat; -use project::{Project, WorktreeId}; -use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*}; +use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot}; +use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*}; use ui_input::SingleLineInput; use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; use workspace::{Item, SplitDirection, Workspace}; use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions}; -use edit_prediction_context::{ - DeclarationStyle, EditPredictionContextOptions, EditPredictionExcerptOptions, -}; +use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions}; actions!( dev, [ /// Opens the language server protocol logs viewer. - OpenZeta2Inspector + OpenZeta2Inspector, + /// Rate prediction as positive. + Zeta2RatePredictionPositive, + /// Rate prediction as negative. + Zeta2RatePredictionNegative, ] ); @@ -89,16 +90,24 @@ struct LastPrediction { buffer: WeakEntity, position: language::Anchor, state: LastPredictionState, + request: PredictEditsRequest, + project_snapshot: Shared>>, _task: Option>, } +#[derive(Clone, Copy, PartialEq)] +enum Feedback { + Positive, + Negative, +} + enum LastPredictionState { Requested, Success { - inference_time: TimeDelta, - parsing_time: TimeDelta, - prompt_planning_time: TimeDelta, model_response_editor: Entity, + feedback_editor: Entity, + feedback: Option, + response: predict_edits_v3::PredictEditsResponse, }, Failed { message: String, @@ -129,7 +138,7 @@ impl Zeta2Inspector { focus_handle: cx.focus_handle(), project: project.clone(), last_prediction: None, - active_view: ActiveView::Context, + active_view: ActiveView::Inference, max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx), min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx), cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx), @@ -300,17 +309,23 @@ impl Zeta2Inspector { let language_registry = self.project.read(cx).languages().clone(); async move |this, cx| { let mut languages = HashMap::default(); - for lang_id in prediction - .context - .declarations + for ext in prediction + .request + .referenced_declarations .iter() - .map(|snippet| snippet.declaration.identifier().language_id) - .chain(prediction.context.excerpt_text.language_id) + .filter_map(|snippet| snippet.path.extension()) + .chain(prediction.request.excerpt_path.extension()) { - if let Entry::Vacant(entry) = languages.entry(lang_id) { + if !languages.contains_key(ext) { // Most snippets are gonna be the same language, // so we think it's fine to do this sequentially for now - entry.insert(language_registry.language_for_id(lang_id).await.ok()); + languages.insert( + ext.to_owned(), + language_registry + .language_for_name_or_extension(&ext.to_string_lossy()) + .await + .ok(), + ); } } @@ -333,13 +348,12 @@ impl Zeta2Inspector { let excerpt_buffer = cx.new(|cx| { let mut buffer = - Buffer::local(prediction.context.excerpt_text.body, cx); + Buffer::local(prediction.request.excerpt.clone(), cx); if let Some(language) = prediction - .context - .excerpt_text - .language_id - .as_ref() - .and_then(|id| languages.get(id)) + .request + .excerpt_path + .extension() + .and_then(|ext| languages.get(ext)) { buffer.set_language(language.clone(), cx); } @@ -353,25 +367,18 @@ impl Zeta2Inspector { cx, ); - let mut declarations = prediction.context.declarations.clone(); + let mut declarations = + prediction.request.referenced_declarations.clone(); declarations.sort_unstable_by_key(|declaration| { - Reverse(OrderedFloat( - declaration.score(DeclarationStyle::Declaration), - )) + Reverse(OrderedFloat(declaration.declaration_score)) }); for snippet in &declarations { - let path = this - .project - .read(cx) - .path_for_entry(snippet.declaration.project_entry_id(), cx); - let snippet_file = Arc::new(ExcerptMetadataFile { title: RelPath::unix(&format!( "{} (Score: {})", - path.map(|p| p.path.display(path_style).to_string()) - .unwrap_or_else(|| "".to_string()), - snippet.score(DeclarationStyle::Declaration) + snippet.path.display(), + snippet.declaration_score )) .unwrap() .into(), @@ -380,11 +387,10 @@ impl Zeta2Inspector { }); let excerpt_buffer = cx.new(|cx| { - let mut buffer = - Buffer::local(snippet.declaration.item_text().0, cx); + let mut buffer = Buffer::local(snippet.text.clone(), cx); buffer.file_updated(snippet_file, cx); - if let Some(language) = - languages.get(&snippet.declaration.identifier().language_id) + if let Some(ext) = snippet.path.extension() + && let Some(language) = languages.get(ext) { buffer.set_language(language.clone(), cx); } @@ -399,7 +405,7 @@ impl Zeta2Inspector { let excerpt_id = excerpt_ids.first().unwrap(); excerpt_score_components - .insert(*excerpt_id, snippet.components.clone()); + .insert(*excerpt_id, snippet.score_components.clone()); } multibuffer @@ -431,25 +437,91 @@ impl Zeta2Inspector { if let Some(prediction) = this.last_prediction.as_mut() { prediction.state = match response { Ok(Ok(response)) => { - prediction.prompt_editor.update( - cx, - |prompt_editor, cx| { - prompt_editor.set_text( - response.prompt, - window, + if let Some(debug_info) = &response.debug_info { + prediction.prompt_editor.update( + cx, + |prompt_editor, cx| { + prompt_editor.set_text( + debug_info.prompt.as_str(), + window, + cx, + ); + }, + ); + } + + let feedback_editor = cx.new(|cx| { + let buffer = cx.new(|cx| { + let mut buffer = Buffer::local("", cx); + buffer.set_language( + markdown_language.clone(), cx, ); + buffer + }); + let buffer = + cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let mut editor = Editor::new( + EditorMode::AutoHeight { + min_lines: 3, + max_lines: None, + }, + buffer, + None, + window, + cx, + ); + editor.set_placeholder_text( + "Write feedback here", + window, + cx, + ); + editor.set_show_line_numbers(false, cx); + editor.set_show_gutter(false, cx); + editor.set_show_scrollbars(false, cx); + editor + }); + + cx.subscribe_in( + &feedback_editor, + window, + |this, editor, ev, window, cx| match ev { + EditorEvent::BufferEdited => { + if let Some(last_prediction) = + this.last_prediction.as_mut() + && let LastPredictionState::Success { + feedback: feedback_state, + .. + } = &mut last_prediction.state + { + if feedback_state.take().is_some() { + editor.update(cx, |editor, cx| { + editor.set_placeholder_text( + "Write feedback here", + window, + cx, + ); + }); + cx.notify(); + } + } + } + _ => {} }, - ); + ) + .detach(); LastPredictionState::Success { - prompt_planning_time: response.prompt_planning_time, - inference_time: response.inference_time, - parsing_time: response.parsing_time, model_response_editor: cx.new(|cx| { let buffer = cx.new(|cx| { let mut buffer = Buffer::local( - response.model_response, + response + .debug_info + .as_ref() + .map(|p| p.model_response.as_str()) + .unwrap_or( + "(Debug info not available)", + ), cx, ); buffer.set_language(markdown_language, cx); @@ -471,6 +543,9 @@ impl Zeta2Inspector { editor.set_show_scrollbars(false, cx); editor }), + feedback_editor, + feedback: None, + response, } } Ok(Err(err)) => { @@ -486,6 +561,8 @@ impl Zeta2Inspector { } }); + let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx); + this.last_prediction = Some(LastPrediction { context_editor, prompt_editor: cx.new(|cx| { @@ -508,6 +585,11 @@ impl Zeta2Inspector { buffer, position, state: LastPredictionState::Requested, + project_snapshot: cx + .foreground_executor() + .spawn(async move { Arc::new(project_snapshot_task.await) }) + .shared(), + request: prediction.request, _task: Some(task), }); cx.notify(); @@ -517,6 +599,103 @@ impl Zeta2Inspector { }); } + fn handle_rate_positive( + &mut self, + _action: &Zeta2RatePredictionPositive, + window: &mut Window, + cx: &mut Context, + ) { + self.handle_rate(Feedback::Positive, window, cx); + } + + fn handle_rate_negative( + &mut self, + _action: &Zeta2RatePredictionNegative, + window: &mut Window, + cx: &mut Context, + ) { + self.handle_rate(Feedback::Negative, window, cx); + } + + fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context) { + let Some(last_prediction) = self.last_prediction.as_mut() else { + return; + }; + if !last_prediction.request.can_collect_data { + return; + } + + let project_snapshot_task = last_prediction.project_snapshot.clone(); + + cx.spawn_in(window, async move |this, cx| { + let project_snapshot = project_snapshot_task.await; + this.update_in(cx, |this, window, cx| { + let Some(last_prediction) = this.last_prediction.as_mut() else { + return; + }; + + let LastPredictionState::Success { + feedback: feedback_state, + feedback_editor, + model_response_editor, + response, + .. + } = &mut last_prediction.state + else { + return; + }; + + *feedback_state = Some(kind); + let text = feedback_editor.update(cx, |feedback_editor, cx| { + feedback_editor.set_placeholder_text( + "Submitted. Edit or submit again to change.", + window, + cx, + ); + feedback_editor.text(cx) + }); + cx.notify(); + + cx.defer_in(window, { + let model_response_editor = model_response_editor.downgrade(); + move |_, window, cx| { + if let Some(model_response_editor) = model_response_editor.upgrade() { + model_response_editor.focus_handle(cx).focus(window); + } + } + }); + + let kind = match kind { + Feedback::Positive => "positive", + Feedback::Negative => "negative", + }; + + telemetry::event!( + "Zeta2 Prediction Rated", + id = response.request_id, + kind = kind, + text = text, + request = last_prediction.request, + response = response, + project_snapshot = project_snapshot, + ); + }) + .log_err(); + }) + .detach(); + } + + fn focus_feedback(&mut self, window: &mut Window, cx: &mut Context) { + if let Some(last_prediction) = self.last_prediction.as_mut() { + if let LastPredictionState::Success { + feedback_editor, .. + } = &mut last_prediction.state + { + feedback_editor.focus_handle(cx).focus(window); + } + }; + } + fn render_options(&self, window: &mut Window, cx: &mut Context) -> Div { v_flex() .gap_2() @@ -618,8 +797,9 @@ impl Zeta2Inspector { ), ui::ToggleButtonSimple::new( "Inference", - cx.listener(|this, _, _, cx| { + cx.listener(|this, _, window, cx| { this.active_view = ActiveView::Inference; + this.focus_feedback(window, cx); cx.notify(); }), ), @@ -640,21 +820,24 @@ impl Zeta2Inspector { return None; }; - let (prompt_planning_time, inference_time, parsing_time) = match &prediction.state { - LastPredictionState::Success { - inference_time, - parsing_time, - prompt_planning_time, + let (prompt_planning_time, inference_time, parsing_time) = + if let LastPredictionState::Success { + response: + PredictEditsResponse { + debug_info: Some(debug_info), + .. + }, .. - } => ( - Some(*prompt_planning_time), - Some(*inference_time), - Some(*parsing_time), - ), - LastPredictionState::Requested | LastPredictionState::Failed { .. } => { + } = &prediction.state + { + ( + Some(debug_info.prompt_planning_time), + Some(debug_info.inference_time), + Some(debug_info.parsing_time), + ) + } else { (None, None, None) - } - }; + }; Some( v_flex() @@ -690,14 +873,16 @@ impl Zeta2Inspector { }) } - fn render_content(&self, cx: &mut Context) -> AnyElement { + fn render_content(&self, window: &mut Window, cx: &mut Context) -> AnyElement { if !cx.has_flag::() { return Self::render_message("`zeta2` feature flag is not enabled"); } match self.last_prediction.as_ref() { None => Self::render_message("No prediction"), - Some(prediction) => self.render_last_prediction(prediction, cx).into_any(), + Some(prediction) => self + .render_last_prediction(prediction, window, cx) + .into_any(), } } @@ -710,7 +895,12 @@ impl Zeta2Inspector { .into_any() } - fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context) -> Div { + fn render_last_prediction( + &self, + prediction: &LastPrediction, + window: &mut Window, + cx: &mut Context, + ) -> Div { match &self.active_view { ActiveView::Context => div().size_full().child(prediction.context_editor.clone()), ActiveView::Inference => h_flex() @@ -748,24 +938,107 @@ impl Zeta2Inspector { .flex_1() .gap_2() .h_full() - .p_4() - .child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall)) - .child(match &prediction.state { - LastPredictionState::Success { - model_response_editor, - .. - } => model_response_editor.clone().into_any_element(), - LastPredictionState::Requested => v_flex() - .p_4() + .child( + v_flex() + .flex_1() .gap_2() - .child(Label::new("Loading...").buffer_font(cx)) - .into_any(), - LastPredictionState::Failed { message } => v_flex() .p_4() - .gap_2() - .child(Label::new(message.clone()).buffer_font(cx)) - .into_any(), - }), + .child( + ui::Headline::new("Model Response") + .size(ui::HeadlineSize::XSmall), + ) + .child(match &prediction.state { + LastPredictionState::Success { + model_response_editor, + .. + } => model_response_editor.clone().into_any_element(), + LastPredictionState::Requested => v_flex() + .gap_2() + .child(Label::new("Loading...").buffer_font(cx)) + .into_any_element(), + LastPredictionState::Failed { message } => v_flex() + .gap_2() + .max_w_96() + .child(Label::new(message.clone()).buffer_font(cx)) + .into_any_element(), + }), + ) + .child(ui::divider()) + .child( + if prediction.request.can_collect_data + && let LastPredictionState::Success { + feedback_editor, + feedback: feedback_state, + .. + } = &prediction.state + { + v_flex() + .key_context("Zeta2Feedback") + .on_action(cx.listener(Self::handle_rate_positive)) + .on_action(cx.listener(Self::handle_rate_negative)) + .gap_2() + .p_2() + .child(feedback_editor.clone()) + .child( + h_flex() + .justify_end() + .w_full() + .child( + ButtonLike::new("rate-positive") + .when( + *feedback_state == Some(Feedback::Positive), + |this| this.style(ButtonStyle::Filled), + ) + .children( + KeyBinding::for_action( + &Zeta2RatePredictionPositive, + window, + cx, + ) + .map(|k| k.size(TextSize::Small.rems(cx))), + ) + .child(ui::Icon::new(ui::IconName::ThumbsUp)) + .on_click(cx.listener( + |this, _, window, cx| { + this.handle_rate_positive( + &Zeta2RatePredictionPositive, + window, + cx, + ); + }, + )), + ) + .child( + ButtonLike::new("rate-negative") + .when( + *feedback_state == Some(Feedback::Negative), + |this| this.style(ButtonStyle::Filled), + ) + .children( + KeyBinding::for_action( + &Zeta2RatePredictionNegative, + window, + cx, + ) + .map(|k| k.size(TextSize::Small.rems(cx))), + ) + .child(ui::Icon::new(ui::IconName::ThumbsDown)) + .on_click(cx.listener( + |this, _, window, cx| { + this.handle_rate_negative( + &Zeta2RatePredictionNegative, + window, + cx, + ); + }, + )), + ), + ) + .into_any() + } else { + Empty.into_any_element() + }, + ), ), } } @@ -808,7 +1081,7 @@ impl Render for Zeta2Inspector { .child(ui::vertical_divider()) .children(self.render_stats()), ) - .child(self.render_content(cx)) + .child(self.render_content(window, cx)) } } From 12d912114fb60681a9b82e28a21eec47a004f582 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 21 Oct 2025 07:43:22 -0300 Subject: [PATCH 097/202] ci: Update `typos` versions and fix new occurrences (#40784) I noticed we had some typos that were getting through CI, but it looks like the new version of `typos` catches them. So I updated it and fixed them. Release Notes: - N/A --- .cargo/ci-config.toml | 2 +- .github/workflows/ci.yml | 2 +- crates/audio/src/rodio_ext.rs | 2 +- crates/client/src/proxy/socks_proxy.rs | 2 +- crates/clock/src/clock.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 2 +- crates/edit_prediction_context/src/syntax_index.rs | 2 +- crates/gpui/src/app/test_context.rs | 2 +- crates/language_model/src/request.rs | 8 ++++---- crates/workspace/src/notifications.rs | 6 +++--- docs/src/languages/yaml.md | 2 +- script/bundle-linux | 2 +- script/bundle-mac | 2 +- tooling/perf/src/implementation.rs | 2 +- 14 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.cargo/ci-config.toml b/.cargo/ci-config.toml index a18554fd6e119010c5692da0be62428d3ea2a342..b31b79a59b262a5cc18cf1d2b32124a97bab4fc7 100644 --- a/.cargo/ci-config.toml +++ b/.cargo/ci-config.toml @@ -5,7 +5,7 @@ # Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure # The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file # we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml` -# would be incovenient. +# would be inconvenient. # The reason for not using the RUSTFLAGS environment variable is that doing so would override all the settings in the config.toml file, even if the contents of the latter are completely nonsensical. See: https://github.com/rust-lang/cargo/issues/5376 # Here, we opted to use `[target.'cfg(all())']` instead of `[build]` because `[target.'**']` is guaranteed to be cumulative. [target.'cfg(all())'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a56f028efc7a94c4b80e10db70d977fafe7c7638..2ebbcaba49823787aafe40e5f3dd80eb67478b42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: uses: ./.github/actions/check_style - name: Check for typos - uses: crate-ci/typos@8e6a4285bcbde632c5d79900a7779746e8b7ea3f # v1.24.6 + uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1 # v1.38.1 with: config: ./typos.toml diff --git a/crates/audio/src/rodio_ext.rs b/crates/audio/src/rodio_ext.rs index af4cc89252dfdc1498471ec7ac09b56d59b62eca..ab74c59fe6661cecab7ec9611dd0b9aa9e7f5aa7 100644 --- a/crates/audio/src/rodio_ext.rs +++ b/crates/audio/src/rodio_ext.rs @@ -433,7 +433,7 @@ where /// Stores already emitted samples, once its full we call the callback. buffer: [Sample; N], /// Next free element in buffer. If this is equal to the buffer length - /// we have no more free lements. + /// we have no more free elements. free: usize, } diff --git a/crates/client/src/proxy/socks_proxy.rs b/crates/client/src/proxy/socks_proxy.rs index 9ccf4906d8efb4d88b6167ed2a46a44df22906a2..bf2a5eab627cbae0f0ac965b3e379e6f388aaa70 100644 --- a/crates/client/src/proxy/socks_proxy.rs +++ b/crates/client/src/proxy/socks_proxy.rs @@ -23,7 +23,7 @@ pub(super) struct Socks5Authorization<'a> { /// Socks Proxy Protocol Version /// -/// V4 allows idenfication using a user_id +/// V4 allows identification using a user_id /// V5 allows authorization using a username and password pub(super) enum SocksVersion<'a> { V4 { diff --git a/crates/clock/src/clock.rs b/crates/clock/src/clock.rs index 04a7c7c881bf47bf901bc9738deafda18ab0ff21..bec98d9bfbc19b7e8ca72b97c5748d3efce4dcf6 100644 --- a/crates/clock/src/clock.rs +++ b/crates/clock/src/clock.rs @@ -83,7 +83,7 @@ impl Global { self.values.get(replica_id.0 as usize).copied().unwrap_or(0) as Seq } - /// Observe the lamport timestampe. + /// Observe the lamport timestamp. /// /// This sets the current sequence number of the observed replica ID to the maximum of this global's observed sequence and the observed timestamp. pub fn observe(&mut self, timestamp: Lamport) { diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 6e6a815a0b4c0fc8e8e2f367738a60aea604b5e1..6a41f84697e17d85a0a9777a9285dad691d73176 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -505,7 +505,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu label: "third_method(…)".into(), detail: Some("fn(&mut self, B, C, D) -> E".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - // no snippet placehodlers + // no snippet placeholders new_text: "third_method".to_string(), range: lsp::Range::new( lsp::Position::new(1, 32), diff --git a/crates/edit_prediction_context/src/syntax_index.rs b/crates/edit_prediction_context/src/syntax_index.rs index e2728ebfc029c7c1b74a35f2e6f5a79003a9a77e..76aa10c076d95aa10bd830bace23ad7b410d8102 100644 --- a/crates/edit_prediction_context/src/syntax_index.rs +++ b/crates/edit_prediction_context/src/syntax_index.rs @@ -854,7 +854,7 @@ mod tests { } #[gpui::test] - async fn test_declarations_limt(cx: &mut TestAppContext) { + async fn test_declarations_limit(cx: &mut TestAppContext) { let (_, index, rust_lang_id) = init_test(cx).await; let index_state = index.read_with(cx, |index, _cx| index.state().clone()); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index cba6de6e31901a43b7c80cff8460af6e0e6a09cc..d974823396d9f0d546a6b035f47b569145eb021b 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -836,7 +836,7 @@ impl VisualTestContext { }) } - /// Simulate an event from the platform, e.g. a SrollWheelEvent + /// Simulate an event from the platform, e.g. a ScrollWheelEvent /// Make sure you've called [VisualTestContext::draw] first! pub fn simulate_event(&mut self, event: E) { self.test_window(self.window) diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index 2902e9ae5aaa45ea4607317bee12a3f91abbbe55..d0f7789e40dd71ada8dcae2712cefcef966ad52f 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -77,7 +77,7 @@ impl std::fmt::Debug for LanguageModelImage { } /// Anthropic wants uploaded images to be smaller than this in both dimensions. -const ANTHROPIC_SIZE_LIMT: f32 = 1568.; +const ANTHROPIC_SIZE_LIMIT: f32 = 1568.; impl LanguageModelImage { pub fn empty() -> Self { @@ -112,13 +112,13 @@ impl LanguageModelImage { let image_size = size(DevicePixels(width as i32), DevicePixels(height as i32)); let base64_image = { - if image_size.width.0 > ANTHROPIC_SIZE_LIMT as i32 - || image_size.height.0 > ANTHROPIC_SIZE_LIMT as i32 + if image_size.width.0 > ANTHROPIC_SIZE_LIMIT as i32 + || image_size.height.0 > ANTHROPIC_SIZE_LIMIT as i32 { let new_bounds = ObjectFit::ScaleDown.get_bounds( gpui::Bounds { origin: point(px(0.0), px(0.0)), - size: size(px(ANTHROPIC_SIZE_LIMT), px(ANTHROPIC_SIZE_LIMT)), + size: size(px(ANTHROPIC_SIZE_LIMIT), px(ANTHROPIC_SIZE_LIMIT)), }, image_size, ); diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 768b10abe4f3973ca6424b7b59b4e3bfb44cbb15..1a0dd2f2c8416ec604ef74d2f3c4908eb0ddb57f 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -499,7 +499,7 @@ impl NotificationFrame { } /// Determines whether the given notification ID should be suppressible - /// Suppressed motifications will not be shown anymore + /// Suppressed notifications will not be shown anymore pub fn show_suppress_button(mut self, show: bool) -> Self { self.show_suppress_button = show; self @@ -761,8 +761,8 @@ pub mod simple_message_notification { self } - /// Determines whether the given notification ID should be supressable - /// Suppressed motifications will not be shown anymor + /// Determines whether the given notification ID should be suppressible + /// Suppressed notifications will not be shown anymor pub fn show_suppress_button(mut self, show: bool) -> Self { self.show_suppress_button = show; self diff --git a/docs/src/languages/yaml.md b/docs/src/languages/yaml.md index e4845e363671597281cf6cd2d26721cdc1e856fb..477d197d11fa4f0ad0e62ee25e416eee7c35ee67 100644 --- a/docs/src/languages/yaml.md +++ b/docs/src/languages/yaml.md @@ -74,7 +74,7 @@ You can override any auto-detected schema via the `schemas` settings key (demons name: Issue Assignment on: issues: - types: [oppened] + types: [opened] ``` You can disable the automatic detection and retrieval of schemas from the JSON Schema if desired: diff --git a/script/bundle-linux b/script/bundle-linux index ad67b7a0f75f8c3e5d22e1a12e175ed248ecaf57..e8263fe4bcc8a90073149bf3a02ff1ed481017c3 100755 --- a/script/bundle-linux +++ b/script/bundle-linux @@ -91,7 +91,7 @@ else if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then echo "Uploading zed debug symbols to sentry..." # note: this uploads the unstripped binary which is needed because it contains - # .eh_frame data for stack unwinindg. see https://github.com/getsentry/symbolic/issues/783 + # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783 sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \ "${target_dir}/${target_triple}"/release/zed \ "${target_dir}/${remote_server_triple}"/release/remote_server diff --git a/script/bundle-mac b/script/bundle-mac index 0bac0f75ee5ade49eba842257e854420f9bca82f..abcdb6cee2e6b35bcc185a40b6ad459dd98389fb 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -375,7 +375,7 @@ function upload_debug_info() { if [[ -n "${SENTRY_AUTH_TOKEN:-}" ]]; then echo "Uploading zed debug symbols to sentry..." # note: this uploads the unstripped binary which is needed because it contains - # .eh_frame data for stack unwinindg. see https://github.com/getsentry/symbolic/issues/783 + # .eh_frame data for stack unwinding. see https://github.com/getsentry/symbolic/issues/783 sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev \ "target/${architecture}/${target_dir}/zed" \ "target/${architecture}/${target_dir}/remote_server" \ diff --git a/tooling/perf/src/implementation.rs b/tooling/perf/src/implementation.rs index 535f25a2b312133d5ef446a668b50f2bcdef7489..c151dda91f0bec64e261738ea593b233dedd9b62 100644 --- a/tooling/perf/src/implementation.rs +++ b/tooling/perf/src/implementation.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use std::{num::NonZero, time::Duration}; pub mod consts { - //! Preset idenitifiers and constants so that the profiler and proc macro agree + //! Preset identifiers and constants so that the profiler and proc macro agree //! on their communication protocol. /// The suffix on the actual test function. From 2bfbe031c66523b3a8483801bde97d75a49e2857 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:50:56 +0200 Subject: [PATCH 098/202] python: Bump version & get rid of explicit deps specifications for PET (#40785) Release Notes: - N/A --- Cargo.lock | 48 +++++----- Cargo.toml | 16 ++-- script/licenses/zed-licenses.toml | 144 ------------------------------ 3 files changed, 32 insertions(+), 176 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 430a52d0f3d41523c737dbcd6ecf4e0e5a9424fd..480358bff1428404d28197df55b2566c098a373f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11387,7 +11387,7 @@ dependencies = [ [[package]] name = "pet" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "clap", "env_logger 0.10.2", @@ -11424,7 +11424,7 @@ dependencies = [ [[package]] name = "pet-conda" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "env_logger 0.10.2", "lazy_static", @@ -11443,7 +11443,7 @@ dependencies = [ [[package]] name = "pet-core" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "clap", "lazy_static", @@ -11458,7 +11458,7 @@ dependencies = [ [[package]] name = "pet-env-var-path" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "lazy_static", "log", @@ -11474,7 +11474,7 @@ dependencies = [ [[package]] name = "pet-fs" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11483,7 +11483,7 @@ dependencies = [ [[package]] name = "pet-global-virtualenvs" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11496,7 +11496,7 @@ dependencies = [ [[package]] name = "pet-homebrew" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "lazy_static", "log", @@ -11514,7 +11514,7 @@ dependencies = [ [[package]] name = "pet-jsonrpc" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "env_logger 0.10.2", "log", @@ -11527,7 +11527,7 @@ dependencies = [ [[package]] name = "pet-linux-global-python" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11540,7 +11540,7 @@ dependencies = [ [[package]] name = "pet-mac-commandlinetools" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11553,7 +11553,7 @@ dependencies = [ [[package]] name = "pet-mac-python-org" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11566,7 +11566,7 @@ dependencies = [ [[package]] name = "pet-mac-xcode" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11579,7 +11579,7 @@ dependencies = [ [[package]] name = "pet-pipenv" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11592,7 +11592,7 @@ dependencies = [ [[package]] name = "pet-pixi" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11604,7 +11604,7 @@ dependencies = [ [[package]] name = "pet-poetry" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "base64 0.22.1", "lazy_static", @@ -11625,7 +11625,7 @@ dependencies = [ [[package]] name = "pet-pyenv" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "lazy_static", "log", @@ -11643,7 +11643,7 @@ dependencies = [ [[package]] name = "pet-python-utils" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "env_logger 0.10.2", "lazy_static", @@ -11660,7 +11660,7 @@ dependencies = [ [[package]] name = "pet-reporter" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "env_logger 0.10.2", "log", @@ -11674,7 +11674,7 @@ dependencies = [ [[package]] name = "pet-telemetry" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "env_logger 0.10.2", "lazy_static", @@ -11689,7 +11689,7 @@ dependencies = [ [[package]] name = "pet-venv" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11701,7 +11701,7 @@ dependencies = [ [[package]] name = "pet-virtualenv" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11713,7 +11713,7 @@ dependencies = [ [[package]] name = "pet-virtualenvwrapper" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "log", "msvc_spectre_libs", @@ -11726,7 +11726,7 @@ dependencies = [ [[package]] name = "pet-windows-registry" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "lazy_static", "log", @@ -11744,7 +11744,7 @@ dependencies = [ [[package]] name = "pet-windows-store" version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=845945b830297a50de0e24020b980a65e4820559#845945b830297a50de0e24020b980a65e4820559" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=e97b9508befa0062929da65a01054d25c4be861c#e97b9508befa0062929da65a01054d25c4be861c" dependencies = [ "lazy_static", "log", diff --git a/Cargo.toml b/Cargo.toml index 33cbacf8040441e8a7df20452d625aef74f5e9d6..f9eecd25cb11701bdc1f22d8b6d79cd2a572e296 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -582,14 +582,14 @@ partial-json-fixer = "0.5.3" parse_int = "0.9" pciid-parser = "0.8.0" pathdiff = "0.2" -pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } -pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } +pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-pixi = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } +pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "e97b9508befa0062929da65a01054d25c4be861c" } portable-pty = "0.9.0" postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = { version = "1.3.0", features = ["unstable"] } diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml index fe007501e4fd5b8a2bd400bc3ff9e648c0c2827e..4f7281a050863b26c6e012acbf116cecadcb4269 100644 --- a/script/licenses/zed-licenses.toml +++ b/script/licenses/zed-licenses.toml @@ -34,147 +34,3 @@ license = "BSD-3-Clause" [[fuchsia-cprng.clarify.files]] path = 'LICENSE' checksum = '03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b' - -[pet.clarify] -license = "MIT" -[[pet.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-conda.clarify] -license = "MIT" -[[pet-conda.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-core.clarify] -license = "MIT" -[[pet-core.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-env-var-path.clarify] -license = "MIT" -[[pet-env-var-path.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-fs.clarify] -license = "MIT" -[[pet-fs.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-global-virtualenvs.clarify] -license = "MIT" -[[pet-global-virtualenvs.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-homebrew.clarify] -license = "MIT" -[[pet-homebrew.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-jsonrpc.clarify] -license = "MIT" -[[pet-jsonrpc.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-linux-global-python.clarify] -license = "MIT" -[[pet-linux-global-python.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-mac-commandlinetools.clarify] -license = "MIT" -[[pet-mac-commandlinetools.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-mac-python-org.clarify] -license = "MIT" -[[pet-mac-python-org.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-mac-xcode.clarify] -license = "MIT" -[[pet-mac-xcode.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-pipenv.clarify] -license = "MIT" -[[pet-pipenv.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-pixi.clarify] -license = "MIT" -[[pet-pixi.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-poetry.clarify] -license = "MIT" -[[pet-poetry.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-pyenv.clarify] -license = "MIT" -[[pet-pyenv.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-python-utils.clarify] -license = "MIT" -[[pet-python-utils.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-reporter.clarify] -license = "MIT" -[[pet-reporter.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-telemetry.clarify] -license = "MIT" -[[pet-telemetry.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-venv.clarify] -license = "MIT" -[[pet-venv.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-virtualenv.clarify] -license = "MIT" -[[pet-virtualenv.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-virtualenvwrapper.clarify] -license = "MIT" -[[pet-virtualenvwrapper.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-windows-registry.clarify] -license = "MIT" -[[pet-windows-registry.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' - -[pet-windows-store.clarify] -license = "MIT" -[[pet-windows-store.clarify.files]] -path = '../../LICENSE' -checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' From 3d4abde55afa6f01bc9f2451e49af3aeb40a89c9 Mon Sep 17 00:00:00 2001 From: Dino Date: Tue, 21 Oct 2025 12:23:00 +0100 Subject: [PATCH 099/202] vim: Fix hang in visual block motion (#40723) The `vim::visual::Vim.visual_block_motion` method was recently updated (https://github.com/zed-industries/zed/pull/39355) in order to jump between buffer rows instead of display rows. However, with this now being the case, the `break` condition was never met when the motion was horizontal rather than vertical and soft wrapped lines were used. As such, this commit udpates the condition to ensure it's always reached, preventing the hanging from happening. Release Notes: - Fixed hang in Vim's visual block motions when updating selections --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/vim/src/visual.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index bce49eb7d4a3c21b00a8076f0474d9da591cc993..59555205d9862e51c2778eec1f321338fd5e7569 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -366,6 +366,8 @@ impl Vim { let mut selections = Vec::new(); let mut row = tail.row(); + let going_up = tail.row() > head.row(); + let direction = if going_up { -1 } else { 1 }; loop { let laid_out_line = map.layout_row(row, &text_layout_details); @@ -396,13 +398,18 @@ impl Vim { selections.push(selection); } - if row == head.row() { + + // When dealing with soft wrapped lines, it's possible that + // `row` ends up being set to a value other than `head.row()` as + // `head.row()` might be a `DisplayPoint` mapped to a soft + // wrapped line, hence the need for `<=` and `>=` instead of + // `==`. + if going_up && row <= head.row() || !going_up && row >= head.row() { break; } - // Move to the next or previous buffer row, ensuring that - // wrapped lines are handled correctly. - let direction = if tail.row() > head.row() { -1 } else { 1 }; + // Find the next or previous buffer row where the `row` should + // be moved to, so that wrapped lines are skipped. row = map .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction) .row(); From 0be70e24d66a5dd2d9ac58183fdd848b885f355f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 21 Oct 2025 13:29:43 +0200 Subject: [PATCH 100/202] persistence: More error contexts (#40787) Release Notes: - N/A *or* Added/Fixed/Improved ... Co-authored-by: David Kleingeld --- crates/editor/src/items.rs | 17 +++++++++++++---- crates/project/src/buffer_store.rs | 7 +++++-- crates/workspace/src/persistence/model.rs | 3 ++- crates/worktree/src/worktree.rs | 6 +++--- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 708efbbe979dd153dbafde265e56bc2ddd725f76..f8e8f1c3750f8c612febc6b8a08b3106a2efaed0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1079,12 +1079,17 @@ impl SerializableItem for Editor { } } Ok(None) => { - return Task::ready(Err(anyhow!("No path or contents found for buffer"))); + return Task::ready(Err(anyhow!( + "Unable to deserialize editor: No entry in database for item_id: {item_id} and workspace_id {workspace_id:?}" + ))); } Err(error) => { return Task::ready(Err(error)); } }; + log::debug!( + "Deserialized editor {item_id:?} in workspace {workspace_id:?}, {serialized_editor:?}" + ); match serialized_editor { SerializedEditor { @@ -1112,7 +1117,8 @@ impl SerializableItem for Editor { // First create the empty buffer let buffer = project .update(cx, |project, cx| project.create_buffer(true, cx))? - .await?; + .await + .context("Failed to create buffer while deserializing editor")?; // Then set the text so that the dirty bit is set correctly buffer.update(cx, |buffer, cx| { @@ -1154,7 +1160,9 @@ impl SerializableItem for Editor { match opened_buffer { Some(opened_buffer) => { window.spawn(cx, async move |cx| { - let (_, buffer) = opened_buffer.await?; + let (_, buffer) = opened_buffer + .await + .context("Failed to open path in project")?; // This is a bit wasteful: we're loading the whole buffer from // disk and then overwrite the content. @@ -1220,7 +1228,8 @@ impl SerializableItem for Editor { } => window.spawn(cx, async move |cx| { let buffer = project .update(cx, |project, cx| project.create_buffer(true, cx))? - .await?; + .await + .context("Failed to create buffer")?; cx.update(|window, cx| { cx.new(|cx| { diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index b36b44cfd4d0b3f73e808becf5168cd47f7e4c06..51bca611f4b69546cc358cb59724dbb7f98d219e 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -24,7 +24,7 @@ use rpc::{ use std::{io, sync::Arc, time::Instant}; use text::{BufferId, ReplicaId}; -use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath}; +use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath}; use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; /// A set of open buffers. @@ -621,8 +621,11 @@ impl LocalBufferStore { let load_file = worktree.load_file(path.as_ref(), cx); let reservation = cx.reserve_entity(); let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); + let path = path.clone(); cx.spawn(async move |_, cx| { - let loaded = load_file.await?; + let loaded = load_file.await.with_context(|| { + format!("Could not open path: {}", path.display(PathStyle::local())) + })?; let text_buffer = cx .background_spawn(async move { text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 08a2f2e38dd142848f8a9c07652e147b58bee233..a37b2ebbe93efb23cad6a98f127ba1f8800a3eb3 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -3,7 +3,7 @@ use crate::{ Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, path_list::PathList, }; -use anyhow::Result; +use anyhow::{Context, Result}; use async_recursion::async_recursion; use collections::IndexSet; use db::sqlez::{ @@ -220,6 +220,7 @@ impl SerializedPaneGroup { let new_items = serialized_pane .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx) .await + .context("Could not deserialize pane)") .log_err()?; if pane diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index f889fb3b8218b18983c4509b653cf0e0ce863fcd..5f8253e2dfb48fa6882dabf49c64073023a2a298 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -1706,9 +1706,9 @@ impl LocalWorktree { refresh.recv().await; log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed()); let new_entry = this.read_with(cx, |this, _| { - this.entry_for_path(&path) - .cloned() - .context("reading path after update") + this.entry_for_path(&path).cloned().with_context(|| { + format!("Could not find entry in worktree for {path:?} after refresh") + }) })??; Ok(Some(new_entry)) }) From 0eccdfe61fce5b75c62f01b459c6c882143b7ac7 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 21 Oct 2025 15:10:21 +0200 Subject: [PATCH 101/202] project: Spawn terminal process on background executor (#40774) We were spawning the process on the foreground thread before which can block an arbitrary amount of time. Likewise we no longer block deserialization on the terminal loading. Release Notes: - Improved startup time on systems with slow process spawning capabilities --- crates/agent_ui/src/agent_diff.rs | 6 +- crates/collab/src/tests/following_tests.rs | 44 +- crates/collab/src/tests/integration_tests.rs | 2 +- crates/collab_ui/src/channel_view.rs | 6 +- crates/diagnostics/src/buffer_diagnostics.rs | 6 +- crates/diagnostics/src/diagnostics.rs | 6 +- crates/editor/src/items.rs | 4 +- crates/git_ui/src/commit_view.rs | 10 +- crates/git_ui/src/project_diff.rs | 10 +- crates/image_viewer/src/image_viewer.rs | 6 +- crates/language_tools/src/key_context_view.rs | 7 +- crates/language_tools/src/lsp_log_view.rs | 8 +- crates/language_tools/src/syntax_tree_view.rs | 8 +- crates/onboarding/src/onboarding.rs | 6 +- crates/project/src/terminals.rs | 355 ++++++------ crates/repl/src/notebook/notebook_ui.rs | 6 +- crates/search/src/project_search.rs | 8 +- crates/terminal/src/terminal.rs | 531 +++++++++--------- crates/terminal_view/src/persistence.rs | 102 ++-- crates/terminal_view/src/terminal_panel.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 45 +- crates/workspace/src/item.rs | 26 +- crates/workspace/src/pane.rs | 17 +- crates/workspace/src/shared_screen.rs | 8 +- crates/workspace/src/theme_preview.rs | 8 +- crates/workspace/src/workspace.rs | 102 ++-- crates/zed/src/zed.rs | 20 +- crates/zed/src/zed/component_preview.rs | 6 +- 28 files changed, 727 insertions(+), 640 deletions(-) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index c2b624ecfca2bed4c9480bf681fe17b43b569685..e463c0eb48816021bc2665d385804c926f1c63f4 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -581,11 +581,13 @@ impl Item for AgentDiffPane { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx))) + Task::ready(Some(cx.new(|cx| { + Self::new(self.thread.clone(), self.workspace.clone(), window, cx) + }))) } fn is_dirty(&self, cx: &App) -> bool { diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index ab72ce3605b7c93bac05dc6321b44b7abb964d93..07cf866a3513d27894307216e904b130eb023e22 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -776,26 +776,30 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T .unwrap(); // Clients A and B follow each other in split panes - workspace_a.update_in(cx_a, |workspace, window, cx| { - workspace.split_and_clone( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ); - }); + workspace_a + .update_in(cx_a, |workspace, window, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ) + }) + .await; workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.follow(client_b.peer_id().unwrap(), window, cx) }); executor.run_until_parked(); - workspace_b.update_in(cx_b, |workspace, window, cx| { - workspace.split_and_clone( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ); - }); + workspace_b + .update_in(cx_b, |workspace, window, cx| { + workspace.split_and_clone( + workspace.active_pane().clone(), + SplitDirection::Right, + window, + cx, + ) + }) + .await; workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.follow(client_a.peer_id().unwrap(), window, cx) }); @@ -1369,9 +1373,11 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont ); // When client B activates a different pane, it continues following client A in the original pane. - workspace_b.update_in(cx_b, |workspace, window, cx| { - workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx) - }); + workspace_b + .update_in(cx_b, |workspace, window, cx| { + workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, window, cx) + }) + .await; assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), Some(leader_id.into()) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index f080b1cc4a56a7597115a35aac3c329f9039421c..cc2c01b7857a1efefd88b47d2ea199fc571051ea 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6748,7 +6748,7 @@ async fn test_preview_tabs(cx: &mut TestAppContext) { pane.update(cx, |pane, cx| { pane.split(workspace::SplitDirection::Right, cx); }); - + cx.run_until_parked(); let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); pane.update(cx, |pane, cx| { diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 4e4bd2ca958d20225a7188b1f7f601e879e22835..18847363bf1b012acb7916bb7b6a9c0adde4de28 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -498,8 +498,8 @@ impl Item for ChannelView { _: Option, window: &mut Window, cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| { + ) -> Task>> { + Task::ready(Some(cx.new(|cx| { Self::new( self.project.clone(), self.workspace.clone(), @@ -508,7 +508,7 @@ impl Item for ChannelView { window, cx, ) - })) + }))) } fn navigate( diff --git a/crates/diagnostics/src/buffer_diagnostics.rs b/crates/diagnostics/src/buffer_diagnostics.rs index e25d3a7702e02c93e38f5808434aff57e743defe..1a7a97c68691d7bdf941322660eddeb70fa15037 100644 --- a/crates/diagnostics/src/buffer_diagnostics.rs +++ b/crates/diagnostics/src/buffer_diagnostics.rs @@ -693,11 +693,11 @@ impl Item for BufferDiagnosticsEditor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| { + Task::ready(Some(cx.new(|cx| { BufferDiagnosticsEditor::new( self.project_path.clone(), self.project.clone(), @@ -706,7 +706,7 @@ impl Item for BufferDiagnosticsEditor { window, cx, ) - })) + }))) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 47e2a9539b7e362bb9b968d8e39cda30d3f17e78..b96e8f891fda3cc470c7091eba8e92847b59562b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -732,11 +732,11 @@ impl Item for ProjectDiagnosticsEditor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| { + Task::ready(Some(cx.new(|cx| { ProjectDiagnosticsEditor::new( self.include_warnings, self.project.clone(), @@ -744,7 +744,7 @@ impl Item for ProjectDiagnosticsEditor { window, cx, ) - })) + }))) } fn is_dirty(&self, cx: &App) -> bool { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f8e8f1c3750f8c612febc6b8a08b3106a2efaed0..3a037899f4273c2af8b1f54aa704d9850d76243e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -762,11 +762,11 @@ impl Item for Editor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| self.clone(window, cx))) + Task::ready(Some(cx.new(|cx| self.clone(window, cx)))) } fn set_nav_history( diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index 9738e13984a0b032b09a218990f3466052e9fa61..2430796c89f68e9b5032c3d05f7106b6f6de0bec 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -4,8 +4,8 @@ use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_con use git::repository::{CommitDetails, CommitDiff, RepoPath}; use gpui::{ Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, - Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity, - Window, actions, + Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task, + WeakEntity, Window, actions, }; use language::{ Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _, @@ -561,11 +561,11 @@ impl Item for CommitView { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| { + Task::ready(Some(cx.new(|cx| { let editor = cx.new(|cx| { self.editor .update(cx, |editor, cx| editor.clone(window, cx)) @@ -577,7 +577,7 @@ impl Item for CommitView { commit: self.commit.clone(), stash: self.stash, } - })) + }))) } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 5cd2e5e3b07bb8bf503cada7f0ca2f8dd0388a4e..f4ee0b8934ae63433eb5d94d52e213b4458c76cd 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -625,12 +625,16 @@ impl Item for ProjectDiff { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - let workspace = self.workspace.upgrade()?; - Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx))) + let Some(workspace) = self.workspace.upgrade() else { + return Task::ready(None); + }; + Task::ready(Some(cx.new(|cx| { + ProjectDiff::new(self.project.clone(), workspace, window, cx) + }))) } fn is_dirty(&self, cx: &App) -> bool { diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index ff68de39e87c7246e0aa07e96a4d0d7e3c2ad523..fab0166efea5d4199973b66dd63f68b8bb0f2e1c 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -176,15 +176,15 @@ impl Item for ImageView { _workspace_id: Option, _: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| Self { + Task::ready(Some(cx.new(|cx| Self { image_item: self.image_item.clone(), project: self.project.clone(), focus_handle: cx.focus_handle(), - })) + }))) } fn has_deleted_file(&self, cx: &App) -> bool { diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 1d3cd451f19ce6bc28540f10b2a91a7a6319214a..7b0b71059e9998914ce511b47e26d1fd0c3abfe5 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -1,6 +1,7 @@ use gpui::{ Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, - KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, actions, + KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task, + actions, }; use itertools::Itertools; use serde_json::json; @@ -157,11 +158,11 @@ impl Item for KeyContextView { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| KeyContextView::new(window, cx))) + Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx)))) } } diff --git a/crates/language_tools/src/lsp_log_view.rs b/crates/language_tools/src/lsp_log_view.rs index e834dd6aec003930d68ed745f67aff50b2c8f66b..b1e24303c47e722460d20023d4f7444a8b006406 100644 --- a/crates/language_tools/src/lsp_log_view.rs +++ b/crates/language_tools/src/lsp_log_view.rs @@ -3,7 +3,7 @@ use copilot::Copilot; use editor::{Editor, EditorEvent, actions::MoveToEnd, scroll::Autoscroll}; use gpui::{ AnyView, App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, - ParentElement, Render, Styled, Subscription, WeakEntity, Window, actions, div, + ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, div, }; use itertools::Itertools; use language::{LanguageServerId, language_settings::SoftWrap}; @@ -763,11 +763,11 @@ impl Item for LspLogView { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| { + Task::ready(Some(cx.new(|cx| { let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), window, cx); if let Some(server_id) = self.current_server_id { match self.active_entry_kind { @@ -778,7 +778,7 @@ impl Item for LspLogView { } } new_view - })) + }))) } } diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 464d518c2e9c697d292d7bffda7ee7bae68dd254..9e5a0374f54f97fc3d75ebc68e2247bf4b904f0c 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -3,7 +3,7 @@ use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll}; use gpui::{ App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, - ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, + ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle, WeakEntity, Window, actions, div, rems, uniform_list, }; use language::{Buffer, OwnedSyntaxLayer}; @@ -573,17 +573,17 @@ impl Item for SyntaxTreeView { _: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| { + Task::ready(Some(cx.new(|cx| { let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx); if let Some(editor) = &self.editor { clone.set_editor(editor.editor.clone(), window, cx) } clone - })) + }))) } } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 9273f0d7d87851b5118d7835244074502fc128c7..e252966814f7eaa381982b4c73583f9e2b051ad2 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -385,14 +385,14 @@ impl Item for Onboarding { _workspace_id: Option, _: &mut Window, cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| Onboarding { + ) -> Task>> { + Task::ready(Some(cx.new(|cx| Onboarding { workspace: self.workspace.clone(), user_store: self.user_store.clone(), scroll_handle: ScrollHandle::new(), focus_handle: cx.focus_handle(), _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - })) + }))) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index c9594a4b55d44608fc80c2e64d8e39ebbeacee13..dd69050a53d8e5477e5bf8be5e5d3a2a86e92af5 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -144,141 +144,146 @@ impl Project { .await .unwrap_or_default(); - project.update(cx, move |this, cx| { - let format_to_run = || { - if let Some(command) = &spawn_task.command { - let mut command: Option> = shell_kind.try_quote(command); - if let Some(command) = &mut command - && command.starts_with('"') - && let Some(prefix) = shell_kind.command_prefix() - { - *command = Cow::Owned(format!("{prefix}{command}")); - } + let builder = project + .update(cx, move |_, cx| { + let format_to_run = || { + if let Some(command) = &spawn_task.command { + let mut command: Option> = shell_kind.try_quote(command); + if let Some(command) = &mut command + && command.starts_with('"') + && let Some(prefix) = shell_kind.command_prefix() + { + *command = Cow::Owned(format!("{prefix}{command}")); + } - let args = spawn_task - .args - .iter() - .filter_map(|arg| shell_kind.try_quote(&arg)); + let args = spawn_task + .args + .iter() + .filter_map(|arg| shell_kind.try_quote(&arg)); - command.into_iter().chain(args).join(" ") - } else { - // todo: this breaks for remotes to windows - format!("exec {shell} -l") - } - }; - - let (shell, env) = { - env.extend(spawn_task.env); - match remote_client { - Some(remote_client) => match activation_script.clone() { - activation_script if !activation_script.is_empty() => { - let activation_script = activation_script.join("; "); - let to_run = format_to_run(); - let args = - vec!["-c".to_owned(), format!("{activation_script}; {to_run}")]; - create_remote_shell( - Some(( - &remote_client - .read(cx) - .shell() - .unwrap_or_else(get_default_system_shell), - &args, - )), + command.into_iter().chain(args).join(" ") + } else { + // todo: this breaks for remotes to windows + format!("exec {shell} -l") + } + }; + + let (shell, env) = { + env.extend(spawn_task.env); + match remote_client { + Some(remote_client) => match activation_script.clone() { + activation_script if !activation_script.is_empty() => { + let activation_script = activation_script.join("; "); + let to_run = format_to_run(); + let args = vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}"), + ]; + create_remote_shell( + Some(( + &remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + &args, + )), + env, + path, + remote_client, + cx, + )? + } + _ => create_remote_shell( + spawn_task + .command + .as_ref() + .map(|command| (command, &spawn_task.args)), env, path, remote_client, cx, - )? - } - _ => create_remote_shell( - spawn_task - .command - .as_ref() - .map(|command| (command, &spawn_task.args)), - env, - path, - remote_client, - cx, - )?, - }, - None => match activation_script.clone() { - activation_script if !activation_script.is_empty() => { - let separator = shell_kind.sequential_commands_separator(); - let activation_script = - activation_script.join(&format!("{separator} ")); - let to_run = format_to_run(); - - let mut arg = format!("{activation_script}{separator} {to_run}"); - if shell_kind == ShellKind::Cmd { - // We need to put the entire command in quotes since otherwise CMD tries to execute them - // as separate commands rather than chaining one after another. - arg = format!("\"{arg}\""); - } + )?, + }, + None => match activation_script.clone() { + activation_script if !activation_script.is_empty() => { + let separator = shell_kind.sequential_commands_separator(); + let activation_script = + activation_script.join(&format!("{separator} ")); + let to_run = format_to_run(); + + let mut arg = + format!("{activation_script}{separator} {to_run}"); + if shell_kind == ShellKind::Cmd { + // We need to put the entire command in quotes since otherwise CMD tries to execute them + // as separate commands rather than chaining one after another. + arg = format!("\"{arg}\""); + } - let args = shell_kind.args_for_shell(false, arg); + let args = shell_kind.args_for_shell(false, arg); - ( - Shell::WithArguments { - program: shell, - args, - title_override: None, + ( + Shell::WithArguments { + program: shell, + args, + title_override: None, + }, + env, + ) + } + _ => ( + if let Some(program) = spawn_task.command { + Shell::WithArguments { + program, + args: spawn_task.args, + title_override: None, + } + } else { + Shell::System }, env, - ) - } - _ => ( - if let Some(program) = spawn_task.command { - Shell::WithArguments { - program, - args: spawn_task.args, - title_override: None, - } - } else { - Shell::System - }, - env, - ), - }, - } - }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - task_state, - shell, - env, - settings.cursor_shape, - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_via_remote, - cx.entity_id().as_u64(), - Some(completion_tx), - cx, - activation_script, - ) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); - - this.terminals - .local_handles - .push(terminal_handle.downgrade()); - - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; - - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); + ), + }, } - }) - .detach(); + }; + anyhow::Ok(TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + task_state, + shell, + env, + settings.cursor_shape, + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + Some(completion_tx), + cx, + activation_script, + )) + })?? + .await?; + project.update(cx, move |this, cx| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; - terminal_handle + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } }) - })? + .detach(); + + terminal_handle + }) }) } @@ -364,53 +369,55 @@ impl Project { }) .await .unwrap_or_default(); - project.update(cx, move |this, cx| { - let (shell, env) = { - match remote_client { - Some(remote_client) => { - create_remote_shell(None, env, path, remote_client, cx)? - } - None => (settings.shell, env), - } - }; - TerminalBuilder::new( - local_path.map(|path| path.to_path_buf()), - None, - shell, - env, - settings.cursor_shape, - settings.alternate_scroll, - settings.max_scroll_history_lines, - is_via_remote, - cx.entity_id().as_u64(), - None, - cx, - activation_script, - ) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); - - this.terminals - .local_handles - .push(terminal_handle.downgrade()); - - let id = terminal_handle.entity_id(); - cx.observe_release(&terminal_handle, move |project, _terminal, cx| { - let handles = &mut project.terminals.local_handles; - - if let Some(index) = handles - .iter() - .position(|terminal| terminal.entity_id() == id) - { - handles.remove(index); - cx.notify(); + let builder = project + .update(cx, move |_, cx| { + let (shell, env) = { + match remote_client { + Some(remote_client) => { + create_remote_shell(None, env, path, remote_client, cx)? + } + None => (settings.shell, env), } - }) - .detach(); + }; + anyhow::Ok(TerminalBuilder::new( + local_path.map(|path| path.to_path_buf()), + None, + shell, + env, + settings.cursor_shape, + settings.alternate_scroll, + settings.max_scroll_history_lines, + is_via_remote, + cx.entity_id().as_u64(), + None, + cx, + activation_script, + )) + })?? + .await?; + project.update(cx, move |this, cx| { + let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + + this.terminals + .local_handles + .push(terminal_handle.downgrade()); + + let id = terminal_handle.entity_id(); + cx.observe_release(&terminal_handle, move |project, _terminal, cx| { + let handles = &mut project.terminals.local_handles; - terminal_handle + if let Some(index) = handles + .iter() + .position(|terminal| terminal.entity_id() == id) + { + handles.remove(index); + cx.notify(); + } }) - })? + .detach(); + + terminal_handle + }) }) } @@ -419,20 +426,21 @@ impl Project { terminal: &Entity, cx: &mut Context<'_, Project>, cwd: Option, - ) -> Result> { + ) -> Task>> { let local_path = if self.is_via_remote_server() { None } else { cwd }; - terminal - .read(cx) - .clone_builder(cx, local_path) - .map(|builder| { - let terminal_handle = cx.new(|cx| builder.subscribe(cx)); + let builder = terminal.read(cx).clone_builder(cx, local_path); + cx.spawn(async |project, cx| { + let terminal = builder.await?; + project.update(cx, |project, cx| { + let terminal_handle = cx.new(|cx| terminal.subscribe(cx)); - self.terminals + project + .terminals .local_handles .push(terminal_handle.downgrade()); @@ -452,6 +460,7 @@ impl Project { terminal_handle }) + }) } pub fn terminal_settings<'a>( diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 6f92fe511528097d363063bf837ad3b7efa83318..7e523a46ddf2dfce9921a3c907de19fb91221f9b 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -709,11 +709,13 @@ impl Item for NotebookEditor { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| Self::new(self.project.clone(), self.notebook_item.clone(), window, cx))) + Task::ready(Some(cx.new(|cx| { + Self::new(self.project.clone(), self.notebook_item.clone(), window, cx) + }))) } fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c292d05df75ad329c608d456aaf07a9d1d3af044..e4ff5e6e540fa5626699e98725c5713a09e7cce8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -572,12 +572,14 @@ impl Item for ProjectSearchView { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { let model = self.entity.update(cx, |model, cx| model.clone(cx)); - Some(cx.new(|cx| Self::new(self.workspace.clone(), model, window, cx, None))) + Task::ready(Some(cx.new(|cx| { + Self::new(self.workspace.clone(), model, window, cx, None) + }))) } fn added_to_workspace( @@ -3694,6 +3696,7 @@ pub mod tests { ) }) .unwrap() + .await .unwrap(); assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); @@ -3889,6 +3892,7 @@ pub mod tests { ) }) .unwrap() + .await .unwrap(); assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); assert!( diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 960812da00060a987fbaafe6f248a0d582d60e4f..e35a3ae4e14dec8c510e4858adaeabb789436de0 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -423,232 +423,233 @@ impl TerminalBuilder { completion_tx: Option>>, cx: &App, activation_script: Vec, - ) -> Result { - // If the parent environment doesn't have a locale set - // (As is the case when launched from a .app on MacOS), - // and the Project doesn't have a locale set, then - // set a fallback for our child environment to use. - if std::env::var("LANG").is_err() { - env.entry("LANG".to_string()) - .or_insert_with(|| "en_US.UTF-8".to_string()); - } - - env.insert("ZED_TERM".to_string(), "true".to_string()); - env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); - env.insert("TERM".to_string(), "xterm-256color".to_string()); - env.insert("COLORTERM".to_string(), "truecolor".to_string()); - env.insert( - "TERM_PROGRAM_VERSION".to_string(), - release_channel::AppVersion::global(cx).to_string(), - ); - - #[derive(Default)] - struct ShellParams { - program: String, - args: Option>, - title_override: Option, - } - - impl ShellParams { - fn new( + ) -> Task> { + let version = release_channel::AppVersion::global(cx); + cx.background_spawn(async move { + // If the parent environment doesn't have a locale set + // (As is the case when launched from a .app on MacOS), + // and the Project doesn't have a locale set, then + // set a fallback for our child environment to use. + if std::env::var("LANG").is_err() { + env.entry("LANG".to_string()) + .or_insert_with(|| "en_US.UTF-8".to_string()); + } + + env.insert("ZED_TERM".to_string(), "true".to_string()); + env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); + env.insert("TERM".to_string(), "xterm-256color".to_string()); + env.insert("COLORTERM".to_string(), "truecolor".to_string()); + env.insert("TERM_PROGRAM_VERSION".to_string(), version.to_string()); + + #[derive(Default)] + struct ShellParams { program: String, args: Option>, title_override: Option, - ) -> Self { - log::info!("Using {program} as shell"); - Self { - program, - args, - title_override, - } } - } - let shell_params = match shell.clone() { - Shell::System => { - if cfg!(windows) { - Some(ShellParams::new( - util::shell::get_windows_system_shell(), - None, - None, - )) - } else { - None + impl ShellParams { + fn new( + program: String, + args: Option>, + title_override: Option, + ) -> Self { + log::info!("Using {program} as shell"); + Self { + program, + args, + title_override, + } } } - Shell::Program(program) => Some(ShellParams::new(program, None, None)), - Shell::WithArguments { - program, - args, - title_override, - } => Some(ShellParams::new(program, Some(args), title_override)), - }; - let terminal_title_override = shell_params.as_ref().and_then(|e| e.title_override.clone()); - #[cfg(windows)] - let shell_program = shell_params.as_ref().map(|params| { - use util::ResultExt; + let shell_params = match shell.clone() { + Shell::System => { + if cfg!(windows) { + Some(ShellParams::new( + util::shell::get_windows_system_shell(), + None, + None, + )) + } else { + None + } + } + Shell::Program(program) => Some(ShellParams::new(program, None, None)), + Shell::WithArguments { + program, + args, + title_override, + } => Some(ShellParams::new(program, Some(args), title_override)), + }; + let terminal_title_override = + shell_params.as_ref().and_then(|e| e.title_override.clone()); - Self::resolve_path(¶ms.program) - .log_err() - .unwrap_or(params.program.clone()) - }); + #[cfg(windows)] + let shell_program = shell_params.as_ref().map(|params| { + use util::ResultExt; - // Note: when remoting, this shell_kind will scrutinize `ssh` or - // `wsl.exe` as a shell and fall back to posix or powershell based on - // the compilation target. This is fine right now due to the restricted - // way we use the return value, but would become incorrect if we - // supported remoting into windows. - let shell_kind = shell.shell_kind(cfg!(windows)); - - let pty_options = { - let alac_shell = shell_params.as_ref().map(|params| { - alacritty_terminal::tty::Shell::new( - params.program.clone(), - params.args.clone().unwrap_or_default(), - ) + Self::resolve_path(¶ms.program) + .log_err() + .unwrap_or(params.program.clone()) }); - alacritty_terminal::tty::Options { - shell: alac_shell, - working_directory: working_directory.clone(), - drain_on_exit: true, - env: env.clone().into_iter().collect(), - // We do not want to escape arguments if we are using CMD as our shell. - // If we do we end up with too many quotes/escaped quotes for CMD to handle. - #[cfg(windows)] - escape_args: shell_kind != util::shell::ShellKind::Cmd, - } - }; - - let default_cursor_style = AlacCursorStyle::from(cursor_shape); - let scrolling_history = if task.is_some() { - // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. - // After the task finishes, we do not allow appending to that terminal, so small tasks output should not - // cause excessive memory usage over time. - MAX_SCROLL_HISTORY_LINES - } else { - max_scroll_history_lines - .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) - .min(MAX_SCROLL_HISTORY_LINES) - }; - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; + // Note: when remoting, this shell_kind will scrutinize `ssh` or + // `wsl.exe` as a shell and fall back to posix or powershell based on + // the compilation target. This is fine right now due to the restricted + // way we use the return value, but would become incorrect if we + // supported remoting into windows. + let shell_kind = shell.shell_kind(cfg!(windows)); + + let pty_options = { + let alac_shell = shell_params.as_ref().map(|params| { + alacritty_terminal::tty::Shell::new( + params.program.clone(), + params.args.clone().unwrap_or_default(), + ) + }); - //Spawn a task so the Alacritty EventLoop can communicate with us - //TODO: Remove with a bounded sender which can be dispatched on &self - let (events_tx, events_rx) = unbounded(); - //Set up the terminal... - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx.clone()), - ); + alacritty_terminal::tty::Options { + shell: alac_shell, + working_directory: working_directory.clone(), + drain_on_exit: true, + env: env.clone().into_iter().collect(), + // We do not want to escape arguments if we are using CMD as our shell. + // If we do we end up with too many quotes/escaped quotes for CMD to handle. + #[cfg(windows)] + escape_args: shell_kind != util::shell::ShellKind::Cmd, + } + }; - //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } + let default_cursor_style = AlacCursorStyle::from(cursor_shape); + let scrolling_history = if task.is_some() { + // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. + // After the task finishes, we do not allow appending to that terminal, so small tasks output should not + // cause excessive memory usage over time. + MAX_SCROLL_HISTORY_LINES + } else { + max_scroll_history_lines + .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) + .min(MAX_SCROLL_HISTORY_LINES) + }; + let config = Config { + scrolling_history, + default_cursor_style, + ..Config::default() + }; - let term = Arc::new(FairMutex::new(term)); + //Spawn a task so the Alacritty EventLoop can communicate with us + //TODO: Remove with a bounded sender which can be dispatched on &self + let (events_tx, events_rx) = unbounded(); + //Set up the terminal... + let mut term = Term::new( + config.clone(), + &TerminalBounds::default(), + ZedListener(events_tx.clone()), + ); - //Setup the pty... - let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { - Ok(pty) => pty, - Err(error) => { - bail!(TerminalError { - directory: working_directory, - program: shell_params.as_ref().map(|params| params.program.clone()), - args: shell_params.as_ref().and_then(|params| params.args.clone()), - title_override: terminal_title_override, - source: error, - }); + //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. + if let AlternateScroll::Off = alternate_scroll { + term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); } - }; - let pty_info = PtyProcessInfo::new(&pty); + let term = Arc::new(FairMutex::new(term)); - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx), - pty, - pty_options.drain_on_exit, - false, - ) - .context("failed to create event loop")?; + //Setup the pty... + let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { + Ok(pty) => pty, + Err(error) => { + bail!(TerminalError { + directory: working_directory, + program: shell_params.as_ref().map(|params| params.program.clone()), + args: shell_params.as_ref().and_then(|params| params.args.clone()), + title_override: terminal_title_override, + source: error, + }); + } + }; - //Kick things off - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); // DANGER + let pty_info = PtyProcessInfo::new(&pty); - let no_task = task.is_none(); + //And connect them together + let event_loop = EventLoop::new( + term.clone(), + ZedListener(events_tx), + pty, + pty_options.drain_on_exit, + false, + ) + .context("failed to create event loop")?; - let terminal = Terminal { - task, - terminal_type: TerminalType::Pty { - pty_tx: Notifier(pty_tx), - info: pty_info, - }, - completion_tx, - term, - term_config: config, - title_override: terminal_title_override, - events: VecDeque::with_capacity(10), //Should never get this high. - last_content: Default::default(), - last_mouse: None, - matches: Vec::new(), - selection_head: None, - breadcrumb_text: String::new(), - scroll_px: px(0.), - next_link_id: 0, - selection_phase: SelectionPhase::Ended, - hyperlink_regex_searches: RegexSearches::new(), - vi_mode_enabled: false, - is_ssh_terminal, - last_mouse_move_time: Instant::now(), - last_hyperlink_search_position: None, - #[cfg(windows)] - shell_program, - activation_script: activation_script.clone(), - template: CopyTemplate { - shell, - env, - cursor_shape, - alternate_scroll, - max_scroll_history_lines, - window_id, - }, - child_exited: None, - }; + //Kick things off + let pty_tx = event_loop.channel(); + let _io_thread = event_loop.spawn(); // DANGER + + let no_task = task.is_none(); - if !activation_script.is_empty() && no_task { - for activation_script in activation_script { - terminal.write_to_pty(activation_script.into_bytes()); + let terminal = Terminal { + task, + terminal_type: TerminalType::Pty { + pty_tx: Notifier(pty_tx), + info: pty_info, + }, + completion_tx, + term, + term_config: config, + title_override: terminal_title_override, + events: VecDeque::with_capacity(10), //Should never get this high. + last_content: Default::default(), + last_mouse: None, + matches: Vec::new(), + selection_head: None, + breadcrumb_text: String::new(), + scroll_px: px(0.), + next_link_id: 0, + selection_phase: SelectionPhase::Ended, + hyperlink_regex_searches: RegexSearches::new(), + vi_mode_enabled: false, + is_ssh_terminal, + last_mouse_move_time: Instant::now(), + last_hyperlink_search_position: None, + #[cfg(windows)] + shell_program, + activation_script: activation_script.clone(), + template: CopyTemplate { + shell, + env, + cursor_shape, + alternate_scroll, + max_scroll_history_lines, + window_id, + }, + child_exited: None, + }; + + if !activation_script.is_empty() && no_task { + for activation_script in activation_script { + terminal.write_to_pty(activation_script.into_bytes()); + // Simulate enter key press + // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character) + // and generally mess up the rendering. + terminal.write_to_pty(b"\x0d"); + } + // In order to clear the screen at this point, we have two options: + // 1. We can send a shell-specific command such as "clear" or "cls" + // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event + // and clear the screen using `terminal.clear()` method + // We cannot issue a `terminal.clear()` command at this point as alacritty is evented + // and while we have sent the activation script to the pty, it will be executed asynchronously. + // Therefore, we somehow need to wait for the activation script to finish executing before we + // can proceed with clearing the screen. + terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes()); // Simulate enter key press - // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character) - // and generally mess up the rendering. terminal.write_to_pty(b"\x0d"); } - // In order to clear the screen at this point, we have two options: - // 1. We can send a shell-specific command such as "clear" or "cls" - // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event - // and clear the screen using `terminal.clear()` method - // We cannot issue a `terminal.clear()` command at this point as alacritty is evented - // and while we have sent the activation script to the pty, it will be executed asynchronously. - // Therefore, we somehow need to wait for the activation script to finish executing before we - // can proceed with clearing the screen. - terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes()); - // Simulate enter key press - terminal.write_to_pty(b"\x0d"); - } - Ok(TerminalBuilder { - terminal, - events_rx, + Ok(TerminalBuilder { + terminal, + events_rx, + }) }) } @@ -2153,7 +2154,7 @@ impl Terminal { self.vi_mode_enabled } - pub fn clone_builder(&self, cx: &App, cwd: Option) -> Result { + pub fn clone_builder(&self, cx: &App, cwd: Option) -> Task> { let working_directory = self.working_directory().or_else(|| cwd); TerminalBuilder::new( working_directory, @@ -2389,28 +2390,30 @@ mod tests { let (completion_tx, completion_rx) = smol::channel::unbounded(); let (program, args) = ShellBuilder::new(&Shell::System, false) .build(Some("echo".to_owned()), &["hello".to_owned()]); - let terminal = cx.new(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - false, - 0, - Some(completion_tx), - cx, - vec![], - ) - .unwrap() - .subscribe(cx) - }); + let builder = cx + .update(|cx| { + TerminalBuilder::new( + None, + None, + task::Shell::WithArguments { + program, + args, + title_override: None, + }, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + Some(completion_tx), + cx, + vec![], + ) + }) + .await + .unwrap(); + let terminal = cx.new(|cx| builder.subscribe(cx)); assert_eq!( completion_rx.recv().await.unwrap(), Some(ExitStatus::default()) @@ -2439,25 +2442,27 @@ mod tests { cx.executor().allow_parking(); let (completion_tx, completion_rx) = smol::channel::unbounded(); + let builder = cx + .update(|cx| { + TerminalBuilder::new( + None, + None, + task::Shell::System, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + Some(completion_tx), + cx, + Vec::new(), + ) + }) + .await + .unwrap(); // Build an empty command, which will result in a tty shell spawned. - let terminal = cx.new(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::System, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - .unwrap() - .subscribe(cx) - }); + let terminal = cx.new(|cx| builder.subscribe(cx)); let (event_tx, event_rx) = smol::channel::unbounded::(); cx.update(|cx| { @@ -2508,28 +2513,30 @@ mod tests { let (completion_tx, completion_rx) = smol::channel::unbounded(); let (program, args) = ShellBuilder::new(&Shell::System, false) .build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]); - let terminal = cx.new(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - .unwrap() - .subscribe(cx) - }); + let builder = cx + .update(|cx| { + TerminalBuilder::new( + None, + None, + task::Shell::WithArguments { + program, + args, + title_override: None, + }, + HashMap::default(), + CursorShape::default(), + AlternateScroll::On, + None, + false, + 0, + Some(completion_tx), + cx, + Vec::new(), + ) + }) + .await + .unwrap(); + let terminal = cx.new(|cx| builder.subscribe(cx)); let (event_tx, event_rx) = smol::channel::unbounded::(); cx.update(|cx| { diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index 14606d4ed58054cca70ca16d420e90083bcbcc14..43f53c3a6516167129d367f29ab957640078b4f1 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -214,14 +214,6 @@ async fn deserialize_pane_group( } SerializedPaneGroup::Pane(serialized_pane) => { let active = serialized_pane.active; - let new_items = deserialize_terminal_views( - workspace_id, - project.clone(), - workspace.clone(), - serialized_pane.children.as_slice(), - cx, - ) - .await; let pane = panel .update_in(cx, |terminal_panel, window, cx| { @@ -236,56 +228,71 @@ async fn deserialize_pane_group( .log_err()?; let active_item = serialized_pane.active_item; let pinned_count = serialized_pane.pinned_count; - let terminal = pane - .update_in(cx, |pane, window, cx| { - populate_pane_items(pane, new_items, active_item, window, cx); - pane.set_pinned_count(pinned_count); + let new_items = deserialize_terminal_views( + workspace_id, + project.clone(), + workspace.clone(), + serialized_pane.children.as_slice(), + cx, + ); + cx.spawn({ + let pane = pane.downgrade(); + async move |cx| { + let new_items = new_items.await; + + let items = pane.update_in(cx, |pane, window, cx| { + populate_pane_items(pane, new_items, active_item, window, cx); + pane.set_pinned_count(pinned_count); + pane.items_len() + }); // Avoid blank panes in splits - if pane.items_len() == 0 { + if items.is_ok_and(|items| items == 0) { let working_directory = workspace .update(cx, |workspace, cx| default_working_directory(workspace, cx)) .ok() .flatten(); - let terminal = project.update(cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) - }); - Some(Some(terminal)) - } else { - Some(None) + let Some(terminal) = project + .update(cx, |project, cx| { + project.create_terminal_shell(working_directory, cx) + }) + .log_err() + else { + return; + }; + + let terminal = terminal.await.log_err(); + pane.update_in(cx, |pane, window, cx| { + if let Some(terminal) = terminal { + let terminal_view = Box::new(cx.new(|cx| { + TerminalView::new( + terminal, + workspace.clone(), + Some(workspace_id), + project.downgrade(), + window, + cx, + ) + })); + pane.add_item(terminal_view, true, false, None, window, cx); + } + }) + .ok(); } - }) - .ok() - .flatten()?; - if let Some(terminal) = terminal { - let terminal = terminal.await.ok()?; - pane.update_in(cx, |pane, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal, - workspace.clone(), - Some(workspace_id), - project.downgrade(), - window, - cx, - ) - })); - pane.add_item(terminal_view, true, false, None, window, cx); - }) - .ok()?; - } + } + }) + .detach(); Some((Member::Pane(pane.clone()), active.then_some(pane))) } } } -async fn deserialize_terminal_views( +fn deserialize_terminal_views( workspace_id: WorkspaceId, project: Entity, workspace: WeakEntity, item_ids: &[u64], cx: &mut AsyncWindowContext, -) -> Vec> { - let mut items = Vec::with_capacity(item_ids.len()); +) -> impl Future>> + use<> { let mut deserialized_items = item_ids .iter() .map(|item_id| { @@ -302,12 +309,15 @@ async fn deserialize_terminal_views( .unwrap_or_else(|e| Task::ready(Err(e.context("no window present")))) }) .collect::>(); - while let Some(item) = deserialized_items.next().await { - if let Some(item) = item.log_err() { - items.push(item); + async move { + let mut items = Vec::with_capacity(deserialized_items.len()); + while let Some(item) = deserialized_items.next().await { + if let Some(item) = item.log_err() { + items.push(item); + } } + items } - items } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index de66bb1ed64851a1101d434e3a7b54a8ae725cfb..df30ea4ddf5611b286c0608c7e6d51d4ff7f9e00 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -462,11 +462,11 @@ impl TerminalPanel { cx.spawn_in(window, async move |panel, cx| { let terminal = project .update(cx, |project, cx| match terminal_view { - Some(view) => Task::ready(project.clone_terminal( + Some(view) => project.clone_terminal( &view.read(cx).terminal.clone(), cx, working_directory, - )), + ), None => project.create_terminal_shell(working_directory, cx), }) .ok()? diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index e89e435b0c5dd2ba064a9904dfacec6870d8d512..65eb993d208629f16c705bbac55c3dc3e0f08261 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1220,28 +1220,31 @@ impl Item for TerminalView { workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> { - let terminal = self - .project - .update(cx, |project, cx| { - let cwd = project - .active_project_directory(cx) - .map(|it| it.to_path_buf()); - project.clone_terminal(self.terminal(), cx, cwd) + ) -> Task>> { + let Ok(terminal) = self.project.update(cx, |project, cx| { + let cwd = project + .active_project_directory(cx) + .map(|it| it.to_path_buf()); + project.clone_terminal(self.terminal(), cx, cwd) + }) else { + return Task::ready(None); + }; + cx.spawn_in(window, async move |this, cx| { + let terminal = terminal.await.log_err()?; + this.update_in(cx, |this, window, cx| { + cx.new(|cx| { + TerminalView::new( + terminal, + this.workspace.clone(), + workspace_id, + this.project.clone(), + window, + cx, + ) + }) }) - .ok()? - .log_err()?; - - Some(cx.new(|cx| { - TerminalView::new( - terminal, - self.workspace.clone(), - workspace_id, - self.project.clone(), - window, - cx, - ) - })) + .ok() + }) } fn is_dirty(&self, cx: &gpui::App) -> bool { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index bc755d851036d10f04b76866b3d7b94673f2df84..42d452a68ee72491a53e9da535d7713c735912f5 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -11,8 +11,9 @@ use anyhow::Result; use client::{Client, proto}; use futures::{StreamExt, channel::mpsc}; use gpui::{ - Action, AnyElement, AnyView, App, Context, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, WeakEntity, Window, + Action, AnyElement, AnyView, App, AppContext, Context, Entity, EntityId, EventEmitter, + FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render, SharedString, Task, + WeakEntity, Window, }; use project::{Project, ProjectEntryId, ProjectPath}; pub use settings::{ @@ -217,11 +218,11 @@ pub trait Item: Focusable + EventEmitter + Render + Sized { _workspace_id: Option, _window: &mut Window, _: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - None + Task::ready(None) } fn is_dirty(&self, _: &App) -> bool { false @@ -422,7 +423,7 @@ pub trait ItemHandle: 'static + Send { workspace_id: Option, window: &mut Window, cx: &mut App, - ) -> Option>; + ) -> Task>>; fn added_to_pane( &self, workspace: &mut Workspace, @@ -635,9 +636,12 @@ impl ItemHandle for Entity { workspace_id: Option, window: &mut Window, cx: &mut App, - ) -> Option> { - self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx)) - .map(|handle| Box::new(handle) as Box) + ) -> Task>> { + let task = self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx)); + cx.background_spawn(async move { + task.await + .map(|handle| Box::new(handle) as Box) + }) } fn added_to_pane( @@ -1504,11 +1508,11 @@ pub mod test { _workspace_id: Option, _: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| Self { + Task::ready(Some(cx.new(|cx| Self { state: self.state.clone(), label: self.label.clone(), save_count: self.save_count, @@ -1525,7 +1529,7 @@ pub mod test { workspace_id: self.workspace_id, focus_handle: cx.focus_handle(), serialize: None, - })) + }))) } fn is_dirty(&self, _: &App) -> bool { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3dcba4aa4ebc38bc7c0006c8402e38d4e12ac016..3e39a8d18faa367798db7a65689bcca59b07a5ae 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3295,11 +3295,18 @@ impl Pane { else { return; }; - if let Some(item) = item.clone_on_split(database_id, window, cx) { - to_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx); - }) - } + let task = item.clone_on_split(database_id, window, cx); + let to_pane = to_pane.downgrade(); + cx.spawn_in(window, async move |_, cx| { + if let Some(item) = task.await { + to_pane + .update_in(cx, |pane, window, cx| { + pane.add_item(item, true, true, None, window, cx) + }) + .ok(); + } + }) + .detach(); } else { move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); } diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index d77be8ed7632dd113e10a03552da79735d82fa6c..34c7d27df73b8b832e9b5b23b832a15161644e3a 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -6,7 +6,7 @@ use call::{RemoteVideoTrack, RemoteVideoTrackView, Room}; use client::{User, proto::PeerId}; use gpui::{ AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, SharedString, Styled, div, + ParentElement, Render, SharedString, Styled, Task, div, }; use std::sync::Arc; use ui::{Icon, IconName, prelude::*}; @@ -114,14 +114,14 @@ impl Item for SharedScreen { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> { - Some(cx.new(|cx| Self { + ) -> Task>> { + Task::ready(Some(cx.new(|cx| Self { view: self.view.update(cx, |view, cx| view.clone(window, cx)), peer_id: self.peer_id, user: self.user.clone(), nav_history: Default::default(), focus: cx.focus_handle(), - })) + }))) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 09a5415ca063d0aab2b2fab97abff3533e113b0b..00f083f353daab677265a2410823c69be0bc5e8f 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -1,5 +1,7 @@ #![allow(unused, dead_code)] -use gpui::{AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, actions, hsla}; +use gpui::{ + AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla, +}; use strum::IntoEnumIterator; use theme::all_theme_colors; use ui::{ @@ -100,11 +102,11 @@ impl Item for ThemePreview { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { - Some(cx.new(|cx| Self::new(window, cx))) + Task::ready(Some(cx.new(|cx| Self::new(window, cx)))) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 51a4edbf2567f941595bcd7307095858b240db57..f3b7fea96d365bb6d12f6eea0cc93518f2683dcb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3630,7 +3630,8 @@ impl Workspace { if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { window.focus(&pane.focus_handle(cx)); } else { - self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx); + self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx) + .detach(); } } @@ -3997,7 +3998,8 @@ impl Workspace { clone_active_item, } => { if *clone_active_item { - self.split_and_clone(pane.clone(), *direction, window, cx); + self.split_and_clone(pane.clone(), *direction, window, cx) + .detach(); } else { self.split_and_move(pane.clone(), *direction, window, cx); } @@ -4138,21 +4140,27 @@ impl Workspace { direction: SplitDirection, window: &mut Window, cx: &mut Context, - ) -> Option> { - let item = pane.read(cx).active_item()?; - let maybe_pane_handle = - if let Some(clone) = item.clone_on_split(self.database_id(), window, cx) { - let new_pane = self.add_pane(window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(clone, true, true, None, window, cx) - }); - self.center.split(&pane, &new_pane, direction).unwrap(); - cx.notify(); - Some(new_pane) + ) -> Task>> { + let Some(item) = pane.read(cx).active_item() else { + return Task::ready(None); + }; + let task = item.clone_on_split(self.database_id(), window, cx); + cx.spawn_in(window, async move |this, cx| { + if let Some(clone) = task.await { + this.update_in(cx, |this, window, cx| { + let new_pane = this.add_pane(window, cx); + new_pane.update(cx, |pane, cx| { + pane.add_item(clone, true, true, None, window, cx) + }); + this.center.split(&pane, &new_pane, direction).unwrap(); + cx.notify(); + new_pane + }) + .ok() } else { None - }; - maybe_pane_handle + } + }) } pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { @@ -8177,19 +8185,27 @@ pub fn clone_active_item( let Some(active_item) = source.read(cx).active_item() else { return; }; - destination.update(cx, |target_pane, cx| { - let Some(clone) = active_item.clone_on_split(workspace_id, window, cx) else { - return; - }; - target_pane.add_item( - clone, - focus_destination, - focus_destination, - Some(target_pane.items_len()), - window, - cx, - ); - }); + let destination = destination.downgrade(); + let task = active_item.clone_on_split(workspace_id, window, cx); + window + .spawn(cx, async move |cx| { + let Some(clone) = task.await else { + return; + }; + destination + .update_in(cx, |target_pane, window, cx| { + target_pane.add_item( + clone, + focus_destination, + focus_destination, + Some(target_pane.items_len()), + window, + cx, + ); + }) + .log_err(); + }) + .detach(); } #[derive(Debug)] @@ -8696,25 +8712,24 @@ mod tests { cx, ); - let right_pane = workspace - .split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx) - .unwrap(); + let right_pane = + workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx); - right_pane.update(cx, |pane, cx| { - pane.add_item( - single_entry_items[1].boxed_clone(), - true, - true, - None, - window, - cx, - ); - pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx); + let boxed_clone = single_entry_items[1].boxed_clone(); + let right_pane = window.spawn(cx, async move |cx| { + right_pane.await.inspect(|right_pane| { + right_pane + .update_in(cx, |pane, window, cx| { + pane.add_item(boxed_clone, true, true, None, window, cx); + pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx); + }) + .unwrap(); + }) }); (left_pane, right_pane) }); - + let right_pane = right_pane.await.unwrap(); cx.focus(&right_pane); let mut close = right_pane.update_in(cx, |pane, window, cx| { @@ -10531,7 +10546,10 @@ mod tests { window, cx, ); + }); + cx.run_until_parked(); + workspace.update(cx, |workspace, cx| { assert_eq!(workspace.panes.len(), 3, "Two new panes were created"); for pane in workspace.panes() { assert_eq!( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1416a74e1697324213c11e1bbc51fd2d8a6bf91b..62cf52de19469f7087c42f91363bc9002312fe1f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2836,14 +2836,16 @@ mod tests { }); // Split the pane with the first entry, then open the second entry again. - window + let (task1, task2) = window .update(cx, |w, window, cx| { - w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx); - w.open_path(file2.clone(), None, true, window, cx) + ( + w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx), + w.open_path(file2.clone(), None, true, window, cx), + ) }) - .unwrap() - .await .unwrap(); + task1.await.unwrap(); + task2.await.unwrap(); window .read_with(cx, |w, cx| { @@ -3466,7 +3468,13 @@ mod tests { SplitDirection::Right, window, cx, - ); + ) + }) + .unwrap() + .await + .unwrap(); + window + .update(cx, |workspace, window, cx| { workspace.open_path( (worktree.read(cx).id(), rel_path("the-new-name.rs")), None, diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 7a287cf3d83f24e7f4d42221bda420053a975860..75369bbe0324b2fd4bbe9279ed253faac52487c8 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -721,7 +721,7 @@ impl Item for ComponentPreview { _workspace_id: Option, window: &mut Window, cx: &mut Context, - ) -> Option> + ) -> Task>> where Self: Sized, { @@ -743,13 +743,13 @@ impl Item for ComponentPreview { cx, ); - match self_result { + Task::ready(match self_result { Ok(preview) => Some(cx.new(|_cx| preview)), Err(e) => { log::error!("Failed to clone component preview: {}", e); None } - } + }) } fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { From cad06011c5c35fd9c9ca463133a05d9173ddaea8 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 21 Oct 2025 06:30:02 -0700 Subject: [PATCH 102/202] language: Fix hang when editing certain tailwind class names (#40791) Closes #36223 Upsteam issue to track: https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1479 Release Notes: - Fixed an issue where Zed hanged when editing certain Tailwind class names. --- crates/language/Cargo.toml | 1 + crates/language/src/language.rs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index b5524913bcc7092b72e6dbf2d55b7393d1bea889..bbbf9e31a5b39069e93a5f52f18df16bbc9f9671 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -67,6 +67,7 @@ tree-sitter.workspace = true unicase = "2.6" util.workspace = true watch.workspace = true +zlog.workspace = true diffy = "0.4.2" [dev-dependencies] diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index fb523aeb9e84e49f4d4c9afb0583eacc1e844030..2d1a274381224978246db618301606caf44a60cb 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2589,7 +2589,12 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { let mut start = point_from_lsp(range.start); let mut end = point_from_lsp(range.end); if start > end { - log::warn!("range_from_lsp called with inverted range {start:?}-{end:?}"); + // We debug instead of warn so that this is not logged by default unless explicitly requested. + // Using warn would write to the log file, and since we receive an enormous amount of + // range_from_lsp calls (especially during completions), that can hang the main thread. + // + // See issue #36223. + zlog::debug!("range_from_lsp called with inverted range {start:?}-{end:?}"); mem::swap(&mut start, &mut end); } start..end From 10b9ae5e449589835edb04d8fb32c5ee8f04c8c9 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 21 Oct 2025 06:31:35 -0700 Subject: [PATCH 103/202] multi_buffer: Assert char boundary for panic due to point_to_buffer_offset (#40777) In an attempt to figure out what's wrong with `point_to_buffer_offset` for crash https://github.com/zed-industries/zed/issues/40453. We want to know which branch among these two is the bad one. Release Notes: - N/A Co-authored-by: Lukas Wirth --- crates/multi_buffer/src/multi_buffer.rs | 15 ++++++++++++++- crates/rope/src/rope.rs | 23 +++++++++++++++++++++++ crates/text/src/text.rs | 11 +---------- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index db15a0774c5411b1ddfb8a3f9772a0ca9789c36c..ff905a1e2e1cb7a866fdb12aaf592b4b925b749f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -4454,10 +4454,23 @@ impl MultiBufferSnapshot { && region.has_trailing_newline && !region.is_main_buffer { - return Some((&cursor.excerpt()?.buffer, cursor.main_buffer_position()?)); + let main_buffer_position = cursor.main_buffer_position()?; + let buffer_snapshot = &cursor.excerpt()?.buffer; + // remove this assert once we figure out the cause of the panics for #40453 + buffer_snapshot + .text + .as_rope() + .assert_char_boundary(main_buffer_position); + return Some((buffer_snapshot, main_buffer_position)); } else if buffer_offset > region.buffer.len() { return None; } + // remove this assert once we figure out the cause of the panics for #40453 + region + .buffer + .text + .as_rope() + .assert_char_boundary(buffer_offset); Some((region.buffer, buffer_offset)) } diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 44e0676bae79bf3e3dee92e4a16652f404c95273..b4bf987e019c424a3ec989454184feea87879750 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -47,6 +47,29 @@ impl Rope { .unwrap_or(false) } + #[track_caller] + #[inline(always)] + pub fn assert_char_boundary(&self, offset: usize) { + if self.is_char_boundary(offset) { + return; + } + panic_char_boundary(self, offset); + + #[cold] + #[inline(never)] + fn panic_char_boundary(rope: &Rope, offset: usize) { + // find the character + let char_start = rope.floor_char_boundary(offset); + // `char_start` must be less than len and a char boundary + let ch = rope.chars_at(char_start).next().unwrap(); + let char_range = char_start..char_start + ch.len_utf8(); + panic!( + "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", + offset, ch, char_range, + ); + } + } + pub fn floor_char_boundary(&self, index: usize) -> usize { if index >= self.len() { self.len() diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index d78dfbea7dc0d3ac65005855eaffadee37fda584..d30a3dca0d5a3a5809440b816b9491f7f1d940c8 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2402,17 +2402,8 @@ impl BufferSnapshot { } else { if offset > self.visible_text.len() { panic!("offset {} is out of bounds", offset) - } else if !self.visible_text.is_char_boundary(offset) { - // find the character - let char_start = self.visible_text.floor_char_boundary(offset); - // `char_start` must be less than len and a char boundary - let ch = self.visible_text.chars_at(char_start).next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - panic!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", - offset, ch, char_range, - ); } + self.visible_text.assert_char_boundary(offset); let (start, _, item) = self.fragments.find::(&None, &offset, bias); let fragment = item.unwrap(); let overshoot = offset - start; From 854d1ec4dce1799a3bebd5a5d446835849f8ca71 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 21 Oct 2025 16:37:16 +0200 Subject: [PATCH 104/202] acp_thread: Fix panic when following acp agents across buffers (#40798) Fixes ZED-2D7 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/acp_thread/src/acp_thread.rs | 15 +++++++++------ crates/multi_buffer/src/multi_buffer.rs | 18 ++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index ed2e01f0b37197d6878dee699fba43ed3410066f..4d8c57dd8f5a97aabc5cf3dc9e8d5aae9d6c8f2f 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1421,15 +1421,18 @@ impl AcpThread { if let Some(Some(location)) = resolved_locations.last() { project.update(cx, |project, cx| { - let should_ignore = if let Some(agent_location) = project.agent_location() { + let should_ignore = if let Some(agent_location) = project + .agent_location() + .filter(|agent_location| agent_location.buffer == location.buffer) + { let snapshot = location.buffer.read(cx).snapshot(); let old_position = agent_location.position.to_point(&snapshot); let new_position = location.position.to_point(&snapshot); - agent_location.buffer == location.buffer - // ignore this so that when we get updates from the edit tool - // the position doesn't reset to the startof line - && (old_position.row == new_position.row - && old_position.column > new_position.column) + + // ignore this so that when we get updates from the edit tool + // the position doesn't reset to the startof line + old_position.row == new_position.row + && old_position.column > new_position.column } else { false }; diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index ff905a1e2e1cb7a866fdb12aaf592b4b925b749f..a5526e823aae6da34dbb1f6ff8d869bad9624b60 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -6170,22 +6170,20 @@ impl MultiBufferSnapshot { ) -> SmallVec<[Locator; 1]> { let mut sorted_ids = ids.into_iter().collect::>(); sorted_ids.sort_unstable(); + sorted_ids.dedup(); let mut locators = SmallVec::new(); while sorted_ids.last() == Some(&ExcerptId::max()) { sorted_ids.pop(); - if let Some(mapping) = self.excerpt_ids.last() { - locators.push(mapping.locator.clone()); - } + locators.push(Locator::max()); } - let mut sorted_ids = sorted_ids.into_iter().dedup().peekable(); - if sorted_ids.peek() == Some(&ExcerptId::min()) { - sorted_ids.next(); - if let Some(mapping) = self.excerpt_ids.first() { - locators.push(mapping.locator.clone()); - } - } + let mut sorted_ids = sorted_ids.into_iter().peekable(); + locators.extend( + sorted_ids + .peeking_take_while(|excerpt| *excerpt == ExcerptId::min()) + .map(|_| Locator::min()), + ); let mut cursor = self.excerpt_ids.cursor::(()); for id in sorted_ids { From 981fa288eb438d011a7b5b1b22b8c38cc9d2a3ba Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Tue, 21 Oct 2025 17:04:13 +0200 Subject: [PATCH 105/202] editor: Hide the git blame popover on escape (#40549) Release Notes: - Added way to hide git blame popover by pressing the escape key. --- crates/editor/src/editor.rs | 17 +++++++++++++---- crates/editor/src/element.rs | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7955aac5e52d3a7fce7299b2d811636a9db2b085..1a8c8600bf5bcbde37d0d4fcfb8a2133bba3766d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3990,6 +3990,10 @@ impl Editor { return true; } + if self.hide_blame_popover(true, cx) { + return true; + } + if hide_hover(self, cx) { return true; } @@ -6839,13 +6843,15 @@ impl Editor { } } - fn hide_blame_popover(&mut self, cx: &mut Context) { + fn hide_blame_popover(&mut self, ignore_timeout: bool, cx: &mut Context) -> bool { self.inline_blame_popover_show_task.take(); if let Some(state) = &mut self.inline_blame_popover { let hide_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(100)) - .await; + if !ignore_timeout { + cx.background_executor() + .timer(std::time::Duration::from_millis(100)) + .await; + } editor .update(cx, |editor, cx| { editor.inline_blame_popover.take(); @@ -6854,6 +6860,9 @@ impl Editor { .ok(); }); state.hide_task = Some(hide_task); + true + } else { + false } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 65c9d1dbc78d14ab1419118f1d56d2b10a494ba6..c726de49475ac62eb6c04c05f8163a17218e57cf 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1207,10 +1207,10 @@ impl EditorElement { if mouse_over_inline_blame || mouse_over_popover { editor.show_blame_popover(*buffer_id, blame_entry, event.position, false, cx); } else if !keyboard_grace { - editor.hide_blame_popover(cx); + editor.hide_blame_popover(false, cx); } } else { - editor.hide_blame_popover(cx); + editor.hide_blame_popover(false, cx); } let breakpoint_indicator = if gutter_hovered { From 69025f3bf4656ae9da5ad576969f3fe58fc28eaf Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 21 Oct 2025 17:31:40 +0200 Subject: [PATCH 106/202] Add `linux_repo_snapshot` got `.gitignore` (#40802) It keeps popping up in my project searches ... This prevents that. Not like we are planning on editing that file again. Release Notes: - N/A *or* Added/Fixed/Improved ... --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d248b1f7e5adf30cb286a1737c1cd4f72f0f5d20..2a91a65b6eaef906681bf3f6e315de07b094c4b1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ /crates/collab/seed.json /crates/theme/schemas/theme.json /crates/zed/resources/flatpak/flatpak-cargo-sources.json +/crates/project_panel/benches/linux_repo_snapshot.txt /dev.zed.Zed*.json /node_modules/ /plugins/bin From b79837695e66b95b7e32790999b6f732e54cc7d1 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Tue, 21 Oct 2025 17:48:40 +0200 Subject: [PATCH 107/202] fs: Implement `FileHandle::current_path` for windows (#40804) Release Notes: - Fixed worktree renames not working on windows --- crates/fs/src/fs.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 9fe9e53eaacc864ad66248131813e305fd5bc172..69ac5fb0d3b4aed9e63166c60ba9550186fb204f 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -321,7 +321,33 @@ impl FileHandle for std::fs::File { #[cfg(target_os = "windows")] fn current_path(&self, _: &Arc) -> Result { - anyhow::bail!("unimplemented") + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + use std::os::windows::io::AsRawHandle; + + use windows::Win32::Foundation::HANDLE; + use windows::Win32::Storage::FileSystem::{ + FILE_NAME_NORMALIZED, GetFinalPathNameByHandleW, + }; + + let handle = HANDLE(self.as_raw_handle() as _); + + // Query required buffer size (in wide chars) + let required_len = + unsafe { GetFinalPathNameByHandleW(handle, &mut [], FILE_NAME_NORMALIZED) }; + if required_len == 0 { + anyhow::bail!("GetFinalPathNameByHandleW returned 0 length"); + } + + // Allocate buffer and retrieve the path + let mut buf: Vec = vec![0u16; required_len as usize + 1]; + let written = unsafe { GetFinalPathNameByHandleW(handle, &mut buf, FILE_NAME_NORMALIZED) }; + if written == 0 { + anyhow::bail!("GetFinalPathNameByHandleW failed to write path"); + } + + let os_str: OsString = OsString::from_wide(&buf[..written as usize]); + Ok(PathBuf::from(os_str)) } } From d7ffc37b149e219eedde6398a2e9369fabc61e85 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 22 Oct 2025 00:05:40 +0800 Subject: [PATCH 108/202] editor: Improve text color in document color highlight (#39372) Release Notes: - Improved text color in LSP document color highlight. ---- Because highlight ranges are implemented using a paint background, there's no way to control the text color. I've been thinking about this problem for a long time, want to solve it. ~~Today, I come up with a new idea. Re-rendering the document color text at the top should solve this problem.~~ #### Update 10/6: > The previous version is not good, when we have soft wrap text, that version will not work correct. Now use exists `bg_segments_per_row` feature to fix text color. ## Before image ## After image --- crates/editor/src/element.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c726de49475ac62eb6c04c05f8163a17218e57cf..944715a0dfa3747bcece23a643a144f891687b53 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8931,10 +8931,20 @@ impl Element for EditorElement { cx, ); + let merged_highlighted_ranges = + if let Some((_, colors)) = document_colors.as_ref() { + &highlighted_ranges + .clone() + .into_iter() + .chain(colors.clone()) + .collect() + } else { + &highlighted_ranges + }; let bg_segments_per_row = Self::bg_segments_per_row( start_row..end_row, &selections, - &highlighted_ranges, + &merged_highlighted_ranges, self.style.background, ); From cf8422f7fddc8d5ef880a88ab638b3665b2138ef Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 21 Oct 2025 09:25:07 -0700 Subject: [PATCH 109/202] settings_ui: Fix focus bugs (#40806) Closes #40608 Release Notes: - settings_ui: Fixed an issue where tabbing to the nav bar from the search bar while the nav bar was scrolled would result in the first _visible_ nav entry being selected, instead of the literal first nav entry - settings_ui: Fixed an issue where scrolling the selected nav entry off screen would cause the keyboard shortcut hint for the focus nav / focus content binding to dissapear - settings_ui: Fixed an issue where text input controls could not be focused via the keyboard --- crates/settings_ui/src/components.rs | 9 ++++--- crates/settings_ui/src/settings_ui.rs | 36 ++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/settings_ui/src/components.rs b/crates/settings_ui/src/components.rs index a29aae3bb1f2ef086e6d3289b03fbe29000d0f45..d424d96f273de131f65b0eae253f90bd52a21b08 100644 --- a/crates/settings_ui/src/components.rs +++ b/crates/settings_ui/src/components.rs @@ -62,10 +62,6 @@ impl RenderOnce for SettingsEditor { } }); - if let Some(tab_index) = self.tab_index { - editor.focus_handle(cx).tab_index(tab_index); - } - let weak_editor = editor.downgrade(); let theme_colors = cx.theme().colors(); @@ -78,6 +74,11 @@ impl RenderOnce for SettingsEditor { .border_1() .border_color(theme_colors.border) .bg(theme_colors.editor_background) + .when_some(self.tab_index, |this, tab_index| { + let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true); + this.track_focus(&focus_handle) + .focus(|s| s.border_color(theme_colors.border_focused)) + }) .child(editor) .when_some(self.confirm, |this, confirm| { this.on_action::({ diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index ef3049ea20953c032714f4855a1b3da8a60a5434..9c1b3f2e561d420b7022a467b86f0368e2330bbd 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1821,6 +1821,9 @@ impl SettingsWindow { .read(cx) .handle .contains_focused(window, cx) + || self + .visible_navbar_entries() + .any(|(_, entry)| entry.focus_handle.is_focused(window)) { "Focus Content" } else { @@ -2020,7 +2023,13 @@ impl SettingsWindow { .border_t_1() .border_color(cx.theme().colors().border_variant) .children( - KeyBinding::for_action(&ToggleFocusNav, window, cx).map(|this| { + KeyBinding::for_action_in( + &ToggleFocusNav, + &self.navbar_focus_handle.focus_handle(cx), + window, + cx, + ) + .map(|this| { KeybindingHint::new( this, cx.theme().colors().surface_background.opacity(0.5), @@ -2123,6 +2132,17 @@ impl SettingsWindow { .any(|(index, _)| index == nav_entry_index) } + fn focus_and_scroll_to_first_visible_nav_entry( + &self, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(nav_entry_index) = self.visible_navbar_entries().next().map(|(index, _)| index) + { + self.focus_and_scroll_to_nav_entry(nav_entry_index, window, cx); + } + } + fn focus_and_scroll_to_nav_entry( &self, nav_entry_index: usize, @@ -2735,9 +2755,17 @@ impl Render for SettingsWindow { let prev_index = this.focused_file_index(window, cx).saturating_sub(1); this.focus_file_at_index(prev_index, window); })) - .on_action(|_: &menu::SelectNext, window, _| { - window.focus_next(); - }) + .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { + if this + .search_bar + .focus_handle(cx) + .contains_focused(window, cx) + { + this.focus_and_scroll_to_first_visible_nav_entry(window, cx); + } else { + window.focus_next(); + } + })) .on_action(|_: &menu::SelectPrevious, window, _| { window.focus_prev(); }) From ce5d597efa54e5374399d3cd79e76254582da91c Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 21 Oct 2025 12:39:10 -0400 Subject: [PATCH 110/202] Centralize Zed.log documentation (#40808) Just wanted a single location to point people to for telling them where to find their log file. I left duplicate text in GitHub Issue templates, as it seems annoying to have to follow a link when making an issue. Release Notes: - N/A --- .github/ISSUE_TEMPLATE/11_crash_report.yml | 3 ++- docs/src/SUMMARY.md | 1 + docs/src/troubleshooting.md | 16 ++++++++++++++++ extensions/slash-commands-example/README.md | 3 +-- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 docs/src/troubleshooting.md diff --git a/.github/ISSUE_TEMPLATE/11_crash_report.yml b/.github/ISSUE_TEMPLATE/11_crash_report.yml index aa736c75341512442720c202a4cadbf51bf253c8..1300809a39c6ecd9a10eb6a28e80ef4478dba6b5 100644 --- a/.github/ISSUE_TEMPLATE/11_crash_report.yml +++ b/.github/ISSUE_TEMPLATE/11_crash_report.yml @@ -33,9 +33,10 @@ body: required: true - type: textarea attributes: - label: If applicable, attach your `~/Library/Logs/Zed/Zed.log` file to this issue. + label: If applicable, attach your `Zed.log` file to this issue. description: | macOS: `~/Library/Logs/Zed/Zed.log` + Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log` Linux: `~/.local/share/zed/logs/Zed.log` or $XDG_DATA_HOME If you only need the most recent lines, you can run the `zed: open log` command palette action to see the last 1000. value: | diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 086f8394d639994b808c4e390653f65bf9978d7a..1620998baf2b575da9d9e990d467f152f988c5fa 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -9,6 +9,7 @@ - [Windows](./windows.md) - [Telemetry](./telemetry.md) - [Workspace Persistence](./workspace-persistence.md) +- [Troubleshooting](./troubleshooting.md) - [Additional Learning Materials](./additional-learning-materials.md) # Configuration diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md new file mode 100644 index 0000000000000000000000000000000000000000..50ce1bcc40680cfaa08f56a9c7e44ce5d4df893d --- /dev/null +++ b/docs/src/troubleshooting.md @@ -0,0 +1,16 @@ +# Troubleshooting + +## Zed Log + +Often, a good first place to look when troubleshooting any issue in Zed is the Zed log, which might contain clues about what's going wrong. +You can review the most recent 1000 lines of the log by running the {#action zed::OpenLog} command from the command palette (`cmd-shift-p` on macOS or `ctrl-shift-p` on Windows/Linux). +If you want to view the full file, you can find it at the respective location on each operating system: + +- macOS: `~/Library/Logs/Zed/Zed.log` +- Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log` +- Linux: `~/.local/share/zed/logs/Zed.log` or `$XDG_DATA_HOME` + +> Note: In some cases, it might be useful to monitor the log live, such as when [developing a Zed extension](https://zed.dev/docs/extensions/developing-extensions). +> Example: `tail -f ~/Library/Logs/Zed/Zed.log` + +The log may contain enough context to help you debug the issue yourself, or you may find specific errors that are useful when filing a [GitHub Issue](https://github.com/zed-industries/zed/issues/new/choose) or when talking to Zed staff in our [Discord server](https://zed.dev/community-links#forums-and-discussions). diff --git a/extensions/slash-commands-example/README.md b/extensions/slash-commands-example/README.md index 6ff00dd2ad673bda951ba323258cfc3db2511c90..8c16a4e168a3334d3197090837eeaf21c956b3c3 100644 --- a/extensions/slash-commands-example/README.md +++ b/extensions/slash-commands-example/README.md @@ -76,8 +76,7 @@ Rebuild to see these changes reflected: ## Troubleshooting / Logs -- MacOS: `tail -f ~/Library/Logs/Zed/Zed.log` -- Linux: `tail -f ~/.local/share/zed/logs/Zed.log` +- [zed.dev docs: Troubleshooting](https://zed.dev/docs/troubleshooting) ## Documentation From fa550de922f3fab7ba4668f3e976a80e576f8528 Mon Sep 17 00:00:00 2001 From: Ruangyot Nanchiang Date: Tue, 21 Oct 2025 23:42:43 +0700 Subject: [PATCH 111/202] Fix Git UI truncation for long branch names (#40598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #40524 Release Notes: - Fixed branch names not truncating properly in git branch picker. Please review if you have time. PS. I’m not fully sure if this completely fixes the issue, but I’ve tested it on my local build and it seems to work fine. Before Fix: 502782621-91ac0578-9f55-4fb3-b0da-49a49e862a33 After Fix: FixedRound2 --- crates/git_ui/src/branch_picker.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 7a046266ac863f78c1e449c7c1b58f0834834b2c..cf8b03d1fd249c978de9e6bbd824e9491c5d24e1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -494,8 +494,12 @@ impl PickerDelegate for BranchListDelegate { ) .into_any_element() } else { - HighlightedLabel::new(entry.branch.name().to_owned(), entry.positions.clone()) - .truncate() + h_flex() + .max_w_48() + .child( + HighlightedLabel::new(entry.branch.name().to_owned(), entry.positions.clone()) + .truncate(), + ) .into_any_element() }; From 641ae90cd62ef5ea4a59f7adbc053f130c479a1f Mon Sep 17 00:00:00 2001 From: Dong Date: Wed, 22 Oct 2025 00:45:40 +0800 Subject: [PATCH 112/202] settings_ui: Fix IEEE 754 floating point error when serializing value to JSON (#40677) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #40556 Release Notes: - settings-ui: Fixed an issue where modifying floating point number fields could result in the values written to the settings file containing IEEE 754 floating point error > [!Note] > Seems like there's another pull request fixing the centered layout in #40661 by normalizing the whole `settings.json` file when updating it. Not sure which is the better way to handle this issue. ## Description This pull request solves the IEEE 754 floating point error when serializing values from settings-ui into `settings.json` by creating a two `serde_helper` to convert the problematic f32 into a formatted two decimal placed f32. Fields currently exists the IEEE 754 error: - Appearance → Unnecessary Code Fade - Editor → Drop Size Target - Window & Layout → Centered Layout Left/Right Padding - Window & Layout → Inactive Opacity ## How to verify ### Unnecessary Code Fade As Is | To Be --- | ---
| -> Stateful
{ let element = element.pt_4(); @@ -639,12 +640,14 @@ impl SettingsPageItem { element.pb_4().border_b_1().border_color(border_variant) } }; + let mut render_setting_item_inner = - |setting_item: &SettingItem, cx: &mut Context| { + |setting_item: &SettingItem, padding: bool, cx: &mut Context| { let renderer = cx.default_global::().clone(); let (_, found) = setting_item.field.file_set_in(file.clone(), cx); let renderers = renderer.renderers.borrow(); + let field_renderer = renderers.get(&AnySettingField::type_id(setting_item.field.as_ref())); let field_renderer_or_warning = @@ -683,8 +686,15 @@ impl SettingsPageItem { ), }; - (field.map(apply_padding), field_renderer_or_warning.is_ok()) + let field = if padding { + field.map(apply_padding) + } else { + field + }; + + (field, field_renderer_or_warning.is_ok()) }; + match self { SettingsPageItem::SectionHeader(header) => v_flex() .w_full() @@ -698,15 +708,13 @@ impl SettingsPageItem { .child(Divider::horizontal().color(DividerColor::BorderFaded)) .into_any_element(), SettingsPageItem::SettingItem(setting_item) => { - render_setting_item_inner(setting_item, cx) - .0 - .into_any_element() + let (field_with_padding, _) = render_setting_item_inner(setting_item, true, cx); + field_with_padding.into_any_element() } SettingsPageItem::SubPageLink(sub_page_link) => h_flex() .id(sub_page_link.title.clone()) .w_full() .min_w_0() - .gap_2() .justify_between() .map(apply_padding) .child( @@ -725,7 +733,7 @@ impl SettingsPageItem { .icon_position(IconPosition::End) .icon_color(Color::Muted) .icon_size(IconSize::Small) - .style(ButtonStyle::Outlined) + .style(ButtonStyle::OutlinedGhost) .size(ButtonSize::Medium) .on_click({ let sub_page_link = sub_page_link.clone(); @@ -760,18 +768,42 @@ impl SettingsPageItem { let discriminant = SettingsStore::global(cx) .get_value_from_file(file, *pick_discriminant) .1; + let (discriminant_element, rendered_ok) = - render_setting_item_inner(discriminant_setting_item, cx); - let mut content = v_flex() - .gap_2() - .id("dynamic-item") - .child(discriminant_element); + render_setting_item_inner(discriminant_setting_item, true, cx); + + let has_sub_fields = + rendered_ok && discriminant.map(|d| !fields[d].is_empty()).unwrap_or(false); + + let discriminant_element = if has_sub_fields { + discriminant_element.pb_4().border_b_0() + } else { + discriminant_element + }; + + let mut content = v_flex().id("dynamic-item").child(discriminant_element); + if rendered_ok { let discriminant = discriminant.expect("This should be Some if rendered_ok is true"); let sub_fields = &fields[discriminant]; - for field in sub_fields { - content = content.child(render_setting_item_inner(field, cx).0.pl_6()); + let sub_field_count = sub_fields.len(); + + for (index, field) in sub_fields.iter().enumerate() { + let is_last_sub_field = index == sub_field_count - 1; + let (raw_field, _) = render_setting_item_inner(field, false, cx); + + content = content.child( + raw_field + .p_4() + .border_x_1() + .border_t_1() + .when(is_last_sub_field, |this| this.border_b_1()) + .when(is_last_sub_field && is_last, |this| this.mb_8()) + .border_dashed() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().element_background.opacity(0.2)), + ); } } @@ -795,7 +827,6 @@ fn render_settings_item( h_flex() .id(setting_item.title) .min_w_0() - .gap_2() .justify_between() .child( v_flex() @@ -1831,12 +1862,6 @@ impl SettingsWindow { }; v_flex() - .w_56() - .p_2p5() - .when(cfg!(target_os = "macos"), |c| c.pt_10()) - .h_full() - .flex_none() - .border_r_1() .key_context("NavigationMenu") .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| { let Some(focused_entry) = this.focused_nav_entry(window, cx) else { @@ -1954,6 +1979,12 @@ impl SettingsWindow { cx, ); })) + .w_56() + .h_full() + .p_2p5() + .when(cfg!(target_os = "macos"), |this| this.pt_10()) + .flex_none() + .border_r_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().panel_background) .child(self.render_search(window, cx)) @@ -2194,6 +2225,20 @@ impl SettingsWindow { .child(Label::new(last)) } + fn render_empty_state(&self, search_query: SharedString) -> impl IntoElement { + v_flex() + .size_full() + .items_center() + .justify_center() + .gap_1() + .child(Label::new("No Results")) + .child( + Label::new(search_query) + .size(LabelSize::Small) + .color(Color::Muted), + ) + } + fn render_page_items( &mut self, page_index: usize, @@ -2208,18 +2253,7 @@ impl SettingsWindow { if has_no_results { let search_query = self.search_bar.read(cx).text(cx); page_content = page_content.child( - v_flex() - .size_full() - .items_center() - .justify_center() - .gap_1() - .child(div().child("No Results")) - .child( - div() - .text_sm() - .text_color(cx.theme().colors().text_muted) - .child(format!("No settings match \"{}\"", search_query)), - ), + self.render_empty_state(format!("No settings match \"{}\"", search_query).into()), ) } else { let last_non_header_index = self @@ -2249,6 +2283,7 @@ impl SettingsWindow { }) .into_any_element(); } + let mut visible_items = this.visible_page_items(); let Some((actual_item_index, item)) = visible_items.nth(index - 1) else { return gpui::Empty.into_any_element(); @@ -2258,6 +2293,7 @@ impl SettingsWindow { .next() .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_))) .unwrap_or(false); + let is_last = Some(actual_item_index) == last_non_header_index; let item_focus_handle = @@ -2307,18 +2343,7 @@ impl SettingsWindow { if has_no_results { let search_query = self.search_bar.read(cx).text(cx); page_content = page_content.child( - v_flex() - .size_full() - .items_center() - .justify_center() - .gap_1() - .child(div().child("No Results")) - .child( - div() - .text_sm() - .text_color(cx.theme().colors().text_muted) - .child(format!("No settings match \"{}\"", search_query)), - ), + self.render_empty_state(format!("No settings match \"{}\"", search_query).into()), ) } else { let last_non_header_index = items @@ -2412,11 +2437,6 @@ impl SettingsWindow { return v_flex() .id("Settings-ui-page") - .flex_1() - .pt_6() - .pb_8() - .px_8() - .bg(cx.theme().colors().editor_background) .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { if !sub_page_stack().is_empty() { window.focus_next(); @@ -2484,6 +2504,10 @@ impl SettingsWindow { this.vertical_scrollbar_for(self.sub_page_scroll_handle.clone(), window, cx) }) .track_focus(&self.content_focus_handle.focus_handle(cx)) + .flex_1() + .pt_6() + .px_8() + .bg(cx.theme().colors().editor_background) .child( div() .size_full() From a398f80ba6ddb4034b32ce88585d7aab2393a5ca Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 21 Oct 2025 14:38:11 -0400 Subject: [PATCH 115/202] Add an action to reveal log file in system file manager (#40815) We document the location of the log file in many places, we should just make it easy to open directly within your file browser. The one thing here is naming. We use dynamic naming for "reveal" actions in the project panel, to reflect the right file manager name per OS, but for a command palette action, I dont think we want to have dynamic code for the action name, just going with finder at the moment. Release Notes: - Added a `zed: reveal log in file manager` action to the command palette. --- crates/workspace/src/workspace.rs | 4 +++- crates/zed/src/zed.rs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f3b7fea96d365bb6d12f6eea0cc93518f2683dcb..3dc3b781175cd2f533184735d7759cb27c34930a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7015,7 +7015,9 @@ actions!( zed, [ /// Opens the Zed log file. - OpenLog + OpenLog, + /// Reveals the Zed log file in the system file manager. + RevealLogInFileManager ] ); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 62cf52de19469f7087c42f91363bc9002312fe1f..3cc6ec86473f09640f2ff35ae0457db65759d7c1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -178,6 +178,9 @@ pub fn init(cx: &mut App) { open_log_file(workspace, window, cx); }); }); + cx.on_action(|_: &workspace::RevealLogInFileManager, cx| { + cx.reveal_path(paths::log_file().as_path()); + }); cx.on_action(|_: &zed_actions::OpenLicenses, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_bundled_file( From 4b429033e74e8b6f5721d87ca9cd80af33d275e3 Mon Sep 17 00:00:00 2001 From: Delvin <64721581+delvin02@users.noreply.github.com> Date: Wed, 22 Oct 2025 05:50:42 +1030 Subject: [PATCH 116/202] settings_ui: Correct stepper increment and enforce max value for Centered Layout Padding (#40751) Closes #40748 This PR improves the Centered Layout Padding in settings ui by limiting the numeric stepper to be within valid values and adding a custom schema generated to improve the JSON LSP completions and warnings when editing the setting field manually. Release Notes: - settings ui: limit stepper increment for centered padding between 0 and 0.4 and increment by 0.05 now --------- Co-authored by: Anthony Eid --- .../settings/src/settings_content/editor.rs | 62 +++++++++++++++++++ .../src/settings_content/workspace.rs | 12 ++-- crates/settings_ui/src/settings_ui.rs | 1 + crates/ui_input/src/number_field.rs | 10 ++- crates/workspace/src/workspace.rs | 20 +++--- 5 files changed, 89 insertions(+), 16 deletions(-) diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs index f5ec805c72d054744477c73cefa50b9bc3fcc7d1..920f02a0f6597454c82d421247787e8ad6f7f74b 100644 --- a/crates/settings/src/settings_content/editor.rs +++ b/crates/settings/src/settings_content/editor.rs @@ -850,3 +850,65 @@ impl From for InactiveOpacity { Self(x) } } + +/// Centered layout related setting (left/right). +/// +/// Valid range: 0.0 to 0.4 +/// Default: 2.0 +#[derive( + Clone, + Copy, + Debug, + Serialize, + Deserialize, + MergeFrom, + PartialEq, + PartialOrd, + derive_more::FromStr, +)] +#[serde(transparent)] +pub struct CenteredPaddingSettings( + #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32, +); + +impl CenteredPaddingSettings { + pub const MIN_PADDING: f32 = 0.0; + // This is an f64 so serde_json can give a type hint without random numbers in the back + pub const DEFAULT_PADDING: f64 = 0.2; + pub const MAX_PADDING: f32 = 0.4; +} + +impl Display for CenteredPaddingSettings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:.2}", self.0) + } +} + +impl From for CenteredPaddingSettings { + fn from(x: f32) -> Self { + Self(x) + } +} + +impl Default for CenteredPaddingSettings { + fn default() -> Self { + Self(Self::DEFAULT_PADDING as f32) + } +} + +impl schemars::JsonSchema for CenteredPaddingSettings { + fn schema_name() -> std::borrow::Cow<'static, str> { + "CenteredPaddingSettings".into() + } + + fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { + use schemars::json_schema; + json_schema!({ + "type": "number", + "minimum": Self::MIN_PADDING, + "maximum": Self::MAX_PADDING, + "default": Self::DEFAULT_PADDING, + "description": "Centered layout related setting (left/right)." + }) + } +} diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs index a76a1347d4431d9479abed6ab7aa5a893f0a438e..c901d7010b37c685180ca67a3c4775da41be87ee 100644 --- a/crates/settings/src/settings_content/workspace.rs +++ b/crates/settings/src/settings_content/workspace.rs @@ -7,8 +7,8 @@ use serde_with::skip_serializing_none; use settings_macros::MergeFrom; use crate::{ - DelayMs, DockPosition, DockSide, InactiveOpacity, ScrollbarSettingsContent, ShowIndentGuides, - serialize_optional_f32_with_two_decimal_places, + CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity, + ScrollbarSettingsContent, ShowIndentGuides, serialize_optional_f32_with_two_decimal_places, }; #[skip_serializing_none] @@ -463,22 +463,20 @@ pub enum PaneSplitDirectionVertical { Right, } -#[skip_serializing_none] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] #[serde(rename_all = "snake_case")] +#[skip_serializing_none] pub struct CenteredLayoutSettings { /// The relative width of the left padding of the central pane from the /// workspace when the centered layout is used. /// /// Default: 0.2 - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - pub left_padding: Option, + pub left_padding: Option, // The relative width of the right padding of the central pane from the // workspace when the centered layout is used. /// /// Default: 0.2 - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - pub right_padding: Option, + pub right_padding: Option, } #[derive( diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 0c287a16fa6fce5182eb01040571ee8ac68624b7..e4b92464766e4d87e9f3afe6d1d639716b5ac5ab 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -420,6 +420,7 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) + .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_number_field) .add_basic_renderer::(render_dropdown) diff --git a/crates/ui_input/src/number_field.rs b/crates/ui_input/src/number_field.rs index 3ae1d77c0400ea474864087bf3a4a5f4705a2e41..929b3851a9127f7ba8f44d954f32ddedcdfa68b6 100644 --- a/crates/ui_input/src/number_field.rs +++ b/crates/ui_input/src/number_field.rs @@ -8,7 +8,7 @@ use std::{ use editor::{Editor, EditorStyle}; use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers}; -use settings::{CodeFade, DelayMs, InactiveOpacity, MinimumContrast}; +use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast}; use ui::prelude::*; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] @@ -71,6 +71,14 @@ impl_newtype_numeric_stepper!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9); impl_newtype_numeric_stepper!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0); impl_newtype_numeric_stepper!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0); impl_newtype_numeric_stepper!(DelayMs, 100, 500, 10, 0, 2000); +impl_newtype_numeric_stepper!( + CenteredPaddingSettings, + 0.05, + 0.2, + 0.1, + CenteredPaddingSettings::MIN_PADDING, + CenteredPaddingSettings::MAX_PADDING +); macro_rules! impl_numeric_stepper_int { ($type:ident) => { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3dc3b781175cd2f533184735d7759cb27c34930a..2c0a024158733860b6adf956e617382b16e74b16 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -83,7 +83,7 @@ use remote::{ use schemars::JsonSchema; use serde::Deserialize; use session::AppSession; -use settings::{Settings, SettingsLocation, update_settings_file}; +use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file}; use shared_screen::SharedScreen; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -1199,9 +1199,6 @@ struct FollowerView { } impl Workspace { - const DEFAULT_PADDING: f32 = 0.2; - const MAX_PADDING: f32 = 0.4; - pub fn new( workspace_id: Option, project: Entity, @@ -5938,8 +5935,11 @@ impl Workspace { fn adjust_padding(padding: Option) -> f32 { padding - .unwrap_or(Self::DEFAULT_PADDING) - .clamp(0.0, Self::MAX_PADDING) + .unwrap_or(CenteredPaddingSettings::default().0) + .clamp( + CenteredPaddingSettings::MIN_PADDING, + CenteredPaddingSettings::MAX_PADDING, + ) } fn render_dock( @@ -6423,8 +6423,12 @@ impl Render for Workspace { let paddings = if centered_layout { let settings = WorkspaceSettings::get_global(cx).centered_layout; ( - render_padding(Self::adjust_padding(settings.left_padding)), - render_padding(Self::adjust_padding(settings.right_padding)), + render_padding(Self::adjust_padding( + settings.left_padding.map(|padding| padding.0), + )), + render_padding(Self::adjust_padding( + settings.right_padding.map(|padding| padding.0), + )), ) } else { (None, None) From fd100178377c074369d75d86a85b4d1467c92885 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 21 Oct 2025 15:44:01 -0400 Subject: [PATCH 117/202] docs: Add model prices for Claude Haiku 4.5 (#40820) This PR adds the model prices for Claude Haiku 4.5 to the docs. Release Notes: - N/A --- docs/src/ai/models.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/ai/models.md b/docs/src/ai/models.md index 57a12a8313d17f68f7a377060afbc34d14fab64f..5b379fc75435c14ac46587f7449c7a5c54becfcf 100644 --- a/docs/src/ai/models.md +++ b/docs/src/ai/models.md @@ -21,6 +21,10 @@ We’re working hard to expand the models supported by Zed’s subscription offe | | Anthropic | Output | $15.00 | $16.50 | | | Anthropic | Input - Cache Write | $3.75 | $4.125 | | | Anthropic | Input - Cache Read | $0.30 | $0.33 | +| Claude Haiku 4.5 | Anthropic | Input | $1.00 | $1.10 | +| | Anthropic | Output | $5.00 | $5.50 | +| | Anthropic | Input - Cache Write | $1.25 | $1.375 | +| | Anthropic | Input - Cache Read | $0.10 | $0.11 | | GPT-5 | OpenAI | Input | $1.25 | $1.375 | | | OpenAI | Output | $10.00 | $11.00 | | | OpenAI | Cached Input | $0.125 | $0.1375 | @@ -62,6 +66,7 @@ A context window is the maximum span of text and code an LLM can consider at onc | Claude Opus 4.1 | Anthropic | 200k | | Claude Sonnet 4 | Anthropic | 200k | | Claude Sonnet 3.7 | Anthropic | 200k | +| Claude Haiku 4.5 | Anthropic | 200k | | GPT-5 | OpenAI | 400k | | GPT-5 mini | OpenAI | 400k | | GPT-5 nano | OpenAI | 400k | From e49edfac749b4b3f182d32a93836437061ef10bc Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Tue, 21 Oct 2025 22:57:31 +0200 Subject: [PATCH 118/202] python: Init venv/virtualenv activation scripts during list/resolve (#40816) This means that existence of activation scripts for venv/virtualenv will be checked locally either on the host if editing locally, or the remote by the remote proxy if editing a remote project. Closes https://github.com/zed-industries/zed/issues/40263 Release Notes: - N/A --- crates/language/src/toolchain.rs | 10 +- crates/languages/src/python.rs | 149 +++++++++++++------ crates/project/src/project.rs | 1 + crates/project/src/project_tests.rs | 4 +- crates/project/src/terminals.rs | 14 +- crates/project/src/toolchain_store.rs | 21 ++- crates/remote_server/src/headless_project.rs | 1 + crates/util/src/shell.rs | 3 +- 8 files changed, 131 insertions(+), 72 deletions(-) diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index d3466307f368e7008eedbc8881aa78ab854bc08b..2896d4827c5e16047a471138122ef0256a24480e 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -98,6 +98,7 @@ pub trait ToolchainLister: Send + Sync + 'static { worktree_root: PathBuf, subroot_relative_path: Arc, project_env: Option>, + fs: &dyn Fs, ) -> ToolchainList; /// Given a user-created toolchain, resolve lister-specific details. @@ -106,14 +107,11 @@ pub trait ToolchainLister: Send + Sync + 'static { &self, path: PathBuf, project_env: Option>, + fs: &dyn Fs, ) -> anyhow::Result; - async fn activation_script( - &self, - toolchain: &Toolchain, - shell: ShellKind, - fs: &dyn Fs, - ) -> Vec; + fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec; + /// Returns various "static" bits of information about this toolchain lister. This function should be pure. fn meta(&self) -> ToolchainMetadata; } diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index d0bdff37676c0a7fc6e4ba4930ad461497dd790d..a9300dbb5ddca610e8d946b0cec211a7d9aa28df 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -19,6 +19,7 @@ use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind}; use pet_virtualenv::is_virtualenv_dir; use project::Fs; use project::lsp_store::language_server_settings; +use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use smol::lock::OnceCell; use std::cmp::Ordering; @@ -39,6 +40,14 @@ use std::{ use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName}; use util::{ResultExt, maybe}; +#[derive(Debug, Serialize, Deserialize)] +pub(crate) struct PythonToolchainData { + #[serde(flatten)] + environment: PythonEnvironment, + #[serde(skip_serializing_if = "Option::is_none")] + activation_scripts: Option>, +} + pub(crate) struct PyprojectTomlManifestProvider; impl ManifestProvider for PyprojectTomlManifestProvider { @@ -165,11 +174,12 @@ impl LspAdapter for TyLspAdapter { })? .unwrap_or_else(|| json!({})); if let Some(toolchain) = toolchain.and_then(|toolchain| { - serde_json::from_value::(toolchain.as_json).ok() + serde_json::from_value::(toolchain.as_json).ok() }) { _ = maybe!({ - let uri = url::Url::from_file_path(toolchain.executable?).ok()?; - let sys_prefix = toolchain.prefix.clone()?; + let uri = + url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?; + let sys_prefix = toolchain.environment.prefix.clone()?; let environment = json!({ "executable": { "uri": uri, @@ -474,9 +484,8 @@ impl LspAdapter for PyrightLspAdapter { // If we have a detected toolchain, configure Pyright to use it if let Some(toolchain) = toolchain - && let Ok(env) = serde_json::from_value::< - pet_core::python_environment::PythonEnvironment, - >(toolchain.as_json.clone()) + && let Ok(env) = + serde_json::from_value::(toolchain.as_json.clone()) { if !user_settings.is_object() { user_settings = Value::Object(serde_json::Map::default()); @@ -484,7 +493,7 @@ impl LspAdapter for PyrightLspAdapter { let object = user_settings.as_object_mut().unwrap(); let interpreter_path = toolchain.path.to_string(); - if let Some(venv_dir) = env.prefix { + if let Some(venv_dir) = &env.environment.prefix { // Set venvPath and venv at the root level // This matches the format of a pyrightconfig.json file if let Some(parent) = venv_dir.parent() { @@ -1023,6 +1032,7 @@ impl ToolchainLister for PythonToolchainProvider { worktree_root: PathBuf, subroot_relative_path: Arc, project_env: Option>, + fs: &dyn Fs, ) -> ToolchainList { let env = project_env.unwrap_or_default(); let environment = EnvironmentApi::from_env(&env); @@ -1114,13 +1124,16 @@ impl ToolchainLister for PythonToolchainProvider { .then_with(exe_ordering) }); - let mut toolchains: Vec<_> = toolchains - .into_iter() - .filter_map(venv_to_toolchain) - .collect(); - toolchains.dedup(); + let mut out_toolchains = Vec::new(); + for toolchain in toolchains { + let Some(toolchain) = venv_to_toolchain(toolchain, fs).await else { + continue; + }; + out_toolchains.push(toolchain); + } + out_toolchains.dedup(); ToolchainList { - toolchains, + toolchains: out_toolchains, default: None, groups: Default::default(), } @@ -1139,6 +1152,7 @@ impl ToolchainLister for PythonToolchainProvider { &self, path: PathBuf, env: Option>, + fs: &dyn Fs, ) -> anyhow::Result { let env = env.unwrap_or_default(); let environment = EnvironmentApi::from_env(&env); @@ -1150,58 +1164,48 @@ impl ToolchainLister for PythonToolchainProvider { let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment) .context("Could not find a virtual environment in provided path")?; let venv = toolchain.resolved.unwrap_or(toolchain.discovered); - venv_to_toolchain(venv).context("Could not convert a venv into a toolchain") + venv_to_toolchain(venv, fs) + .await + .context("Could not convert a venv into a toolchain") } - async fn activation_script( - &self, - toolchain: &Toolchain, - shell: ShellKind, - fs: &dyn Fs, - ) -> Vec { - let Ok(toolchain) = serde_json::from_value::( - toolchain.as_json.clone(), - ) else { + fn activation_script(&self, toolchain: &Toolchain, shell: ShellKind) -> Vec { + let Ok(toolchain) = + serde_json::from_value::(toolchain.as_json.clone()) + else { return vec![]; }; + + log::debug!("(Python) Composing activation script for toolchain {toolchain:?}"); + let mut activation_script = vec![]; - match toolchain.kind { + match toolchain.environment.kind { Some(PythonEnvironmentKind::Conda) => { - if let Some(name) = &toolchain.name { + if let Some(name) = &toolchain.environment.name { activation_script.push(format!("conda activate {name}")); } else { activation_script.push("conda activate".to_string()); } } Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => { - if let Some(prefix) = &toolchain.prefix { - let activate_keyword = shell.activate_keyword(); - let activate_script_name = match shell { - ShellKind::Posix | ShellKind::Rc => "activate", - ShellKind::Csh => "activate.csh", - ShellKind::Tcsh => "activate.csh", - ShellKind::Fish => "activate.fish", - ShellKind::Nushell => "activate.nu", - ShellKind::PowerShell => "activate.ps1", - ShellKind::Cmd => "activate.bat", - ShellKind::Xonsh => "activate.xsh", - }; - let path = prefix.join(BINARY_DIR).join(activate_script_name); - - if let Some(quoted) = shell.try_quote(&path.to_string_lossy()) - && fs.is_file(&path).await - { - activation_script.push(format!("{activate_keyword} {quoted}")); + if let Some(activation_scripts) = &toolchain.activation_scripts { + if let Some(activate_script_path) = activation_scripts.get(&shell) { + let activate_keyword = shell.activate_keyword(); + if let Some(quoted) = + shell.try_quote(&activate_script_path.to_string_lossy()) + { + activation_script.push(format!("{activate_keyword} {quoted}")); + } } } } Some(PythonEnvironmentKind::Pyenv) => { - let Some(manager) = toolchain.manager else { + let Some(manager) = &toolchain.environment.manager else { return vec![]; }; - let version = toolchain.version.as_deref().unwrap_or("system"); - let pyenv = manager.executable; + let version = toolchain.environment.version.as_deref().unwrap_or("system"); + let pyenv = &manager.executable; let pyenv = pyenv.display(); activation_script.extend(match shell { ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")), @@ -1221,7 +1225,7 @@ impl ToolchainLister for PythonToolchainProvider { } } -fn venv_to_toolchain(venv: PythonEnvironment) -> Option { +async fn venv_to_toolchain(venv: PythonEnvironment, fs: &dyn Fs) -> Option { let mut name = String::from("Python"); if let Some(ref version) = venv.version { _ = write!(name, " {version}"); @@ -1238,14 +1242,61 @@ fn venv_to_toolchain(venv: PythonEnvironment) -> Option { _ = write!(name, " {nk}"); } + let mut activation_scripts = HashMap::default(); + match venv.kind { + Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => { + resolve_venv_activation_scripts(&venv, fs, &mut activation_scripts).await + } + _ => {} + } + let data = PythonToolchainData { + environment: venv, + activation_scripts: Some(activation_scripts), + }; + Some(Toolchain { name: name.into(), - path: venv.executable.as_ref()?.to_str()?.to_owned().into(), + path: data + .environment + .executable + .as_ref()? + .to_str()? + .to_owned() + .into(), language_name: LanguageName::new("Python"), - as_json: serde_json::to_value(venv).ok()?, + as_json: serde_json::to_value(data).ok()?, }) } +async fn resolve_venv_activation_scripts( + venv: &PythonEnvironment, + fs: &dyn Fs, + activation_scripts: &mut HashMap, +) { + log::debug!("(Python) Resolving activation scripts for venv toolchain {venv:?}"); + if let Some(prefix) = &venv.prefix { + for (shell_kind, script_name) in &[ + (ShellKind::Posix, "activate"), + (ShellKind::Rc, "activate"), + (ShellKind::Csh, "activate.csh"), + (ShellKind::Tcsh, "activate.csh"), + (ShellKind::Fish, "activate.fish"), + (ShellKind::Nushell, "activate.nu"), + (ShellKind::PowerShell, "activate.ps1"), + (ShellKind::Cmd, "activate.bat"), + (ShellKind::Xonsh, "activate.xsh"), + ] { + let path = prefix.join(BINARY_DIR).join(script_name); + + log::debug!("Trying path: {}", path.display()); + + if fs.is_file(&path).await { + activation_scripts.insert(*shell_kind, path); + } + } + } +} + pub struct EnvironmentApi<'a> { global_search_locations: Arc>>, project_env: &'a HashMap, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c0d853966694a68fad9d69ad160071c3d5fca9bf..1fd4bcd583908f0428586b988d82808598a501b3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1060,6 +1060,7 @@ impl Project { worktree_store.clone(), environment.clone(), manifest_tree.clone(), + fs.clone(), cx, ) }); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 43f233b2d1fdb8ffdeca6fe7e40d7c28a8e3084c..7f504a676c8ef6bd46efdd5f4fd570e69921d652 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -9653,6 +9653,7 @@ fn python_lang(fs: Arc) -> Arc { worktree_root: PathBuf, subroot_relative_path: Arc, _: Option>, + _: &dyn Fs, ) -> ToolchainList { // This lister will always return a path .venv directories within ancestors let ancestors = subroot_relative_path.ancestors().collect::>(); @@ -9677,6 +9678,7 @@ fn python_lang(fs: Arc) -> Arc { &self, _: PathBuf, _: Option>, + _: &dyn Fs, ) -> anyhow::Result { Err(anyhow::anyhow!("Not implemented")) } @@ -9689,7 +9691,7 @@ fn python_lang(fs: Arc) -> Arc { manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")), } } - async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec { + fn activation_script(&self, _: &Toolchain, _: ShellKind) -> Vec { vec![] } } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index dd69050a53d8e5477e5bf8be5e5d3a2a86e92af5..360391210c75d88e67b1e08be3e0c865b33397c8 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -120,7 +120,6 @@ impl Project { .map(|p| self.active_toolchain(p, LanguageName::new("Python"), cx)) .collect::>(); let lang_registry = self.languages.clone(); - let fs = self.fs.clone(); cx.spawn(async move |project, cx| { let shell_kind = ShellKind::new(&shell, is_windows); let activation_script = maybe!(async { @@ -133,11 +132,7 @@ impl Project { .await .ok(); let lister = language?.toolchain_lister(); - return Some( - lister? - .activation_script(&toolchain, shell_kind, fs.as_ref()) - .await, - ); + return Some(lister?.activation_script(&toolchain, shell_kind)); } None }) @@ -347,7 +342,6 @@ impl Project { let shell_kind = ShellKind::new(&shell, self.path_style(cx).is_windows()); let lang_registry = self.languages.clone(); - let fs = self.fs.clone(); cx.spawn(async move |project, cx| { let activation_script = maybe!(async { for toolchain in toolchains { @@ -359,11 +353,7 @@ impl Project { .await .ok(); let lister = language?.toolchain_lister(); - return Some( - lister? - .activation_script(&toolchain, shell_kind, fs.as_ref()) - .await, - ); + return Some(lister?.activation_script(&toolchain, shell_kind)); } None }) diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 64a167206864a4bffe488fbd70da01c6c3a993b0..d1c4fc629698bb70d156786837bc2540533d4867 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -4,6 +4,7 @@ use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; use collections::{BTreeMap, IndexSet}; +use fs::Fs; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -60,6 +61,7 @@ impl ToolchainStore { worktree_store: Entity, project_environment: Entity, manifest_tree: Entity, + fs: Arc, cx: &mut Context, ) -> Self { let entity = cx.new(|_| LocalToolchainStore { @@ -68,6 +70,7 @@ impl ToolchainStore { project_environment, active_toolchains: Default::default(), manifest_tree, + fs, }); let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { cx.emit(e.clone()) @@ -397,6 +400,7 @@ pub struct LocalToolchainStore { project_environment: Entity, active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap, Toolchain>>, manifest_tree: Entity, + fs: Arc, } #[async_trait(?Send)] @@ -485,6 +489,7 @@ impl LocalToolchainStore { let registry = self.languages.clone(); let manifest_tree = self.manifest_tree.downgrade(); + let fs = self.fs.clone(); let environment = self.project_environment.clone(); cx.spawn(async move |this, cx| { @@ -534,7 +539,12 @@ impl LocalToolchainStore { cx.background_spawn(async move { Some(( toolchains - .list(worktree_root, relative_path.path.clone(), project_env) + .list( + worktree_root, + relative_path.path.clone(), + project_env, + fs.as_ref(), + ) .await, relative_path.path, )) @@ -568,6 +578,7 @@ impl LocalToolchainStore { ) -> Task> { let registry = self.languages.clone(); let environment = self.project_environment.clone(); + let fs = self.fs.clone(); cx.spawn(async move |_, cx| { let language = cx .background_spawn(registry.language_for_name(&language_name.0)) @@ -586,8 +597,12 @@ impl LocalToolchainStore { ) })? .await; - cx.background_spawn(async move { toolchain_lister.resolve(path, project_env).await }) - .await + cx.background_spawn(async move { + toolchain_lister + .resolve(path, project_env, fs.as_ref()) + .await + }) + .await }) } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 2f429ed80aa4bf914e2cf3c6a03ad3e64f39ce81..caec0f5c1a4829ad0117469d705567ccf557ef46 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -102,6 +102,7 @@ impl HeadlessProject { worktree_store.clone(), environment.clone(), manifest_tree.clone(), + fs.clone(), cx, ) }); diff --git a/crates/util/src/shell.rs b/crates/util/src/shell.rs index a031f98fc27134a142af1cbaa9de341d1413188f..22e07acf25b46161138a297e6de701f74b483861 100644 --- a/crates/util/src/shell.rs +++ b/crates/util/src/shell.rs @@ -1,6 +1,7 @@ +use serde::{Deserialize, Serialize}; use std::{borrow::Cow, fmt, path::Path, sync::LazyLock}; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ShellKind { #[default] Posix, From 256fe6e45c44b6390ea73946dbf97992f502c6ca Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 22 Oct 2025 00:08:56 +0300 Subject: [PATCH 119/202] Fix task terminal split (#40824) Before: https://github.com/user-attachments/assets/efe2aeb6-94bb-46b6-944f-5a6345c072b4 After: https://github.com/user-attachments/assets/61a7f699-6b4d-465f-add1-07774068420c Release Notes: - Fixed task terminal split not working correctly --- crates/project/src/terminals.rs | 6 ++++++ crates/terminal/src/terminal.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 360391210c75d88e67b1e08be3e0c865b33397c8..4a0a1790b49449fd82b8aeff58f6c11c8e63261b 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -417,6 +417,12 @@ impl Project { cx: &mut Context<'_, Project>, cwd: Option, ) -> Task>> { + // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable. + // For now, create a new shell instead. + if terminal.read(cx).task().is_some() { + return self.create_terminal_shell(cwd, cx); + } + let local_path = if self.is_via_remote_server() { None } else { diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e35a3ae4e14dec8c510e4858adaeabb789436de0..fa42a94e932a81d171ffc871393a30abf965678f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -454,7 +454,7 @@ impl TerminalBuilder { args: Option>, title_override: Option, ) -> Self { - log::info!("Using {program} as shell"); + log::debug!("Using {program} as shell"); Self { program, args, From fcecf379dc87da521ce9cccda4995bb18eb94950 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 21 Oct 2025 17:49:29 -0400 Subject: [PATCH 120/202] Switch to fork of `async-tar` (#40828) This PR switches to our own fork of `async-tar`. Release Notes: - N/A --- Cargo.lock | 3 +-- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 480358bff1428404d28197df55b2566c098a373f..d116940e5de7968cba610eaf842c6bcdb334df77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,8 +1185,7 @@ dependencies = [ [[package]] name = "async-tar" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a42f905d4f623faf634bbd1e001e84e0efc24694afa64be9ad239bf6ca49e1f8" +source = "git+https://github.com/zed-industries/async-tar?rev=8af312477196311c9ea4097f2a22022f6d609bf6#8af312477196311c9ea4097f2a22022f6d609bf6" dependencies = [ "async-std", "filetime", diff --git a/Cargo.toml b/Cargo.toml index f9eecd25cb11701bdc1f22d8b6d79cd2a572e296..9637569566e7d3ebc8035d7a93b6e35d2ef84b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -454,7 +454,7 @@ async-fs = "2.1" async-lock = "2.1" async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" -async-tar = "0.5.0" +async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "8af312477196311c9ea4097f2a22022f6d609bf6" } async-task = "4.7" async-trait = "0.1" async-tungstenite = "0.31.0" From 2bba3358b8b83f2d2d14e5e76d0ee6c39b4ceed5 Mon Sep 17 00:00:00 2001 From: John Tur Date: Tue, 21 Oct 2025 17:51:54 -0400 Subject: [PATCH 121/202] Add Windows Arm64 builds to CI (#40821) Closes https://github.com/zed-industries/zed/issues/40378 Release Notes: - N/A --------- Co-authored-by: Julia Ryan --- .github/workflows/ci.yml | 61 ++++++++++++++++- .github/workflows/release_nightly.yml | 46 ++++++++++++- crates/zed/resources/windows/zed.iss | 3 + script/bundle-windows.ps1 | 99 ++++++++++++++++++++------- 4 files changed, 180 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ebbcaba49823787aafe40e5f3dd80eb67478b42..e594cdcfff4e5ba2383cee4d2b4551ea86d9e8d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -789,7 +789,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 - name: Create a Windows installer + name: Create a Windows installer for x86_64 runs-on: [self-32vcpu-windows-2022] if: | ( startsWith(github.ref, 'refs/tags/v') @@ -844,13 +844,70 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + bundle-windows-aarch64: + timeout-minutes: 120 + name: Create a Windows installer for aarch64 + runs-on: [self-32vcpu-windows-2022] + if: | + ( startsWith(github.ref, 'refs/tags/v') + || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) + needs: [windows_tests] + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} + ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} + CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} + ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} + FILE_DIGEST: SHA256 + TIMESTAMP_DIGEST: SHA256 + TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + clean: false + + - name: Setup Sentry CLI + uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 + with: + token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} + + - name: Determine version and release channel + working-directory: ${{ env.ZED_WORKSPACE }} + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + # This exports RELEASE_CHANNEL into env (GITHUB_ENV) + script/determine-release-channel.ps1 + + - name: Build Zed installer + working-directory: ${{ env.ZED_WORKSPACE }} + run: script/bundle-windows.ps1 -Architecture aarch64 + + - name: Upload installer (aarch64) to Workflow - zed (run-bundling) + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + if: contains(github.event.pull_request.labels.*.name, 'run-bundling') + with: + name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.exe + path: ${{ env.SETUP_PATH }} + + - name: Upload Artifacts to release + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 + if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }} + with: + draft: true + prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} + files: ${{ env.SETUP_PATH }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + auto-release-preview: name: Auto release preview if: | false && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') - needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64] + needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, bundle-windows-aarch64] runs-on: - self-mini-macos steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 2026ee7b730698cd7e40eebcd141f5b8a6ee9d04..a6cfd9c43b55c2cd57bb4b87485ddc1b82f0b82a 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -246,7 +246,7 @@ jobs: bundle-windows-x64: timeout-minutes: 60 - name: Create a Windows installer + name: Create a Windows installer for x86_64 if: github.repository_owner == 'zed-industries' runs-on: [self-32vcpu-windows-2022] needs: windows-tests @@ -287,6 +287,49 @@ jobs: working-directory: ${{ env.ZED_WORKSPACE }} run: script/upload-nightly.ps1 windows + bundle-windows-arm64: + timeout-minutes: 60 + name: Create a Windows installer for aarch64 + if: github.repository_owner == 'zed-industries' + runs-on: [self-32vcpu-windows-2022] + needs: windows-tests + env: + AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} + ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} + CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} + ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} + FILE_DIGEST: SHA256 + TIMESTAMP_DIGEST: SHA256 + TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" + steps: + - name: Checkout repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + clean: false + + - name: Set release channel to nightly + working-directory: ${{ env.ZED_WORKSPACE }} + run: | + $ErrorActionPreference = "Stop" + $version = git rev-parse --short HEAD + Write-Host "Publishing version: $version on release channel nightly" + "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" + + - name: Setup Sentry CLI + uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 + with: + token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} + + - name: Build Zed installer + working-directory: ${{ env.ZED_WORKSPACE }} + run: script/bundle-windows.ps1 -Architecture aarch64 + + - name: Upload Zed Nightly + working-directory: ${{ env.ZED_WORKSPACE }} + run: script/upload-nightly.ps1 windows + update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' @@ -296,6 +339,7 @@ jobs: - bundle-linux-x86 - bundle-linux-arm - bundle-windows-x64 + - bundle-windows-arm64 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss index b726bb1c2117b1d53f560aaff83acb370c2f2cd4..c25888becb10779b2328b1ceb4cee42601210bfa 100644 --- a/crates/zed/resources/windows/zed.iss +++ b/crates/zed/resources/windows/zed.iss @@ -31,7 +31,10 @@ WizardStyle=modern CloseApplications=force +#ifdef DefaultSign SignTool=Defaultsign +#endif + DefaultDirName={autopf}\{#AppName} PrivilegesRequired=lowest diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index f6f44307ff7c2be960b40cd837739d2657095ab2..889d3e1390828f09b070f702db2d5f5ea8e9d63c 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -2,6 +2,7 @@ Param( [Parameter()][Alias('i')][switch]$Install, [Parameter()][Alias('h')][switch]$Help, + [Parameter()][Alias('a')][string]$Architecture, [Parameter()][string]$Name ) @@ -14,12 +15,44 @@ $PSNativeCommandUseErrorActionPreference = $true $buildSuccess = $false +$OSArchitecture = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { + "X64" { "x86_64" } + "Arm64" { "aarch64" } + default { throw "Unsupported architecture" } +} + +$Architecture = if ($Architecture) { + $Architecture +} else { + $OSArchitecture +} + +$CargoOutDir = "./target/$Architecture-pc-windows-msvc/release" + +function Get-VSArch { + param( + [string]$Arch + ) + + switch ($Arch) { + "x86_64" { "amd64" } + "aarch64" { "arm64" } + } +} + +Push-Location +& "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch (Get-VSArch -Arch $Architecture) -HostArch (Get-VSArch -Arch $OSArchitecture) +Pop-Location + +$target = "$Architecture-pc-windows-msvc" + if ($Help) { Write-Output "Usage: test.ps1 [-Install] [-Help]" Write-Output "Build the installer for Windows.\n" Write-Output "Options:" - Write-Output " -Install, -i Run the installer after building." - Write-Output " -Help, -h Show this help message." + Write-Output " -Architecture, -a Which architecture to build (x86_64 or aarch64)" + Write-Output " -Install, -i Run the installer after building." + Write-Output " -Help, -h Show this help message." exit 0 } @@ -30,6 +63,10 @@ $env:RELEASE_CHANNEL = $channel Pop-Location function CheckEnvironmentVariables { + if(-not $env:CI) { + return + } + $requiredVars = @( 'ZED_WORKSPACE', 'RELEASE_VERSION', 'ZED_RELEASE_CHANNEL', 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', @@ -55,6 +92,8 @@ function PrepareForBundle { New-Item -Path "$innoDir\appx" -ItemType Directory -Force New-Item -Path "$innoDir\bin" -ItemType Directory -Force New-Item -Path "$innoDir\tools" -ItemType Directory -Force + + rustup target add $target } function GenerateLicenses { @@ -67,34 +106,34 @@ function GenerateLicenses { function BuildZedAndItsFriends { Write-Output "Building Zed and its friends, for channel: $channel" # Build zed.exe, cli.exe and auto_update_helper.exe - cargo build --release --package zed --package cli --package auto_update_helper - Copy-Item -Path ".\target\release\zed.exe" -Destination "$innoDir\Zed.exe" -Force - Copy-Item -Path ".\target\release\cli.exe" -Destination "$innoDir\cli.exe" -Force - Copy-Item -Path ".\target\release\auto_update_helper.exe" -Destination "$innoDir\auto_update_helper.exe" -Force + cargo build --release --package zed --package cli --package auto_update_helper --target $target + Copy-Item -Path ".\$CargoOutDir\zed.exe" -Destination "$innoDir\Zed.exe" -Force + Copy-Item -Path ".\$CargoOutDir\cli.exe" -Destination "$innoDir\cli.exe" -Force + Copy-Item -Path ".\$CargoOutDir\auto_update_helper.exe" -Destination "$innoDir\auto_update_helper.exe" -Force # Build explorer_command_injector.dll switch ($channel) { "stable" { - cargo build --release --features stable --no-default-features --package explorer_command_injector + cargo build --release --features stable --no-default-features --package explorer_command_injector --target $target } "preview" { - cargo build --release --features preview --no-default-features --package explorer_command_injector + cargo build --release --features preview --no-default-features --package explorer_command_injector --target $target } default { - cargo build --release --package explorer_command_injector + cargo build --release --package explorer_command_injector --target $target } } - Copy-Item -Path ".\target\release\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force + Copy-Item -Path ".\$CargoOutDir\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force } function ZipZedAndItsFriendsDebug { $items = @( - ".\target\release\zed.pdb", - ".\target\release\cli.pdb", - ".\target\release\auto_update_helper.pdb", - ".\target\release\explorer_command_injector.pdb" + ".\$CargoOutDir\zed.pdb", + ".\$CargoOutDir\cli.pdb", + ".\$CargoOutDir\auto_update_helper.pdb", + ".\$CargoOutDir\explorer_command_injector.pdb" ) - Compress-Archive -Path $items -DestinationPath ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force + Compress-Archive -Path $items -DestinationPath ".\$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force } @@ -109,7 +148,7 @@ function UploadToSentry { return } Write-Output "Uploading zed debug symbols to sentry..." - sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev .\target\release\ + sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev $CargoOutDir } function MakeAppx { @@ -132,6 +171,10 @@ function MakeAppx { } function SignZedAndItsFriends { + if (-not $env:CI) { + return + } + $files = "$innoDir\Zed.exe,$innoDir\cli.exe,$innoDir\auto_update_helper.exe,$innoDir\zed_explorer_command_injector.dll,$innoDir\zed_explorer_command_injector.appx" & "$innoDir\sign.ps1" $files } @@ -172,7 +215,7 @@ function BuildInstaller { $appIconName = "app-icon" $appName = "Zed" $appDisplayName = "Zed" - $appSetupName = "Zed-x86_64" + $appSetupName = "Zed-$Architecture" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Stable-Instance-Mutex" $appExeName = "Zed" @@ -186,7 +229,7 @@ function BuildInstaller { $appIconName = "app-icon-preview" $appName = "Zed Preview" $appDisplayName = "Zed Preview" - $appSetupName = "Zed-x86_64" + $appSetupName = "Zed-$Architecture" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Preview-Instance-Mutex" $appExeName = "Zed" @@ -200,7 +243,7 @@ function BuildInstaller { $appIconName = "app-icon-nightly" $appName = "Zed Nightly" $appDisplayName = "Zed Nightly" - $appSetupName = "Zed-x86_64" + $appSetupName = "Zed-$Architecture" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Nightly-Instance-Mutex" $appExeName = "Zed" @@ -214,7 +257,7 @@ function BuildInstaller { $appIconName = "app-icon-dev" $appName = "Zed Dev" $appDisplayName = "Zed Dev" - $appSetupName = "Zed-x86_64" + $appSetupName = "Zed-$Architecture" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Dev-Instance-Mutex" $appExeName = "Zed" @@ -252,14 +295,16 @@ function BuildInstaller { "AppxFullName" = $appAppxFullName } - $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f" - $defs = @() foreach ($key in $definitions.Keys) { $defs += "/d$key=`"$($definitions[$key])`"" } - $innoArgs = @($issFilePath) + $defs + "/sDefaultsign=`"$signTool`"" + $innoArgs = @($issFilePath) + $defs + if($env:CI) { + $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f" + $innoArgs += "/sDefaultsign=`"$signTool`"" + } # Execute Inno Setup Write-Host "🚀 Running Inno Setup: $innoSetupPath $innoArgs" @@ -278,7 +323,7 @@ function BuildInstaller { ParseZedWorkspace $innoDir = "$env:ZED_WORKSPACE\inno" -$debugArchive = ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" +$debugArchive = "$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" $debugStoreKey = "$env:ZED_RELEASE_CHANNEL/zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" CheckEnvironmentVariables @@ -293,8 +338,10 @@ DownloadConpty CollectFiles BuildInstaller -UploadToBlobStorePublic -BucketName "zed-debug-symbols" -FileToUpload $debugArchive -BlobStoreKey $debugStoreKey -UploadToSentry +if($env:CI) { + UploadToBlobStorePublic -BucketName "zed-debug-symbols" -FileToUpload $debugArchive -BlobStoreKey $debugStoreKey + UploadToSentry +} if ($buildSuccess) { Write-Output "Build successful" From 50d184b6a6b3f259045b5eb5e5405d4982371657 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:15:20 +0200 Subject: [PATCH 122/202] Revert "search: New old search implementation (#39956)" (#40831) This reverts commit 7c4fb5a899c34efaea9e52ddd84daebc8d9ccf49. Closes #40792 Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 27 - Cargo.toml | 2 - crates/project/src/buffer_store.rs | 71 +- crates/project/src/project.rs | 235 ++++-- crates/project/src/project_search.rs | 754 ------------------- crates/project/src/worktree_store.rs | 151 +++- crates/project_benchmarks/Cargo.toml | 21 - crates/project_benchmarks/LICENSE-GPL | 1 - crates/project_benchmarks/src/main.rs | 136 ---- crates/remote_server/Cargo.toml | 2 +- crates/remote_server/src/headless_project.rs | 12 +- 11 files changed, 389 insertions(+), 1023 deletions(-) delete mode 100644 crates/project/src/project_search.rs delete mode 100644 crates/project_benchmarks/Cargo.toml delete mode 120000 crates/project_benchmarks/LICENSE-GPL delete mode 100644 crates/project_benchmarks/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index d116940e5de7968cba610eaf842c6bcdb334df77..08bffae4bc9c901aa3f12d7df76cf02048f7c51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12889,23 +12889,6 @@ dependencies = [ "zlog", ] -[[package]] -name = "project_benchmarks" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "client", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "node_runtime", - "project", - "settings", - "watch", -] - [[package]] name = "project_panel" version = "0.1.0" @@ -20606,16 +20589,6 @@ dependencies = [ "zlog", ] -[[package]] -name = "worktree_benchmarks" -version = "0.1.0" -dependencies = [ - "fs", - "gpui", - "settings", - "worktree", -] - [[package]] name = "writeable" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index 9637569566e7d3ebc8035d7a93b6e35d2ef84b70..f5b9a809de680b89c1d989dc8541c4aa308f24bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,7 +126,6 @@ members = [ "crates/picker", "crates/prettier", "crates/project", - "crates/project_benchmarks", "crates/project_panel", "crates/project_symbols", "crates/prompt_store", @@ -195,7 +194,6 @@ members = [ "crates/web_search_providers", "crates/workspace", "crates/worktree", - "crates/worktree_benchmarks", "crates/x_ai", "crates/zed", "crates/zed_actions", diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 51bca611f4b69546cc358cb59724dbb7f98d219e..442cd35dc1b171a1510439f5314d3f543293350f 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,12 +1,14 @@ use crate::{ - ProjectPath, + ProjectItem as _, ProjectPath, lsp_store::OpenLspBufferHandle, + search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, }; use anyhow::{Context as _, Result, anyhow}; use client::Client; use collections::{HashMap, HashSet, hash_map}; -use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; +use fs::Fs; +use futures::{Future, FutureExt as _, StreamExt, channel::oneshot, future::Shared}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; @@ -21,8 +23,8 @@ use rpc::{ AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope, proto::{self}, }; - -use std::{io, sync::Arc, time::Instant}; +use smol::channel::Receiver; +use std::{io, pin::pin, sync::Arc, time::Instant}; use text::{BufferId, ReplicaId}; use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, paths::PathStyle, rel_path::RelPath}; use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; @@ -973,10 +975,6 @@ impl BufferStore { .filter_map(|buffer| buffer.upgrade()) } - pub(crate) fn is_searchable(&self, id: &BufferId) -> bool { - !self.non_searchable_buffers.contains(&id) - } - pub fn loading_buffers( &self, ) -> impl Iterator>>)> { @@ -1101,6 +1099,63 @@ impl BufferStore { Some(()) } + pub fn find_search_candidates( + &mut self, + query: &SearchQuery, + mut limit: usize, + fs: Arc, + cx: &mut Context, + ) -> Receiver> { + let (tx, rx) = smol::channel::unbounded(); + let mut open_buffers = HashSet::default(); + let mut unnamed_buffers = Vec::new(); + for handle in self.buffers() { + let buffer = handle.read(cx); + if self.non_searchable_buffers.contains(&buffer.remote_id()) { + continue; + } else if let Some(entry_id) = buffer.entry_id(cx) { + open_buffers.insert(entry_id); + } else { + limit = limit.saturating_sub(1); + unnamed_buffers.push(handle) + }; + } + + const MAX_CONCURRENT_BUFFER_OPENS: usize = 64; + let project_paths_rx = self + .worktree_store + .update(cx, |worktree_store, cx| { + worktree_store.find_search_candidates(query.clone(), limit, open_buffers, fs, cx) + }) + .chunks(MAX_CONCURRENT_BUFFER_OPENS); + + cx.spawn(async move |this, cx| { + for buffer in unnamed_buffers { + tx.send(buffer).await.ok(); + } + + let mut project_paths_rx = pin!(project_paths_rx); + while let Some(project_paths) = project_paths_rx.next().await { + let buffers = this.update(cx, |this, cx| { + project_paths + .into_iter() + .map(|project_path| this.open_buffer(project_path, cx)) + .collect::>() + })?; + for buffer_task in buffers { + if let Some(buffer) = buffer_task.await.log_err() + && tx.send(buffer).await.is_err() + { + return anyhow::Ok(()); + } + } + } + anyhow::Ok(()) + }) + .detach(); + rx + } + fn on_buffer_event( &mut self, buffer: Entity, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fd4bcd583908f0428586b988d82808598a501b3..d167434c52abc161f81d92e2a51c992d52fb9872 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,6 @@ pub mod lsp_command; pub mod lsp_store; mod manifest_tree; pub mod prettier_store; -mod project_search; pub mod project_settings; pub mod search; mod task_inventory; @@ -41,7 +40,6 @@ use crate::{ agent_server_store::AllAgentServersSettings, git_store::GitStore, lsp_store::{SymbolLocation, log_store::LogKind}, - project_search::SearchResultsHandle, }; pub use agent_server_store::{AgentServerStore, AgentServersUpdated}; pub use git_store::{ @@ -49,7 +47,6 @@ pub use git_store::{ git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal}, }; pub use manifest_tree::ManifestTree; -pub use project_search::Search; use anyhow::{Context as _, Result, anyhow}; use buffer_store::{BufferStore, BufferStoreEvent}; @@ -113,7 +110,7 @@ use snippet_provider::SnippetProvider; use std::{ borrow::Cow, collections::BTreeMap, - ops::{Not as _, Range}, + ops::Range, path::{Path, PathBuf}, pin::pin, str, @@ -127,7 +124,7 @@ use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope}; use toolchain_store::EmptyToolchainStore; use util::{ ResultExt as _, maybe, - paths::{PathStyle, SanitizedPath, is_absolute}, + paths::{PathStyle, SanitizedPath, compare_paths, is_absolute}, rel_path::RelPath, }; use worktree::{CreatedEntry, Snapshot, Traversal}; @@ -154,6 +151,8 @@ pub use lsp_store::{ }; pub use toolchain_store::{ToolchainStore, Toolchains}; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; +const MAX_SEARCH_RESULT_FILES: usize = 5_000; +const MAX_SEARCH_RESULT_RANGES: usize = 10_000; pub trait ProjectItem: 'static { fn try_open( @@ -3938,44 +3937,179 @@ impl Project { }) } - fn search_impl(&mut self, query: SearchQuery, cx: &mut Context) -> SearchResultsHandle { - let client: Option<(AnyProtoClient, _)> = if let Some(ssh_client) = &self.remote_client { - Some((ssh_client.read(cx).proto_client(), 0)) - } else if let Some(remote_id) = self.remote_id() { - self.is_local() - .not() - .then(|| (self.collab_client.clone().into(), remote_id)) + pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { + let (result_tx, result_rx) = smol::channel::unbounded(); + + let matching_buffers_rx = if query.is_opened_only() { + self.sort_search_candidates(&query, cx) } else { - None + self.find_search_candidate_buffers(&query, MAX_SEARCH_RESULT_FILES + 1, cx) }; - let searcher = if query.is_opened_only() { - project_search::Search::open_buffers_only( - self.buffer_store.clone(), - self.worktree_store.clone(), - project_search::Search::MAX_SEARCH_RESULT_FILES + 1, - ) - } else { - match client { - Some((client, remote_id)) => project_search::Search::remote( - self.buffer_store.clone(), - self.worktree_store.clone(), - project_search::Search::MAX_SEARCH_RESULT_FILES + 1, - (client, remote_id, self.remotely_created_models.clone()), - ), - None => project_search::Search::local( - self.fs.clone(), - self.buffer_store.clone(), - self.worktree_store.clone(), - project_search::Search::MAX_SEARCH_RESULT_FILES + 1, - cx, - ), + + cx.spawn(async move |_, cx| { + let mut range_count = 0; + let mut buffer_count = 0; + let mut limit_reached = false; + let query = Arc::new(query); + let chunks = matching_buffers_rx.ready_chunks(64); + + // Now that we know what paths match the query, we will load at most + // 64 buffers at a time to avoid overwhelming the main thread. For each + // opened buffer, we will spawn a background task that retrieves all the + // ranges in the buffer matched by the query. + let mut chunks = pin!(chunks); + 'outer: while let Some(matching_buffer_chunk) = chunks.next().await { + let mut chunk_results = Vec::with_capacity(matching_buffer_chunk.len()); + for buffer in matching_buffer_chunk { + let query = query.clone(); + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; + chunk_results.push(cx.background_spawn(async move { + let ranges = query + .search(&snapshot, None) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect::>(); + anyhow::Ok((buffer, ranges)) + })); + } + + let chunk_results = futures::future::join_all(chunk_results).await; + for result in chunk_results { + if let Some((buffer, ranges)) = result.log_err() { + range_count += ranges.len(); + buffer_count += 1; + result_tx + .send(SearchResult::Buffer { buffer, ranges }) + .await?; + if buffer_count > MAX_SEARCH_RESULT_FILES + || range_count > MAX_SEARCH_RESULT_RANGES + { + limit_reached = true; + break 'outer; + } + } + } } - }; - searcher.into_handle(query, cx) + + if limit_reached { + result_tx.send(SearchResult::LimitReached).await?; + } + + anyhow::Ok(()) + }) + .detach(); + + result_rx } - pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { - self.search_impl(query, cx).results(cx) + fn find_search_candidate_buffers( + &mut self, + query: &SearchQuery, + limit: usize, + cx: &mut Context, + ) -> Receiver> { + if self.is_local() { + let fs = self.fs.clone(); + self.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.find_search_candidates(query, limit, fs, cx) + }) + } else { + self.find_search_candidates_remote(query, limit, cx) + } + } + + fn sort_search_candidates( + &mut self, + search_query: &SearchQuery, + cx: &mut Context, + ) -> Receiver> { + let worktree_store = self.worktree_store.read(cx); + let mut buffers = search_query + .buffers() + .into_iter() + .flatten() + .filter(|buffer| { + let b = buffer.read(cx); + if let Some(file) = b.file() { + if !search_query.match_path(file.path().as_std_path()) { + return false; + } + if let Some(entry) = b + .entry_id(cx) + .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) + && entry.is_ignored + && !search_query.include_ignored() + { + return false; + } + } + true + }) + .collect::>(); + let (tx, rx) = smol::channel::unbounded(); + buffers.sort_by(|a, b| match (a.read(cx).file(), b.read(cx).file()) { + (None, None) => a.read(cx).remote_id().cmp(&b.read(cx).remote_id()), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (Some(a), Some(b)) => compare_paths( + (a.path().as_std_path(), true), + (b.path().as_std_path(), true), + ), + }); + for buffer in buffers { + tx.send_blocking(buffer.clone()).unwrap() + } + + rx + } + + fn find_search_candidates_remote( + &mut self, + query: &SearchQuery, + limit: usize, + cx: &mut Context, + ) -> Receiver> { + let (tx, rx) = smol::channel::unbounded(); + + let (client, remote_id): (AnyProtoClient, _) = if let Some(ssh_client) = &self.remote_client + { + (ssh_client.read(cx).proto_client(), 0) + } else if let Some(remote_id) = self.remote_id() { + (self.collab_client.clone().into(), remote_id) + } else { + return rx; + }; + + let request = client.request(proto::FindSearchCandidates { + project_id: remote_id, + query: Some(query.to_proto()), + limit: limit as _, + }); + let guard = self.retain_remotely_created_models(cx); + + cx.spawn(async move |project, cx| { + let response = request.await?; + for buffer_id in response.buffer_ids { + let buffer_id = BufferId::new(buffer_id)?; + let buffer = project + .update(cx, |project, cx| { + project.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.wait_for_remote_buffer(buffer_id, cx) + }) + })? + .await?; + let _ = tx.send(buffer).await; + } + + drop(guard); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + rx } pub fn request_lsp( @@ -4700,31 +4834,18 @@ impl Project { fn retain_remotely_created_models( &mut self, cx: &mut Context, - ) -> RemotelyCreatedModelGuard { - Self::retain_remotely_created_models_impl( - &self.remotely_created_models, - &self.buffer_store, - &self.worktree_store, - cx, - ) - } - - fn retain_remotely_created_models_impl( - models: &Arc>, - buffer_store: &Entity, - worktree_store: &Entity, - cx: &mut App, ) -> RemotelyCreatedModelGuard { { - let mut remotely_create_models = models.lock(); + let mut remotely_create_models = self.remotely_created_models.lock(); if remotely_create_models.retain_count == 0 { - remotely_create_models.buffers = buffer_store.read(cx).buffers().collect(); - remotely_create_models.worktrees = worktree_store.read(cx).worktrees().collect(); + remotely_create_models.buffers = self.buffer_store.read(cx).buffers().collect(); + remotely_create_models.worktrees = + self.worktree_store.read(cx).worktrees().collect(); } remotely_create_models.retain_count += 1; } RemotelyCreatedModelGuard { - remote_models: Arc::downgrade(&models), + remote_models: Arc::downgrade(&self.remotely_created_models), } } @@ -4794,7 +4915,7 @@ impl Project { let query = SearchQuery::from_proto(message.query.context("missing query field")?, path_style)?; let results = this.update(&mut cx, |this, cx| { - this.search_impl(query, cx).matching_buffers(cx) + this.find_search_candidate_buffers(&query, message.limit as _, cx) })?; let mut response = proto::FindSearchCandidatesResponse { diff --git a/crates/project/src/project_search.rs b/crates/project/src/project_search.rs deleted file mode 100644 index 25fe578bd7dc2302645dcfb4fd557de1f2b22081..0000000000000000000000000000000000000000 --- a/crates/project/src/project_search.rs +++ /dev/null @@ -1,754 +0,0 @@ -use std::{ - io::{BufRead, BufReader}, - path::Path, - pin::pin, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, -}; - -use anyhow::Context; -use collections::HashSet; -use fs::Fs; -use futures::{SinkExt, StreamExt, select_biased, stream::FuturesOrdered}; -use gpui::{App, AppContext, AsyncApp, Entity, Task}; -use language::{Buffer, BufferSnapshot}; -use parking_lot::Mutex; -use postage::oneshot; -use rpc::{AnyProtoClient, proto}; -use smol::{ - channel::{Receiver, Sender, bounded, unbounded}, - future::FutureExt, -}; - -use text::BufferId; -use util::{ResultExt, maybe, paths::compare_rel_paths}; -use worktree::{Entry, ProjectEntryId, Snapshot, Worktree}; - -use crate::{ - Project, ProjectItem, ProjectPath, RemotelyCreatedModels, - buffer_store::BufferStore, - search::{SearchQuery, SearchResult}, - worktree_store::WorktreeStore, -}; - -pub struct Search { - buffer_store: Entity, - worktree_store: Entity, - limit: usize, - kind: SearchKind, -} - -/// Represents search setup, before it is actually kicked off with Search::into_results -enum SearchKind { - /// Search for candidates by inspecting file contents on file system, avoiding loading the buffer unless we know that a given file contains a match. - Local { - fs: Arc, - worktrees: Vec>, - }, - /// Query remote host for candidates. As of writing, the host runs a local search in "buffers with matches only" mode. - Remote { - client: AnyProtoClient, - remote_id: u64, - models: Arc>, - }, - /// Run search against a known set of candidates. Even when working with a remote host, this won't round-trip to host. - OpenBuffersOnly, -} - -/// Represents results of project search and allows one to either obtain match positions OR -/// just the handles to buffers that may match the search. Grabbing the handles is cheaper than obtaining full match positions, because in that case we'll look for -/// at most one match in each file. -#[must_use] -pub struct SearchResultsHandle { - results: Receiver, - matching_buffers: Receiver>, - trigger_search: Box Task<()> + Send + Sync>, -} - -impl SearchResultsHandle { - pub fn results(self, cx: &mut App) -> Receiver { - (self.trigger_search)(cx).detach(); - self.results - } - pub fn matching_buffers(self, cx: &mut App) -> Receiver> { - (self.trigger_search)(cx).detach(); - self.matching_buffers - } -} - -#[derive(Clone)] -enum FindSearchCandidates { - Local { - fs: Arc, - /// Start off with all paths in project and filter them based on: - /// - Include filters - /// - Exclude filters - /// - Only open buffers - /// - Scan ignored files - /// Put another way: filter out files that can't match (without looking at file contents) - input_paths_rx: Receiver, - /// After that, if the buffer is not yet loaded, we'll figure out if it contains at least one match - /// based on disk contents of a buffer. This step is not performed for buffers we already have in memory. - confirm_contents_will_match_tx: Sender, - confirm_contents_will_match_rx: Receiver, - /// Of those that contain at least one match (or are already in memory), look for rest of matches (and figure out their ranges). - /// But wait - first, we need to go back to the main thread to open a buffer (& create an entity for it). - get_buffer_for_full_scan_tx: Sender, - }, - Remote, - OpenBuffersOnly, -} - -impl Search { - pub fn local( - fs: Arc, - buffer_store: Entity, - worktree_store: Entity, - limit: usize, - cx: &mut App, - ) -> Self { - let worktrees = worktree_store.read(cx).visible_worktrees(cx).collect(); - Self { - kind: SearchKind::Local { fs, worktrees }, - buffer_store, - worktree_store, - limit, - } - } - - pub(crate) fn remote( - buffer_store: Entity, - worktree_store: Entity, - limit: usize, - client_state: (AnyProtoClient, u64, Arc>), - ) -> Self { - Self { - kind: SearchKind::Remote { - client: client_state.0, - remote_id: client_state.1, - models: client_state.2, - }, - buffer_store, - worktree_store, - limit, - } - } - pub(crate) fn open_buffers_only( - buffer_store: Entity, - worktree_store: Entity, - limit: usize, - ) -> Self { - Self { - kind: SearchKind::OpenBuffersOnly, - buffer_store, - worktree_store, - limit, - } - } - - pub(crate) const MAX_SEARCH_RESULT_FILES: usize = 5_000; - pub(crate) const MAX_SEARCH_RESULT_RANGES: usize = 10_000; - /// Prepares a project search run. The resulting [`SearchResultsHandle`] has to be used to specify whether you're interested in matching buffers - /// or full search results. - pub fn into_handle(mut self, query: SearchQuery, cx: &mut App) -> SearchResultsHandle { - let mut open_buffers = HashSet::default(); - let mut unnamed_buffers = Vec::new(); - const MAX_CONCURRENT_BUFFER_OPENS: usize = 64; - let buffers = self.buffer_store.read(cx); - for handle in buffers.buffers() { - let buffer = handle.read(cx); - if !buffers.is_searchable(&buffer.remote_id()) { - continue; - } else if let Some(entry_id) = buffer.entry_id(cx) { - open_buffers.insert(entry_id); - } else { - self.limit -= self.limit.saturating_sub(1); - unnamed_buffers.push(handle) - }; - } - let executor = cx.background_executor().clone(); - let (tx, rx) = unbounded(); - let (grab_buffer_snapshot_tx, grab_buffer_snapshot_rx) = unbounded(); - let matching_buffers = grab_buffer_snapshot_rx.clone(); - let trigger_search = Box::new(move |cx: &mut App| { - cx.spawn(async move |cx| { - for buffer in unnamed_buffers { - _ = grab_buffer_snapshot_tx.send(buffer).await; - } - - let (find_all_matches_tx, find_all_matches_rx) = - bounded(MAX_CONCURRENT_BUFFER_OPENS); - - let (candidate_searcher, tasks) = match self.kind { - SearchKind::OpenBuffersOnly => { - let Ok(open_buffers) = cx.update(|cx| self.all_loaded_buffers(&query, cx)) - else { - return; - }; - let fill_requests = cx - .background_spawn(async move { - for buffer in open_buffers { - if let Err(_) = grab_buffer_snapshot_tx.send(buffer).await { - return; - } - } - }) - .boxed_local(); - (FindSearchCandidates::OpenBuffersOnly, vec![fill_requests]) - } - SearchKind::Local { - fs, - ref mut worktrees, - } => { - let (get_buffer_for_full_scan_tx, get_buffer_for_full_scan_rx) = - unbounded(); - let (confirm_contents_will_match_tx, confirm_contents_will_match_rx) = - bounded(64); - let (sorted_search_results_tx, sorted_search_results_rx) = unbounded(); - - let (input_paths_tx, input_paths_rx) = unbounded(); - - let tasks = vec![ - cx.spawn(Self::provide_search_paths( - std::mem::take(worktrees), - query.include_ignored(), - input_paths_tx, - sorted_search_results_tx, - )) - .boxed_local(), - Self::open_buffers( - &self.buffer_store, - get_buffer_for_full_scan_rx, - grab_buffer_snapshot_tx, - cx.clone(), - ) - .boxed_local(), - cx.background_spawn(Self::maintain_sorted_search_results( - sorted_search_results_rx, - get_buffer_for_full_scan_tx.clone(), - self.limit, - )) - .boxed_local(), - ]; - ( - FindSearchCandidates::Local { - fs, - get_buffer_for_full_scan_tx, - confirm_contents_will_match_tx, - confirm_contents_will_match_rx, - input_paths_rx, - }, - tasks, - ) - } - SearchKind::Remote { - client, - remote_id, - models, - } => { - let request = client.request(proto::FindSearchCandidates { - project_id: remote_id, - query: Some(query.to_proto()), - limit: self.limit as _, - }); - let Ok(guard) = cx.update(|cx| { - Project::retain_remotely_created_models_impl( - &models, - &self.buffer_store, - &self.worktree_store, - cx, - ) - }) else { - return; - }; - let buffer_store = self.buffer_store.downgrade(); - let issue_remote_buffers_request = cx - .spawn(async move |cx| { - let _ = maybe!(async move { - let response = request.await?; - - for buffer_id in response.buffer_ids { - let buffer_id = BufferId::new(buffer_id)?; - let buffer = buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let _ = grab_buffer_snapshot_tx.send(buffer).await; - } - - drop(guard); - anyhow::Ok(()) - }) - .await - .log_err(); - }) - .boxed_local(); - ( - FindSearchCandidates::Remote, - vec![issue_remote_buffers_request], - ) - } - }; - - let matches_count = AtomicUsize::new(0); - let matched_buffer_count = AtomicUsize::new(0); - - let worker_pool = executor.scoped(|scope| { - let num_cpus = executor.num_cpus(); - - assert!(num_cpus > 0); - for _ in 0..executor.num_cpus() - 1 { - let worker = Worker { - query: &query, - open_buffers: &open_buffers, - matched_buffer_count: &matched_buffer_count, - matches_count: &matches_count, - candidates: candidate_searcher.clone(), - find_all_matches_rx: find_all_matches_rx.clone(), - publish_matches: tx.clone(), - }; - scope.spawn(worker.run()); - } - drop(tx); - drop(find_all_matches_rx); - drop(candidate_searcher); - }); - - let buffer_snapshots = Self::grab_buffer_snapshots( - grab_buffer_snapshot_rx, - find_all_matches_tx, - cx.clone(), - ); - futures::future::join_all( - [worker_pool.boxed_local(), buffer_snapshots.boxed_local()] - .into_iter() - .chain(tasks), - ) - .await; - }) - }); - - SearchResultsHandle { - results: rx, - matching_buffers, - trigger_search, - } - } - - fn provide_search_paths( - worktrees: Vec>, - include_ignored: bool, - tx: Sender, - results: Sender>, - ) -> impl AsyncFnOnce(&mut AsyncApp) { - async move |cx| { - _ = maybe!(async move { - for worktree in worktrees { - let (mut snapshot, worktree_settings) = worktree - .read_with(cx, |this, _| { - Some((this.snapshot(), this.as_local()?.settings())) - })? - .context("The worktree is not local")?; - if include_ignored { - // Pre-fetch all of the ignored directories as they're going to be searched. - let mut entries_to_refresh = vec![]; - for entry in snapshot.entries(include_ignored, 0) { - if entry.is_ignored && entry.kind.is_unloaded() { - if !worktree_settings.is_path_excluded(&entry.path) { - entries_to_refresh.push(entry.path.clone()); - } - } - } - let barrier = worktree.update(cx, |this, _| { - let local = this.as_local_mut()?; - let barrier = entries_to_refresh - .into_iter() - .map(|path| local.add_path_prefix_to_scan(path).into_future()) - .collect::>(); - Some(barrier) - })?; - if let Some(barriers) = barrier { - futures::future::join_all(barriers).await; - } - snapshot = worktree.read_with(cx, |this, _| this.snapshot())?; - } - cx.background_executor() - .scoped(|scope| { - scope.spawn(async { - for entry in snapshot.files(include_ignored, 0) { - let (should_scan_tx, should_scan_rx) = oneshot::channel(); - let Ok(_) = tx - .send(InputPath { - entry: entry.clone(), - snapshot: snapshot.clone(), - should_scan_tx, - }) - .await - else { - return; - }; - if results.send(should_scan_rx).await.is_err() { - return; - }; - } - }) - }) - .await; - } - anyhow::Ok(()) - }) - .await; - } - } - - async fn maintain_sorted_search_results( - rx: Receiver>, - paths_for_full_scan: Sender, - limit: usize, - ) { - let mut rx = pin!(rx); - let mut matched = 0; - while let Some(mut next_path_result) = rx.next().await { - let Some(successful_path) = next_path_result.next().await else { - // This math did not produce a match, hence skip it. - continue; - }; - if paths_for_full_scan.send(successful_path).await.is_err() { - return; - }; - matched += 1; - if matched >= limit { - break; - } - } - } - - /// Background workers cannot open buffers by themselves, hence main thread will do it on their behalf. - async fn open_buffers( - buffer_store: &Entity, - rx: Receiver, - find_all_matches_tx: Sender>, - mut cx: AsyncApp, - ) { - let mut rx = pin!(rx.ready_chunks(64)); - _ = maybe!(async move { - while let Some(requested_paths) = rx.next().await { - let mut buffers = buffer_store.update(&mut cx, |this, cx| { - requested_paths - .into_iter() - .map(|path| this.open_buffer(path, cx)) - .collect::>() - })?; - - while let Some(buffer) = buffers.next().await { - if let Some(buffer) = buffer.log_err() { - find_all_matches_tx.send(buffer).await?; - } - } - } - Result::<_, anyhow::Error>::Ok(()) - }) - .await; - } - - async fn grab_buffer_snapshots( - rx: Receiver>, - find_all_matches_tx: Sender<(Entity, BufferSnapshot)>, - mut cx: AsyncApp, - ) { - _ = maybe!(async move { - while let Ok(buffer) = rx.recv().await { - let snapshot = buffer.read_with(&mut cx, |this, _| this.snapshot())?; - find_all_matches_tx.send((buffer, snapshot)).await?; - } - Result::<_, anyhow::Error>::Ok(()) - }) - .await; - } - - fn all_loaded_buffers(&self, search_query: &SearchQuery, cx: &App) -> Vec> { - let worktree_store = self.worktree_store.read(cx); - let mut buffers = search_query - .buffers() - .into_iter() - .flatten() - .filter(|buffer| { - let b = buffer.read(cx); - if let Some(file) = b.file() { - if !search_query.match_path(file.path().as_std_path()) { - return false; - } - if !search_query.include_ignored() - && let Some(entry) = b - .entry_id(cx) - .and_then(|entry_id| worktree_store.entry_for_id(entry_id, cx)) - && entry.is_ignored - { - return false; - } - } - true - }) - .cloned() - .collect::>(); - buffers.sort_by(|a, b| { - let a = a.read(cx); - let b = b.read(cx); - match (a.file(), b.file()) { - (None, None) => a.remote_id().cmp(&b.remote_id()), - (None, Some(_)) => std::cmp::Ordering::Less, - (Some(_), None) => std::cmp::Ordering::Greater, - (Some(a), Some(b)) => compare_rel_paths((a.path(), true), (b.path(), true)), - } - }); - - buffers - } -} - -struct Worker<'search> { - query: &'search SearchQuery, - matched_buffer_count: &'search AtomicUsize, - matches_count: &'search AtomicUsize, - open_buffers: &'search HashSet, - candidates: FindSearchCandidates, - /// Ok, we're back in background: run full scan & find all matches in a given buffer snapshot. - find_all_matches_rx: Receiver<(Entity, BufferSnapshot)>, - /// Cool, we have results; let's share them with the world. - publish_matches: Sender, -} - -impl Worker<'_> { - async fn run(mut self) { - let ( - input_paths_rx, - confirm_contents_will_match_rx, - mut confirm_contents_will_match_tx, - mut get_buffer_for_full_scan_tx, - fs, - ) = match self.candidates { - FindSearchCandidates::Local { - fs, - input_paths_rx, - confirm_contents_will_match_rx, - confirm_contents_will_match_tx, - get_buffer_for_full_scan_tx, - } => ( - input_paths_rx, - confirm_contents_will_match_rx, - confirm_contents_will_match_tx, - get_buffer_for_full_scan_tx, - Some(fs), - ), - FindSearchCandidates::Remote | FindSearchCandidates::OpenBuffersOnly => ( - unbounded().1, - unbounded().1, - unbounded().0, - unbounded().0, - None, - ), - }; - let mut find_all_matches = pin!(self.find_all_matches_rx.fuse()); - let mut find_first_match = pin!(confirm_contents_will_match_rx.fuse()); - let mut scan_path = pin!(input_paths_rx.fuse()); - - loop { - let handler = RequestHandler { - query: self.query, - open_entries: &self.open_buffers, - fs: fs.as_deref(), - matched_buffer_count: self.matched_buffer_count, - matches_count: self.matches_count, - confirm_contents_will_match_tx: &confirm_contents_will_match_tx, - get_buffer_for_full_scan_tx: &get_buffer_for_full_scan_tx, - publish_matches: &self.publish_matches, - }; - // Whenever we notice that some step of a pipeline is closed, we don't want to close subsequent - // steps straight away. Another worker might be about to produce a value that will - // be pushed there, thus we'll replace current worker's pipe with a dummy one. - // That way, we'll only ever close a next-stage channel when ALL workers do so. - select_biased! { - find_all_matches = find_all_matches.next() => { - - if self.publish_matches.is_closed() { - break; - } - let Some(matches) = find_all_matches else { - self.publish_matches = bounded(1).0; - continue; - }; - let result = handler.handle_find_all_matches(matches).await; - if let Some(_should_bail) = result { - - self.publish_matches = bounded(1).0; - continue; - } - }, - find_first_match = find_first_match.next() => { - if let Some(buffer_with_at_least_one_match) = find_first_match { - handler.handle_find_first_match(buffer_with_at_least_one_match).await; - } else { - get_buffer_for_full_scan_tx = bounded(1).0; - } - - }, - scan_path = scan_path.next() => { - if let Some(path_to_scan) = scan_path { - handler.handle_scan_path(path_to_scan).await; - } else { - // If we're the last worker to notice that this is not producing values, close the upstream. - confirm_contents_will_match_tx = bounded(1).0; - } - - } - complete => { - break - }, - - } - } - } -} - -struct RequestHandler<'worker> { - query: &'worker SearchQuery, - fs: Option<&'worker dyn Fs>, - open_entries: &'worker HashSet, - matched_buffer_count: &'worker AtomicUsize, - matches_count: &'worker AtomicUsize, - - confirm_contents_will_match_tx: &'worker Sender, - get_buffer_for_full_scan_tx: &'worker Sender, - publish_matches: &'worker Sender, -} - -struct LimitReached; - -impl RequestHandler<'_> { - async fn handle_find_all_matches( - &self, - (buffer, snapshot): (Entity, BufferSnapshot), - ) -> Option { - let ranges = self - .query - .search(&snapshot, None) - .await - .iter() - .map(|range| snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)) - .collect::>(); - - let matched_ranges = ranges.len(); - if self.matched_buffer_count.fetch_add(1, Ordering::Release) - > Search::MAX_SEARCH_RESULT_FILES - || self - .matches_count - .fetch_add(matched_ranges, Ordering::Release) - > Search::MAX_SEARCH_RESULT_RANGES - { - _ = self.publish_matches.send(SearchResult::LimitReached).await; - Some(LimitReached) - } else { - _ = self - .publish_matches - .send(SearchResult::Buffer { buffer, ranges }) - .await; - None - } - } - async fn handle_find_first_match(&self, mut entry: MatchingEntry) { - _=maybe!(async move { - let abs_path = entry.worktree_root.join(entry.path.path.as_std_path()); - let Some(file) = self.fs.context("Trying to query filesystem in remote project search")?.open_sync(&abs_path).await.log_err() else { - return anyhow::Ok(()); - }; - - let mut file = BufReader::new(file); - let file_start = file.fill_buf()?; - - if let Err(Some(starting_position)) = - std::str::from_utf8(file_start).map_err(|e| e.error_len()) - { - // Before attempting to match the file content, throw away files that have invalid UTF-8 sequences early on; - // That way we can still match files in a streaming fashion without having look at "obviously binary" files. - log::debug!( - "Invalid UTF-8 sequence in file {abs_path:?} at byte position {starting_position}" - ); - return Ok(()); - } - - if self.query.detect(file).unwrap_or(false) { - // Yes, we should scan the whole file. - entry.should_scan_tx.send(entry.path).await?; - } - Ok(()) - }).await; - } - - async fn handle_scan_path(&self, req: InputPath) { - _ = maybe!(async move { - let InputPath { - entry, - - snapshot, - should_scan_tx, - } = req; - - if entry.is_fifo || !entry.is_file() { - return Ok(()); - } - - if self.query.filters_path() { - let matched_path = if self.query.match_full_paths() { - let mut full_path = snapshot.root_name().as_std_path().to_owned(); - full_path.push(entry.path.as_std_path()); - self.query.match_path(&full_path) - } else { - self.query.match_path(entry.path.as_std_path()) - }; - if !matched_path { - return Ok(()); - } - } - - if self.open_entries.contains(&entry.id) { - // The buffer is already in memory and that's the version we want to scan; - // hence skip the dilly-dally and look for all matches straight away. - self.get_buffer_for_full_scan_tx - .send(ProjectPath { - worktree_id: snapshot.id(), - path: entry.path.clone(), - }) - .await?; - } else { - self.confirm_contents_will_match_tx - .send(MatchingEntry { - should_scan_tx: should_scan_tx, - worktree_root: snapshot.abs_path().clone(), - path: ProjectPath { - worktree_id: snapshot.id(), - path: entry.path.clone(), - }, - }) - .await?; - } - - anyhow::Ok(()) - }) - .await; - } -} - -struct InputPath { - entry: Entry, - snapshot: Snapshot, - should_scan_tx: oneshot::Sender, -} - -struct MatchingEntry { - worktree_root: Arc, - path: ProjectPath, - should_scan_tx: oneshot::Sender, -} diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 676c96f4331d73b87d4bc16766a5f6c4d6194864..e6da207dadbde3ebc725fbb84ed19b3b35414f87 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -8,7 +8,10 @@ use std::{ use anyhow::{Context as _, Result, anyhow, bail}; use collections::{HashMap, HashSet}; use fs::{Fs, copy_recursive}; -use futures::{FutureExt, SinkExt, future::Shared}; +use futures::{ + FutureExt, SinkExt, + future::{BoxFuture, Shared}, +}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity, }; @@ -996,14 +999,148 @@ impl WorktreeStore { matching_paths_rx } + fn scan_ignored_dir<'a>( + fs: &'a Arc, + snapshot: &'a worktree::Snapshot, + path: &'a RelPath, + query: &'a SearchQuery, + filter_tx: &'a Sender, + output_tx: &'a Sender>, + ) -> BoxFuture<'a, Result<()>> { + async move { + let abs_path = snapshot.absolutize(path); + let Some(mut files) = fs + .read_dir(&abs_path) + .await + .with_context(|| format!("listing ignored path {abs_path:?}")) + .log_err() + else { + return Ok(()); + }; + + let mut results = Vec::new(); + + while let Some(Ok(file)) = files.next().await { + let Some(metadata) = fs + .metadata(&file) + .await + .with_context(|| format!("fetching fs metadata for {abs_path:?}")) + .log_err() + .flatten() + else { + continue; + }; + if metadata.is_symlink || metadata.is_fifo { + continue; + } + let relative_path = file.strip_prefix(snapshot.abs_path())?; + let relative_path = RelPath::new(&relative_path, snapshot.path_style()) + .context("getting relative path")?; + results.push((relative_path.into_arc(), !metadata.is_dir)) + } + results.sort_by(|(a_path, _), (b_path, _)| a_path.cmp(b_path)); + for (path, is_file) in results { + if is_file { + if query.filters_path() { + let matched_path = if query.match_full_paths() { + let mut full_path = snapshot.root_name().as_std_path().to_owned(); + full_path.push(path.as_std_path()); + query.match_path(&full_path) + } else { + query.match_path(&path.as_std_path()) + }; + if !matched_path { + continue; + } + } + let (tx, rx) = oneshot::channel(); + output_tx.send(rx).await?; + filter_tx + .send(MatchingEntry { + respond: tx, + worktree_root: snapshot.abs_path().clone(), + path: ProjectPath { + worktree_id: snapshot.id(), + path: path.into_arc(), + }, + }) + .await?; + } else { + Self::scan_ignored_dir(fs, snapshot, &path, query, filter_tx, output_tx) + .await?; + } + } + Ok(()) + } + .boxed() + } + async fn find_candidate_paths( - _: Arc, - _: Vec<(worktree::Snapshot, WorktreeSettings)>, - _: HashSet, - _: SearchQuery, - _: Sender, - _: Sender>, + fs: Arc, + snapshots: Vec<(worktree::Snapshot, WorktreeSettings)>, + open_entries: HashSet, + query: SearchQuery, + filter_tx: Sender, + output_tx: Sender>, ) -> Result<()> { + for (snapshot, settings) in snapshots { + for entry in snapshot.entries(query.include_ignored(), 0) { + if entry.is_dir() && entry.is_ignored { + if !settings.is_path_excluded(&entry.path) { + Self::scan_ignored_dir( + &fs, + &snapshot, + &entry.path, + &query, + &filter_tx, + &output_tx, + ) + .await?; + } + continue; + } + + if entry.is_fifo || !entry.is_file() { + continue; + } + + if query.filters_path() { + let matched_path = if query.match_full_paths() { + let mut full_path = snapshot.root_name().as_std_path().to_owned(); + full_path.push(entry.path.as_std_path()); + query.match_path(&full_path) + } else { + query.match_path(entry.path.as_std_path()) + }; + if !matched_path { + continue; + } + } + + let (mut tx, rx) = oneshot::channel(); + + if open_entries.contains(&entry.id) { + tx.send(ProjectPath { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }) + .await?; + } else { + filter_tx + .send(MatchingEntry { + respond: tx, + worktree_root: snapshot.abs_path().clone(), + path: ProjectPath { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }, + }) + .await?; + } + + output_tx.send(rx).await?; + } + } Ok(()) } diff --git a/crates/project_benchmarks/Cargo.toml b/crates/project_benchmarks/Cargo.toml deleted file mode 100644 index 1171d468c649bdd9f76a44b3ef0155dc652c6034..0000000000000000000000000000000000000000 --- a/crates/project_benchmarks/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "project_benchmarks" -version = "0.1.0" -publish.workspace = true -edition.workspace = true - -[dependencies] -anyhow.workspace = true -clap.workspace = true -client.workspace = true -futures.workspace = true -gpui = { workspace = true, features = ["windows-manifest"] } -http_client = { workspace = true, features = ["test-support"]} -language.workspace = true -node_runtime.workspace = true -project.workspace = true -settings.workspace = true -watch.workspace = true - -[lints] -workspace = true diff --git a/crates/project_benchmarks/LICENSE-GPL b/crates/project_benchmarks/LICENSE-GPL deleted file mode 120000 index 89e542f750cd3860a0598eff0dc34b56d7336dc4..0000000000000000000000000000000000000000 --- a/crates/project_benchmarks/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/project_benchmarks/src/main.rs b/crates/project_benchmarks/src/main.rs deleted file mode 100644 index 5075016665a072f172da461cffdf6c5dbcabb4ac..0000000000000000000000000000000000000000 --- a/crates/project_benchmarks/src/main.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::sync::Arc; - -use clap::Parser; -use client::{Client, UserStore}; -use gpui::{AppContext as _, Application}; -use http_client::FakeHttpClient; -use language::LanguageRegistry; -use node_runtime::NodeRuntime; -use project::{ - Project, RealFs, - search::{SearchQuery, SearchResult}, -}; - -#[derive(Parser)] -struct Args { - /// List of worktrees to run the search against. - worktrees: Vec, - #[clap(short)] - query: String, - /// Treat query as a regex. - #[clap(short, long)] - regex: bool, - /// Matches have to be standalone words. - #[clap(long)] - whole_word: bool, - /// Make matching case-sensitive. - #[clap(long, default_value_t = true)] - case_sensitive: bool, - /// Include gitignored files in the search. - #[clap(long)] - include_ignored: bool, -} - -fn main() -> Result<(), anyhow::Error> { - let args = Args::parse(); - let query = if args.regex { - SearchQuery::regex( - args.query, - args.whole_word, - args.case_sensitive, - args.include_ignored, - false, - Default::default(), - Default::default(), - false, - None, - ) - } else { - SearchQuery::text( - args.query, - args.whole_word, - args.case_sensitive, - args.include_ignored, - Default::default(), - Default::default(), - false, - None, - ) - }?; - Application::headless().run(|cx| { - settings::init(cx); - client::init_settings(cx); - language::init(cx); - Project::init_settings(cx); - let client = Client::production(cx); - let http_client = FakeHttpClient::with_200_response(); - let (_, rx) = watch::channel(None); - let node = NodeRuntime::new(http_client, None, rx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); - let project = Project::local( - client, - node, - user_store, - registry, - fs, - Some(Default::default()), - cx, - ); - - project.clone().update(cx, move |_, cx| { - cx.spawn(async move |_, cx| { - println!("Loading worktrees"); - let worktrees = project.update(cx, |this, cx| { - args.worktrees - .into_iter() - .map(|worktree| this.find_or_create_worktree(worktree, true, cx)) - .collect::>() - })?; - - let worktrees = futures::future::join_all(worktrees) - .await - .into_iter() - .collect::, anyhow::Error>>()?; - - for (worktree, _) in &worktrees { - worktree - .update(cx, |this, _| this.as_local().unwrap().scan_complete())? - .await; - } - println!("Worktrees loaded"); - - println!("Starting a project search"); - let timer = std::time::Instant::now(); - let mut first_match = None; - let matches = project - .update(cx, |this, cx| this.search(query, cx)) - .unwrap(); - let mut matched_files = 0; - let mut matched_chunks = 0; - while let Ok(match_result) = matches.recv().await { - if first_match.is_none() { - let time = timer.elapsed(); - first_match = Some(time); - println!("First match found after {time:?}"); - } - if let SearchResult::Buffer { ranges, .. } = match_result { - matched_files += 1; - matched_chunks += ranges.len(); - } - } - let elapsed = timer.elapsed(); - println!( - "Finished project search after {elapsed:?}. Matched {matched_files} files and {matched_chunks} excerpts" - ); - drop(project); - cx.update(|cx| cx.quit())?; - - anyhow::Ok(()) - }) - .detach(); - }); - }); - Ok(()) -} diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index b702c75119af9f49707c079a3a799ae8c59209b1..3d28f6ba565330a5fc3c0ea0249aaf760c880439 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -75,7 +75,7 @@ minidumper.workspace = true [dev-dependencies] action_log.workspace = true -agent = { workspace = true, features = ["test-support"] } +agent.workspace = true client = { workspace = true, features = ["test-support"] } clock = { workspace = true, features = ["test-support"] } collections.workspace = true diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index caec0f5c1a4829ad0117469d705567ccf557ef46..534eae6f44986afa42b6d202e4f34691935b3b33 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -640,15 +640,9 @@ impl HeadlessProject { PathStyle::local(), )?; let results = this.update(&mut cx, |this, cx| { - project::Search::local( - this.fs.clone(), - this.buffer_store.clone(), - this.worktree_store.clone(), - message.limit as _, - cx, - ) - .into_handle(query, cx) - .matching_buffers(cx) + this.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.find_search_candidates(&query, message.limit as _, this.fs.clone(), cx) + }) })?; let mut response = proto::FindSearchCandidatesResponse { From b519f53b3ecab94b0d9db6c87261657fb1c48c38 Mon Sep 17 00:00:00 2001 From: Martin Pool Date: Tue, 21 Oct 2025 18:42:05 -0400 Subject: [PATCH 123/202] Rope benchmarks: Generate random strings measured in bytes, not chars (#39951) Follows on from https://github.com/zed-industries/zed/pull/39949. Again I'm not 100% sure of the intent but I think this is a fix: `generate_random_string(rng, 4096)` would previously give you a string of 4096 *chars* which could be anywhere between 4kB and 16kB in bytes. This seems probably not what was intended, because Ropes generally work in bytes not chars, including for the offsets used to index into them. This seems to possibly cause a _regression_ in benchmark performance, which is surprising because it should generally cause smaller test data. But, possibly it's doing better at exercising different paths? cc @mrnugget Release Notes: - N/A --- crates/rope/benches/rope_benchmark.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/rope/benches/rope_benchmark.rs b/crates/rope/benches/rope_benchmark.rs index 4ae6f1b54f19b756e12cc399181bdf6b5d894ad5..030bec01df4d223cd5288842ba0f9c1386dac31b 100644 --- a/crates/rope/benches/rope_benchmark.rs +++ b/crates/rope/benches/rope_benchmark.rs @@ -9,11 +9,21 @@ use rope::{Point, Rope}; use sum_tree::Bias; use util::RandomCharIter; -/// Generate a random text of the given length using the provided RNG. +/// Returns a biased random string whose UTF-8 length is close to but no more than `len` bytes. /// -/// *Note*: The length is in *characters*, not bytes. -fn generate_random_text(rng: &mut StdRng, text_len: usize) -> String { - RandomCharIter::new(rng).take(text_len).collect() +/// The string is biased towards characters expected to occur in text or likely to exercise edge +/// cases. +fn generate_random_text(rng: &mut StdRng, len: usize) -> String { + let mut str = String::with_capacity(len); + let mut chars = RandomCharIter::new(rng); + loop { + let ch = chars.next().unwrap(); + if str.len() + ch.len_utf8() > len { + break; + } + str.push(ch); + } + str } fn generate_random_rope(rng: &mut StdRng, text_len: usize) -> Rope { From 8f3da5c5cd9cc14694cdb8b1da4fac17b47b64eb Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:58:43 -0300 Subject: [PATCH 124/202] settings_ui: Add pickers for theme and icon themes (#40829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the process of adding pickers for the theme and icon themes fields in the settings UI, I felt like there was an improvement opportunity in regards to where some of these components are stored. The `ui_input` crate originally was meant only for the text field-like component, which couldn't be in the regular `ui` crate due to the dependency with `editor`. Given we had also added the number field there—which is similar in also having the same dependency—it made sense to think of this crate more like a home for form-like components rather than for only one component. However, we were also storing some settings UI-specific stuff in that crate, which didn't feel right. So I ended up creating a new directory within the `settings_ui` for components and moved all the pickers and the custom input field there. I think this makes it for a cleaner structure. Release Notes: - settings_ui: Added the ability to search for theme and icon themes in their respective fields. --- Cargo.lock | 4 +- .../add_llm_provider_modal.rs | 27 +- crates/keymap_editor/src/keymap_editor.rs | 8 +- .../language_models/src/provider/anthropic.rs | 6 +- .../language_models/src/provider/bedrock.rs | 21 +- .../language_models/src/provider/deepseek.rs | 6 +- crates/language_models/src/provider/google.rs | 6 +- .../language_models/src/provider/mistral.rs | 10 +- crates/language_models/src/provider/ollama.rs | 11 +- .../language_models/src/provider/open_ai.rs | 6 +- .../src/provider/open_ai_compatible.rs | 6 +- .../src/provider/open_router.rs | 6 +- crates/language_models/src/provider/vercel.rs | 6 +- crates/language_models/src/provider/x_ai.rs | 6 +- crates/onboarding/Cargo.toml | 1 - crates/onboarding/src/onboarding.rs | 1 - crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/components.rs | 105 +------- .../src/components}/font_picker.rs | 0 .../src/components/icon_theme_picker.rs | 189 ++++++++++++++ .../settings_ui/src/components/input_field.rs | 96 +++++++ .../src/components/theme_picker.rs | 179 +++++++++++++ crates/settings_ui/src/settings_ui.rs | 241 ++++++++---------- crates/ui_input/Cargo.toml | 2 - crates/ui_input/src/input_field.rs | 222 ++++++++++++++++ crates/ui_input/src/ui_input.rs | 230 +---------------- crates/zed/src/zed/component_preview.rs | 7 +- crates/zeta2_tools/src/zeta2_tools.rs | 18 +- 28 files changed, 882 insertions(+), 539 deletions(-) rename crates/{ui_input/src => settings_ui/src/components}/font_picker.rs (100%) create mode 100644 crates/settings_ui/src/components/icon_theme_picker.rs create mode 100644 crates/settings_ui/src/components/input_field.rs create mode 100644 crates/settings_ui/src/components/theme_picker.rs create mode 100644 crates/ui_input/src/input_field.rs diff --git a/Cargo.lock b/Cargo.lock index 08bffae4bc9c901aa3f12d7df76cf02048f7c51d..f7d0fb48c82d8f50a193aafebb1ce1fae60440d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10788,7 +10788,6 @@ dependencies = [ "telemetry", "theme", "ui", - "ui_input", "util", "vim_mode_setting", "workspace", @@ -15271,6 +15270,7 @@ dependencies = [ "menu", "node_runtime", "paths", + "picker", "pretty_assertions", "project", "schemars 1.0.4", @@ -18191,10 +18191,8 @@ version = "0.1.0" dependencies = [ "component", "editor", - "fuzzy", "gpui", "menu", - "picker", "settings", "theme", "ui", 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 5e1712e626da98c60834da28906afa3eb30b92e6..97e2cc3e8bac47df5105e94d52bd5bd21799f830 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 @@ -10,7 +10,7 @@ use settings::{OpenAiCompatibleSettingsContent, update_settings_file}; use ui::{ Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*, }; -use ui_input::SingleLineInput; +use ui_input::InputField; use workspace::{ModalView, Workspace}; #[derive(Clone, Copy)] @@ -33,9 +33,9 @@ impl LlmCompatibleProvider { } struct AddLlmProviderInput { - provider_name: Entity, - api_url: Entity, - api_key: Entity, + provider_name: Entity, + api_url: Entity, + api_key: Entity, models: Vec, } @@ -76,10 +76,10 @@ struct ModelCapabilityToggles { } struct ModelInput { - name: Entity, - max_completion_tokens: Entity, - max_output_tokens: Entity, - max_tokens: Entity, + name: Entity, + max_completion_tokens: Entity, + max_output_tokens: Entity, + max_tokens: Entity, capabilities: ModelCapabilityToggles, } @@ -171,9 +171,9 @@ fn single_line_input( text: Option<&str>, window: &mut Window, cx: &mut App, -) -> Entity { +) -> Entity { cx.new(|cx| { - let input = SingleLineInput::new(window, cx, placeholder).label(label); + let input = InputField::new(window, cx, placeholder).label(label); if let Some(text) = text { input .editor() @@ -757,12 +757,7 @@ mod tests { models: Vec<(&str, &str, &str, &str)>, cx: &mut VisualTestContext, ) -> Option { - fn set_text( - input: &Entity, - text: &str, - window: &mut Window, - cx: &mut App, - ) { + fn set_text(input: &Entity, text: &str, window: &mut Window, cx: &mut App) { input.update(cx, |input, cx| { input.editor().update(cx, |editor, cx| { editor.set_text(text, window, cx); diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index fce98ef596e2fb77cdf8091755de39ea55dce615..2740ca14f68263fb520130e36d981535ca80aa3b 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -32,7 +32,7 @@ use ui::{ SharedString, Styled as _, Table, TableColumnWidths, TableInteractionState, TableResizeBehavior, Tooltip, Window, prelude::*, }; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::ResultExt; use workspace::{ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, @@ -2114,7 +2114,7 @@ struct KeybindingEditorModal { editing_keybind: ProcessedBinding, editing_keybind_idx: usize, keybind_editor: Entity, - context_editor: Entity, + context_editor: Entity, action_arguments_editor: Option>, fs: Arc, error: Option, @@ -2148,8 +2148,8 @@ impl KeybindingEditorModal { let keybind_editor = cx .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx)); - let context_editor: Entity = cx.new(|cx| { - let input = SingleLineInput::new(window, cx, "Keybinding Context") + let context_editor: Entity = cx.new(|cx| { + let input = InputField::new(window, cx, "Keybinding Context") .label("Edit Context") .label_size(LabelSize::Default); diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 2897b836b12d7bcaabfe3841a9f0c77ba6ab497e..9eb96cb79815bdbdc06c58ca4156e68e2962b0a4 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -21,7 +21,7 @@ use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; use ui::{Icon, IconName, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::{EnvVar, env_var}; @@ -823,7 +823,7 @@ fn convert_usage(usage: &Usage) -> language_model::TokenUsage { } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, target_agent: ConfigurationViewTargetAgent, @@ -862,7 +862,7 @@ impl ConfigurationView { })); Self { - api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, Self::PLACEHOLDER_TEXT)), + api_key_editor: cx.new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_TEXT)), state, load_credentials_task, target_agent, diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 47dd565f6af64d5ddb1d19cd6ed95ceeffd57cc9..f3e265e925822b2de7950af9fbef5b121da3ed82 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -42,7 +42,7 @@ use settings::{BedrockAvailableModel as AvailableModel, Settings, SettingsStore} use smol::lock::OnceCell; use strum::{EnumIter, IntoEnumIterator, IntoStaticStr}; use ui::{Icon, IconName, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::ResultExt; use crate::AllLanguageModelSettings; @@ -1006,10 +1006,10 @@ pub fn map_to_language_model_completion_events( } struct ConfigurationView { - access_key_id_editor: Entity, - secret_access_key_editor: Entity, - session_token_editor: Entity, - region_editor: Entity, + access_key_id_editor: Entity, + secret_access_key_editor: Entity, + session_token_editor: Entity, + region_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -1047,20 +1047,19 @@ impl ConfigurationView { Self { access_key_id_editor: cx.new(|cx| { - SingleLineInput::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT) + InputField::new(window, cx, Self::PLACEHOLDER_ACCESS_KEY_ID_TEXT) .label("Access Key ID") }), secret_access_key_editor: cx.new(|cx| { - SingleLineInput::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT) + InputField::new(window, cx, Self::PLACEHOLDER_SECRET_ACCESS_KEY_TEXT) .label("Secret Access Key") }), session_token_editor: cx.new(|cx| { - SingleLineInput::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT) + InputField::new(window, cx, Self::PLACEHOLDER_SESSION_TOKEN_TEXT) .label("Session Token (Optional)") }), - region_editor: cx.new(|cx| { - SingleLineInput::new(window, cx, Self::PLACEHOLDER_REGION).label("Region") - }), + region_editor: cx + .new(|cx| InputField::new(window, cx, Self::PLACEHOLDER_REGION).label("Region")), state, load_credentials_task, } diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index ec420bfd83b427701ffa6eb13a9eb6035604f0b1..8784d3805f22974ffa441ecd04ddea4b56be911b 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -20,7 +20,7 @@ use std::str::FromStr; use std::sync::{Arc, LazyLock}; use ui::{Icon, IconName, List, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::{EnvVar, env_var}; @@ -525,7 +525,7 @@ impl DeepSeekEventMapper { } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -533,7 +533,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = - cx.new(|cx| SingleLineInput::new(window, cx, "sk-00000000000000000000000000000000")); + cx.new(|cx| InputField::new(window, cx, "sk-00000000000000000000000000000000")); cx.observe(&state, |_, _, cx| { cx.notify(); diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index f6ac364611c0e121115a4bd692893ed8bfa89ab3..a4d1202bee4fc4b2f1e071a815bc2f5887d2457d 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -29,7 +29,7 @@ use std::sync::{ }; use strum::IntoEnumIterator; use ui::{Icon, IconName, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::EnvVar; @@ -751,7 +751,7 @@ fn convert_usage(usage: &UsageMetadata) -> language_model::TokenUsage { } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, target_agent: language_model::ConfigurationViewTargetAgent, load_credentials_task: Option>, @@ -788,7 +788,7 @@ impl ConfigurationView { })); Self { - api_key_editor: cx.new(|cx| SingleLineInput::new(window, cx, "AIzaSy...")), + api_key_editor: cx.new(|cx| InputField::new(window, cx, "AIzaSy...")), target_agent, state, load_credentials_task, diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index ad7bf600d56354ee12e72c9ebc2bfe09f0094da7..66527792ff0b82348457fd28ae04dba60d10de5b 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -20,7 +20,7 @@ use std::str::FromStr; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; use ui::{Icon, IconName, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::{EnvVar, env_var}; @@ -744,8 +744,8 @@ struct RawToolCall { } struct ConfigurationView { - api_key_editor: Entity, - codestral_api_key_editor: Entity, + api_key_editor: Entity, + codestral_api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -753,9 +753,9 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = - cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2")); + cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2")); let codestral_api_key_editor = - cx.new(|cx| SingleLineInput::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2")); + cx.new(|cx| InputField::new(window, cx, "0aBCDEFGhIjKLmNOpqrSTUVwxyzabCDE1f2")); cx.observe(&state, |_, _, cx| { cx.notify(); diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index a25ecbe01aa659817b41cac54d76871b4742ea66..2150966c1af0fdb1bdcc028cba67bcb7b7cbf89f 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -23,7 +23,7 @@ use std::sync::LazyLock; use std::sync::atomic::{AtomicU64, Ordering}; use std::{collections::HashMap, sync::Arc}; use ui::{ButtonLike, ElevationIndex, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use zed_env_vars::{EnvVar, env_var}; use crate::AllLanguageModelSettings; @@ -623,18 +623,17 @@ fn map_to_language_model_completion_events( } struct ConfigurationView { - api_key_editor: Entity, - api_url_editor: Entity, + api_key_editor: Entity, + api_url_editor: Entity, state: Entity, } impl ConfigurationView { pub fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { - let api_key_editor = - cx.new(|cx| SingleLineInput::new(window, cx, "63e02e...").label("API key")); + let api_key_editor = cx.new(|cx| InputField::new(window, cx, "63e02e...").label("API key")); let api_url_editor = cx.new(|cx| { - let input = SingleLineInput::new(window, cx, OLLAMA_API_URL).label("API URL"); + let input = InputField::new(window, cx, OLLAMA_API_URL).label("API URL"); input.set_text(OllamaLanguageModelProvider::api_url(cx), window, cx); input }); diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 3eaaa8b58598328e6e843b1f86b8f4e5cbd04c1e..6c3f063c1111f31a37325f0767a14e8533c1b23f 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -21,7 +21,7 @@ use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; use ui::{ElevationIndex, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::{EnvVar, env_var}; @@ -675,7 +675,7 @@ pub fn count_open_ai_tokens( } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -683,7 +683,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "sk-000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs index cca49c982ce6e93614c4574a84ffaa4bd1d8f0c6..c8a1da5f5af9feb72ec514854403d15d6e73774b 100644 --- a/crates/language_models/src/provider/open_ai_compatible.rs +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -14,7 +14,7 @@ use open_ai::{ResponseStreamEvent, stream_completion}; use settings::{Settings, SettingsStore}; use std::sync::Arc; use ui::{ElevationIndex, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::EnvVar; @@ -340,7 +340,7 @@ impl LanguageModel for OpenAiCompatibleLanguageModel { } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -348,7 +348,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "000000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index 5bfc97c41f60351288ccf08c3a86b4b0947ee997..50131f0a8ef7076420df9a9dc1dbdcd4c840a5c2 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -18,7 +18,7 @@ use std::pin::Pin; use std::str::FromStr as _; use std::sync::{Arc, LazyLock}; use ui::{Icon, IconName, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use zed_env_vars::{EnvVar, env_var}; @@ -692,7 +692,7 @@ pub fn count_open_router_tokens( } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -700,7 +700,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "sk_or_000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index ad12e5a628779eddf333bdd4f91bddbea016c402..ff5d4567c60423939c38d00a1203f613df353ccf 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; use ui::{ElevationIndex, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use vercel::{Model, VERCEL_API_URL}; use zed_env_vars::{EnvVar, env_var}; @@ -362,7 +362,7 @@ pub fn count_vercel_tokens( } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -370,7 +370,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "v1:0000000000000000000000000000000000000000000000000", diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs index 243a2e3e0217f10d580a278d25e7168f4f62fe21..8b51ca12099691e4ae70084b509c6c40547bd432 100644 --- a/crates/language_models/src/provider/x_ai.rs +++ b/crates/language_models/src/provider/x_ai.rs @@ -15,7 +15,7 @@ use settings::{Settings, SettingsStore}; use std::sync::{Arc, LazyLock}; use strum::IntoEnumIterator; use ui::{ElevationIndex, List, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, truncate_and_trailoff}; use x_ai::{Model, XAI_API_URL}; use zed_env_vars::{EnvVar, env_var}; @@ -359,7 +359,7 @@ pub fn count_xai_tokens( } struct ConfigurationView { - api_key_editor: Entity, + api_key_editor: Entity, state: Entity, load_credentials_task: Option>, } @@ -367,7 +367,7 @@ struct ConfigurationView { impl ConfigurationView { fn new(state: Entity, window: &mut Window, cx: &mut Context) -> Self { let api_key_editor = cx.new(|cx| { - SingleLineInput::new( + InputField::new( window, cx, "xai-0000000000000000000000000000000000000000000000000", diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 24278f38673f754e68658ad7df89bdc9b5ef3d2d..2ff3467c4804f7c0a50488a2c4a1e283ea571292 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -34,7 +34,6 @@ settings.workspace = true telemetry.workspace = true theme.workspace = true ui.workspace = true -ui_input.workspace = true util.workspace = true vim_mode_setting.workspace = true workspace.workspace = true diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index e252966814f7eaa381982b4c73583f9e2b051ad2..a1139d7f25f08fa54edf7ea71438b92884c8e124 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -17,7 +17,6 @@ use ui::{ Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName, WithScrollbar as _, prelude::*, rems_from_px, }; -pub use ui_input::font_picker; use workspace::{ AppState, Workspace, WorkspaceId, dock::DockPosition, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index b8a75694ecb7fdc4ce1d78f43c93832524f2d0e7..b2b40bf250c18fc63ed737cd80110f0a9fd83d6d 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -26,6 +26,7 @@ fuzzy.workspace = true gpui.workspace = true menu.workspace = true paths.workspace = true +picker.workspace = true project.workspace = true schemars.workspace = true search.workspace = true diff --git a/crates/settings_ui/src/components.rs b/crates/settings_ui/src/components.rs index d424d96f273de131f65b0eae253f90bd52a21b08..5026ca806128baf27e0c95e0c45b47eba24c8e41 100644 --- a/crates/settings_ui/src/components.rs +++ b/crates/settings_ui/src/components.rs @@ -1,96 +1,9 @@ -use editor::Editor; -use gpui::{Focusable, div}; -use ui::{ - ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement, - ParentElement as _, RenderOnce, Styled as _, Window, -}; - -#[derive(IntoElement)] -pub struct SettingsEditor { - initial_text: Option, - placeholder: Option<&'static str>, - confirm: Option, &mut App)>>, - tab_index: Option, -} - -impl SettingsEditor { - pub fn new() -> Self { - Self { - initial_text: None, - placeholder: None, - confirm: None, - tab_index: None, - } - } - - pub fn with_initial_text(mut self, initial_text: String) -> Self { - self.initial_text = Some(initial_text); - self - } - - pub fn with_placeholder(mut self, placeholder: &'static str) -> Self { - self.placeholder = Some(placeholder); - self - } - - pub fn on_confirm(mut self, confirm: impl Fn(Option, &mut App) + 'static) -> Self { - self.confirm = Some(Box::new(confirm)); - self - } - - pub(crate) fn tab_index(mut self, arg: isize) -> Self { - self.tab_index = Some(arg); - self - } -} - -impl RenderOnce for SettingsEditor { - fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement { - let editor = window.use_state(cx, { - move |window, cx| { - let mut editor = Editor::single_line(window, cx); - if let Some(text) = self.initial_text { - editor.set_text(text, window, cx); - } - - if let Some(placeholder) = self.placeholder { - editor.set_placeholder_text(placeholder, window, cx); - } - // todo(settings_ui): We should have an observe global use for settings store - // so whenever a settings file is updated, the settings ui updates too - editor - } - }); - - let weak_editor = editor.downgrade(); - - let theme_colors = cx.theme().colors(); - - div() - .py_1() - .px_2() - .min_w_64() - .rounded_md() - .border_1() - .border_color(theme_colors.border) - .bg(theme_colors.editor_background) - .when_some(self.tab_index, |this, tab_index| { - let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true); - this.track_focus(&focus_handle) - .focus(|s| s.border_color(theme_colors.border_focused)) - }) - .child(editor) - .when_some(self.confirm, |this, confirm| { - this.on_action::({ - move |_, _, cx| { - let Some(editor) = weak_editor.upgrade() else { - return; - }; - let new_value = editor.read_with(cx, |editor, cx| editor.text(cx)); - let new_value = (!new_value.is_empty()).then_some(new_value); - confirm(new_value, cx); - } - }) - }) - } -} +mod font_picker; +mod icon_theme_picker; +mod input_field; +mod theme_picker; + +pub use font_picker::font_picker; +pub use icon_theme_picker::icon_theme_picker; +pub use input_field::*; +pub use theme_picker::theme_picker; diff --git a/crates/ui_input/src/font_picker.rs b/crates/settings_ui/src/components/font_picker.rs similarity index 100% rename from crates/ui_input/src/font_picker.rs rename to crates/settings_ui/src/components/font_picker.rs diff --git a/crates/settings_ui/src/components/icon_theme_picker.rs b/crates/settings_ui/src/components/icon_theme_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..33a648f81bcacdc961d77d7a5532c9807072dbd2 --- /dev/null +++ b/crates/settings_ui/src/components/icon_theme_picker.rs @@ -0,0 +1,189 @@ +use std::sync::Arc; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; +use picker::{Picker, PickerDelegate}; +use theme::ThemeRegistry; +use ui::{ListItem, ListItemSpacing, prelude::*}; + +type IconThemePicker = Picker; + +pub struct IconThemePickerDelegate { + icon_themes: Vec, + filtered_themes: Vec, + selected_index: usize, + current_theme: SharedString, + on_theme_changed: Arc, +} + +impl IconThemePickerDelegate { + fn new( + current_theme: SharedString, + on_theme_changed: impl Fn(SharedString, &mut App) + 'static, + cx: &mut Context, + ) -> Self { + let theme_registry = ThemeRegistry::global(cx); + + let icon_themes: Vec = theme_registry + .list_icon_themes() + .into_iter() + .map(|theme_meta| theme_meta.name) + .collect(); + + let selected_index = icon_themes + .iter() + .position(|icon_themes| *icon_themes == current_theme) + .unwrap_or(0); + + let filtered_themes = icon_themes + .iter() + .enumerate() + .map(|(index, icon_themes)| StringMatch { + candidate_id: index, + string: icon_themes.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect(); + + Self { + icon_themes, + filtered_themes, + selected_index, + current_theme, + on_theme_changed: Arc::new(on_theme_changed), + } + } +} + +impl PickerDelegate for IconThemePickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.filtered_themes.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { + self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search icon theme…".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let icon_themes = self.icon_themes.clone(); + let current_theme = self.current_theme.clone(); + + let matches: Vec = if query.is_empty() { + icon_themes + .iter() + .enumerate() + .map(|(index, icon_theme)| StringMatch { + candidate_id: index, + string: icon_theme.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + let _candidates: Vec = icon_themes + .iter() + .enumerate() + .map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref())) + .collect(); + + icon_themes + .iter() + .enumerate() + .filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase())) + .map(|(index, icon_theme)| StringMatch { + candidate_id: index, + string: icon_theme.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + }; + + let selected_index = if query.is_empty() { + icon_themes + .iter() + .position(|icon_theme| *icon_theme == current_theme) + .unwrap_or(0) + } else { + matches + .iter() + .position(|m| icon_themes[m.candidate_id] == current_theme) + .unwrap_or(0) + }; + + self.filtered_themes = matches; + self.selected_index = selected_index; + cx.notify(); + + Task::ready(()) + } + + fn confirm( + &mut self, + _secondary: bool, + _window: &mut Window, + cx: &mut Context, + ) { + if let Some(theme_match) = self.filtered_themes.get(self.selected_index) { + let theme = theme_match.string.clone(); + (self.on_theme_changed)(theme.into(), cx); + } + } + + fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { + cx.defer_in(window, |picker, window, cx| { + picker.set_query("", window, cx); + }); + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let theme_match = self.filtered_themes.get(ix)?; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(Label::new(theme_match.string.clone())) + .into_any_element(), + ) + } +} + +pub fn icon_theme_picker( + current_theme: SharedString, + on_theme_changed: impl Fn(SharedString, &mut App) + 'static, + window: &mut Window, + cx: &mut Context, +) -> IconThemePicker { + let delegate = IconThemePickerDelegate::new(current_theme, on_theme_changed, cx); + + Picker::uniform_list(delegate, window, cx) + .show_scrollbar(true) + .width(rems_from_px(210.)) + .max_height(Some(rems(18.).into())) +} diff --git a/crates/settings_ui/src/components/input_field.rs b/crates/settings_ui/src/components/input_field.rs new file mode 100644 index 0000000000000000000000000000000000000000..57917c321127baf2e96e3862106461331afaf86f --- /dev/null +++ b/crates/settings_ui/src/components/input_field.rs @@ -0,0 +1,96 @@ +use editor::Editor; +use gpui::{Focusable, div}; +use ui::{ + ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement, + ParentElement as _, RenderOnce, Styled as _, Window, +}; + +#[derive(IntoElement)] +pub struct SettingsInputField { + initial_text: Option, + placeholder: Option<&'static str>, + confirm: Option, &mut App)>>, + tab_index: Option, +} + +impl SettingsInputField { + pub fn new() -> Self { + Self { + initial_text: None, + placeholder: None, + confirm: None, + tab_index: None, + } + } + + pub fn with_initial_text(mut self, initial_text: String) -> Self { + self.initial_text = Some(initial_text); + self + } + + pub fn with_placeholder(mut self, placeholder: &'static str) -> Self { + self.placeholder = Some(placeholder); + self + } + + pub fn on_confirm(mut self, confirm: impl Fn(Option, &mut App) + 'static) -> Self { + self.confirm = Some(Box::new(confirm)); + self + } + + pub(crate) fn tab_index(mut self, arg: isize) -> Self { + self.tab_index = Some(arg); + self + } +} + +impl RenderOnce for SettingsInputField { + fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement { + let editor = window.use_state(cx, { + move |window, cx| { + let mut editor = Editor::single_line(window, cx); + if let Some(text) = self.initial_text { + editor.set_text(text, window, cx); + } + + if let Some(placeholder) = self.placeholder { + editor.set_placeholder_text(placeholder, window, cx); + } + // todo(settings_ui): We should have an observe global use for settings store + // so whenever a settings file is updated, the settings ui updates too + editor + } + }); + + let weak_editor = editor.downgrade(); + + let theme_colors = cx.theme().colors(); + + div() + .py_1() + .px_2() + .min_w_64() + .rounded_md() + .border_1() + .border_color(theme_colors.border) + .bg(theme_colors.editor_background) + .when_some(self.tab_index, |this, tab_index| { + let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true); + this.track_focus(&focus_handle) + .focus(|s| s.border_color(theme_colors.border_focused)) + }) + .child(editor) + .when_some(self.confirm, |this, confirm| { + this.on_action::({ + move |_, _, cx| { + let Some(editor) = weak_editor.upgrade() else { + return; + }; + let new_value = editor.read_with(cx, |editor, cx| editor.text(cx)); + let new_value = (!new_value.is_empty()).then_some(new_value); + confirm(new_value, cx); + } + }) + }) + } +} diff --git a/crates/settings_ui/src/components/theme_picker.rs b/crates/settings_ui/src/components/theme_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..2146ab314f94bb0c0535a462566e6673fc5601bc --- /dev/null +++ b/crates/settings_ui/src/components/theme_picker.rs @@ -0,0 +1,179 @@ +use std::sync::Arc; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; +use picker::{Picker, PickerDelegate}; +use theme::ThemeRegistry; +use ui::{ListItem, ListItemSpacing, prelude::*}; + +type ThemePicker = Picker; + +pub struct ThemePickerDelegate { + themes: Vec, + filtered_themes: Vec, + selected_index: usize, + current_theme: SharedString, + on_theme_changed: Arc, +} + +impl ThemePickerDelegate { + fn new( + current_theme: SharedString, + on_theme_changed: impl Fn(SharedString, &mut App) + 'static, + cx: &mut Context, + ) -> Self { + let theme_registry = ThemeRegistry::global(cx); + + let themes = theme_registry.list_names(); + let selected_index = themes + .iter() + .position(|theme| *theme == current_theme) + .unwrap_or(0); + + let filtered_themes = themes + .iter() + .enumerate() + .map(|(index, theme)| StringMatch { + candidate_id: index, + string: theme.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect(); + + Self { + themes, + filtered_themes, + selected_index, + current_theme, + on_theme_changed: Arc::new(on_theme_changed), + } + } +} + +impl PickerDelegate for ThemePickerDelegate { + type ListItem = AnyElement; + + fn match_count(&self) -> usize { + self.filtered_themes.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { + self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1)); + cx.notify(); + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Search theme…".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + cx: &mut Context, + ) -> Task<()> { + let themes = self.themes.clone(); + let current_theme = self.current_theme.clone(); + + let matches: Vec = if query.is_empty() { + themes + .iter() + .enumerate() + .map(|(index, theme)| StringMatch { + candidate_id: index, + string: theme.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + let _candidates: Vec = themes + .iter() + .enumerate() + .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref())) + .collect(); + + themes + .iter() + .enumerate() + .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase())) + .map(|(index, theme)| StringMatch { + candidate_id: index, + string: theme.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect() + }; + + let selected_index = if query.is_empty() { + themes + .iter() + .position(|theme| *theme == current_theme) + .unwrap_or(0) + } else { + matches + .iter() + .position(|m| themes[m.candidate_id] == current_theme) + .unwrap_or(0) + }; + + self.filtered_themes = matches; + self.selected_index = selected_index; + cx.notify(); + + Task::ready(()) + } + + fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context) { + if let Some(theme_match) = self.filtered_themes.get(self.selected_index) { + let theme = theme_match.string.clone(); + (self.on_theme_changed)(theme.into(), cx); + } + } + + fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { + cx.defer_in(window, |picker, window, cx| { + picker.set_query("", window, cx); + }); + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut Window, + _cx: &mut Context, + ) -> Option { + let theme_match = self.filtered_themes.get(ix)?; + + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(Label::new(theme_match.string.clone())) + .into_any_element(), + ) + } +} + +pub fn theme_picker( + current_theme: SharedString, + on_theme_changed: impl Fn(SharedString, &mut App) + 'static, + window: &mut Window, + cx: &mut Context, +) -> ThemePicker { + let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx); + + Picker::uniform_list(delegate, window, cx) + .show_scrollbar(true) + .width(rems_from_px(210.)) + .max_height(Some(rems(18.).into())) +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index e4b92464766e4d87e9f3afe6d1d639716b5ac5ab..5c76008e834b456b29079626813ec80423d53d6f 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -36,7 +36,7 @@ use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; use workspace::{OpenOptions, OpenVisible, Workspace, client_side_decorations}; use zed_actions::OpenSettings; -use crate::components::SettingsEditor; +use crate::components::{SettingsInputField, font_picker, icon_theme_picker, theme_picker}; const NAVBAR_CONTAINER_TAB_INDEX: isize = 0; const NAVBAR_GROUP_TAB_INDEX: isize = 1; @@ -2869,7 +2869,7 @@ fn render_text_field + Into + AsRef + Clone>( SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); let initial_text = initial_text.filter(|s| !s.as_ref().is_empty()); - SettingsEditor::new() + SettingsInputField::new() .tab_index(0) .when_some(initial_text, |editor, text| { editor.with_initial_text(text.as_ref().to_string()) @@ -2920,54 +2920,6 @@ fn render_toggle_button + From + Copy>( .into_any_element() } -fn render_font_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let current_value = SettingsStore::global(cx) - .get_value_from_file(file.to_settings(), field.pick) - .1 - .cloned() - .unwrap_or_else(|| SharedString::default().into()); - - let font_picker = cx.new(|cx| { - ui_input::font_picker( - current_value.clone().into(), - move |font_name, cx| { - update_settings_file(file.clone(), cx, move |settings, _cx| { - (field.write)(settings, Some(font_name.into())); - }) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - }); - - PopoverMenu::new("font-picker") - .menu(move |_window, _cx| Some(font_picker.clone())) - .trigger( - Button::new("font-family-button", current_value) - .tab_index(0_isize) - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .icon(IconName::ChevronUpDown) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End), - ) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - fn render_number_field( field: SettingField, file: SettingsUiFile, @@ -3056,6 +3008,59 @@ where .into_any_element() } +fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button { + Button::new(id, label) + .tab_index(0_isize) + .style(ButtonStyle::Outlined) + .size(ButtonSize::Medium) + .icon(IconName::ChevronUpDown) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .icon_position(IconPosition::End) +} + +fn render_font_picker( + field: SettingField, + file: SettingsUiFile, + _metadata: Option<&SettingsFieldMetadata>, + window: &mut Window, + cx: &mut App, +) -> AnyElement { + let current_value = SettingsStore::global(cx) + .get_value_from_file(file.to_settings(), field.pick) + .1 + .cloned() + .unwrap_or_else(|| SharedString::default().into()); + + let font_picker = cx.new(|cx| { + font_picker( + current_value.clone().into(), + move |font_name, cx| { + update_settings_file(file.clone(), cx, move |settings, _cx| { + (field.write)(settings, Some(font_name.into())); + }) + .log_err(); // todo(settings_ui) don't log err + }, + window, + cx, + ) + }); + + PopoverMenu::new("font-picker") + .menu(move |_window, _cx| Some(font_picker.clone())) + .trigger(render_picker_trigger_button( + "font_family_picker_trigger".into(), + current_value.into(), + )) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(2.0), + }) + .with_handle(ui::PopoverMenuHandle::default()) + .into_any_element() +} + fn render_theme_picker( field: SettingField, file: SettingsUiFile, @@ -3069,42 +3074,33 @@ fn render_theme_picker( .map(|theme_name| theme_name.0.into()) .unwrap_or_else(|| cx.theme().name.clone()); - DropdownMenu::new( - "font-picker", - current_value.clone(), - ContextMenu::build(window, cx, move |mut menu, _, cx| { - let all_theme_names = theme::ThemeRegistry::global(cx).list_names(); - for theme_name in all_theme_names { - let file = file.clone(); - let selected = theme_name.as_ref() == current_value.as_ref(); - menu = menu.toggleable_entry( - theme_name.clone(), - selected, - IconPosition::End, - None, - move |_, cx| { - if selected { - return; - } - let theme_name = theme_name.clone(); - update_settings_file(file.clone(), cx, move |settings, _cx| { - (field.write)(settings, Some(settings::ThemeName(theme_name.into()))); - }) - .log_err(); // todo(settings_ui) don't log err - }, - ); - } - menu - }), - ) - .trigger_size(ButtonSize::Medium) - .style(DropdownStyle::Outlined) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .tab_index(0) - .into_any_element() + let theme_picker = cx.new(|cx| { + theme_picker( + current_value.clone(), + move |theme_name, cx| { + update_settings_file(file.clone(), cx, move |settings, _cx| { + (field.write)(settings, Some(settings::ThemeName(theme_name.into()))); + }) + .log_err(); // todo(settings_ui) don't log err + }, + window, + cx, + ) + }); + + PopoverMenu::new("theme-picker") + .menu(move |_window, _cx| Some(theme_picker.clone())) + .trigger(render_picker_trigger_button( + "theme_picker_trigger".into(), + current_value, + )) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(2.0), + }) + .with_handle(ui::PopoverMenuHandle::default()) + .into_any_element() } fn render_icon_theme_picker( @@ -3117,51 +3113,36 @@ fn render_icon_theme_picker( let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); let current_value = value .cloned() - .map(|icon_theme_name| icon_theme_name.0.into()) - .unwrap_or_else(|| theme::default_icon_theme().name.clone()); + .map(|theme_name| theme_name.0.into()) + .unwrap_or_else(|| cx.theme().name.clone()); - DropdownMenu::new( - "font-picker", - current_value.clone(), - ContextMenu::build(window, cx, move |mut menu, _, cx| { - let all_theme_names = theme::ThemeRegistry::global(cx) - .list_icon_themes() - .into_iter() - .map(|theme| theme.name); - for theme_name in all_theme_names { - let file = file.clone(); - let selected = theme_name.as_ref() == current_value.as_ref(); - menu = menu.toggleable_entry( - theme_name.clone(), - selected, - IconPosition::End, - None, - move |_, cx| { - if selected { - return; - } - let theme_name = theme_name.clone(); - update_settings_file(file.clone(), cx, move |settings, _cx| { - (field.write)( - settings, - Some(settings::IconThemeName(theme_name.into())), - ); - }) - .log_err(); // todo(settings_ui) don't log err - }, - ); - } - menu - }), - ) - .trigger_size(ButtonSize::Medium) - .style(DropdownStyle::Outlined) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .tab_index(0) - .into_any_element() + let icon_theme_picker = cx.new(|cx| { + icon_theme_picker( + current_value.clone(), + move |theme_name, cx| { + update_settings_file(file.clone(), cx, move |settings, _cx| { + (field.write)(settings, Some(settings::IconThemeName(theme_name.into()))); + }) + .log_err(); // todo(settings_ui) don't log err + }, + window, + cx, + ) + }); + + PopoverMenu::new("icon-theme-picker") + .menu(move |_window, _cx| Some(icon_theme_picker.clone())) + .trigger(render_picker_trigger_button( + "icon_theme_picker_trigger".into(), + current_value, + )) + .anchor(gpui::Corner::TopLeft) + .offset(gpui::Point { + x: px(0.0), + y: px(2.0), + }) + .with_handle(ui::PopoverMenuHandle::default()) + .into_any_element() } #[cfg(test)] diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml index 7aa74591b82c7455de080a40d9c0955ad0384b59..4e7b08241dff8e3e5c00052826485c309449d205 100644 --- a/crates/ui_input/Cargo.toml +++ b/crates/ui_input/Cargo.toml @@ -14,10 +14,8 @@ path = "src/ui_input.rs" [dependencies] component.workspace = true editor.workspace = true -fuzzy.workspace = true gpui.workspace = true menu.workspace = true -picker.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/ui_input/src/input_field.rs b/crates/ui_input/src/input_field.rs new file mode 100644 index 0000000000000000000000000000000000000000..82f7f0261facef8a7c6a422b2ff4ed335229aeb3 --- /dev/null +++ b/crates/ui_input/src/input_field.rs @@ -0,0 +1,222 @@ +use component::{example_group, single_example}; +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle}; +use settings::Settings; +use std::sync::Arc; +use theme::ThemeSettings; +use ui::prelude::*; + +pub struct InputFieldStyle { + text_color: Hsla, + background_color: Hsla, + border_color: Hsla, +} + +/// An Input Field component that can be used to create text fields like search inputs, form fields, etc. +/// +/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc. +#[derive(RegisterComponent)] +pub struct InputField { + /// An optional label for the text field. + /// + /// Its position is determined by the [`FieldLabelLayout`]. + label: Option, + /// The size of the label text. + label_size: LabelSize, + /// The placeholder text for the text field. + placeholder: SharedString, + /// Exposes the underlying [`Entity`] to allow for customizing the editor beyond the provided API. + /// + /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases. + pub editor: Entity, + /// An optional icon that is displayed at the start of the text field. + /// + /// For example, a magnifying glass icon in a search field. + start_icon: Option, + /// Whether the text field is disabled. + disabled: bool, + /// The minimum width of for the input + min_width: Length, +} + +impl Focusable for InputField { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + +impl InputField { + pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into) -> Self { + let placeholder_text = placeholder.into(); + + let editor = cx.new(|cx| { + let mut input = Editor::single_line(window, cx); + input.set_placeholder_text(&placeholder_text, window, cx); + input + }); + + Self { + label: None, + label_size: LabelSize::Small, + placeholder: placeholder_text, + editor, + start_icon: None, + disabled: false, + min_width: px(192.).into(), + } + } + + pub fn start_icon(mut self, icon: IconName) -> Self { + self.start_icon = Some(icon); + self + } + + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + pub fn label_size(mut self, size: LabelSize) -> Self { + self.label_size = size; + self + } + + pub fn label_min_width(mut self, width: impl Into) -> Self { + self.min_width = width.into(); + self + } + + pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { + self.disabled = disabled; + self.editor + .update(cx, |editor, _| editor.set_read_only(disabled)) + } + + pub fn is_empty(&self, cx: &App) -> bool { + self.editor().read(cx).text(cx).trim().is_empty() + } + + pub fn editor(&self) -> &Entity { + &self.editor + } + + pub fn text(&self, cx: &App) -> String { + self.editor().read(cx).text(cx) + } + + pub fn set_text(&self, text: impl Into>, window: &mut Window, cx: &mut App) { + self.editor() + .update(cx, |editor, cx| editor.set_text(text, window, cx)) + } +} + +impl Render for InputField { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let theme_color = cx.theme().colors(); + + let mut style = InputFieldStyle { + text_color: theme_color.text, + background_color: theme_color.editor_background, + border_color: theme_color.border_variant, + }; + + if self.disabled { + style.text_color = theme_color.text_disabled; + style.background_color = theme_color.editor_background; + style.border_color = theme_color.border_disabled; + } + + // if self.error_message.is_some() { + // style.text_color = cx.theme().status().error; + // style.border_color = cx.theme().status().error_border + // } + + let text_style = TextStyle { + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_size: rems(0.875).into(), + font_weight: settings.buffer_font.weight, + font_style: FontStyle::Normal, + line_height: relative(1.2), + color: style.text_color, + ..Default::default() + }; + + let editor_style = EditorStyle { + background: theme_color.ghost_element_background, + local_player: cx.theme().players().local(), + syntax: cx.theme().syntax().clone(), + text: text_style, + ..Default::default() + }; + + v_flex() + .id(self.placeholder.clone()) + .w_full() + .gap_1() + .when_some(self.label.clone(), |this, label| { + this.child( + Label::new(label) + .size(self.label_size) + .color(if self.disabled { + Color::Disabled + } else { + Color::Default + }), + ) + }) + .child( + h_flex() + .min_w(self.min_width) + .min_h_8() + .w_full() + .px_2() + .py_1p5() + .flex_grow() + .text_color(style.text_color) + .rounded_md() + .bg(style.background_color) + .border_1() + .border_color(style.border_color) + .when_some(self.start_icon, |this, icon| { + this.gap_1() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + }) + .child(EditorElement::new(&self.editor, editor_style)), + ) + } +} + +impl Component for InputField { + fn scope() -> ComponentScope { + ComponentScope::Input + } + + fn preview(window: &mut Window, cx: &mut App) -> Option { + let input_small = + cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label")); + + let input_regular = cx.new(|cx| { + InputField::new(window, cx, "placeholder") + .label("Regular Label") + .label_size(LabelSize::Default) + }); + + Some( + v_flex() + .gap_6() + .children(vec![example_group(vec![ + single_example( + "Small Label (Default)", + div().child(input_small).into_any_element(), + ), + single_example( + "Regular Label", + div().child(input_regular).into_any_element(), + ), + ])]) + .into_any_element(), + ) + } +} diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 56f0626f0a502c2bbf5471491441f69e7820e86e..ddc0e659a2c34ffe53424bff24480c3f1b5875fb 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -1,233 +1,9 @@ -//! # UI – Text Field -//! -//! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc. +//! This crate provides UI components that can be used for form-like scenarios, such as a input and number field. //! //! It can't be located in the `ui` crate because it depends on `editor`. //! -mod font_picker; +mod input_field; mod number_field; -use component::{example_group, single_example}; -use editor::{Editor, EditorElement, EditorStyle}; -pub use font_picker::*; -use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle}; +pub use input_field::*; pub use number_field::*; -use settings::Settings; -use std::sync::Arc; -use theme::ThemeSettings; -use ui::prelude::*; - -pub struct SingleLineInputStyle { - text_color: Hsla, - background_color: Hsla, - border_color: Hsla, -} - -/// A Text Field that can be used to create text fields like search inputs, form fields, etc. -/// -/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc. -#[derive(RegisterComponent)] -pub struct SingleLineInput { - /// An optional label for the text field. - /// - /// Its position is determined by the [`FieldLabelLayout`]. - label: Option, - /// The size of the label text. - label_size: LabelSize, - /// The placeholder text for the text field. - placeholder: SharedString, - /// Exposes the underlying [`Entity`] to allow for customizing the editor beyond the provided API. - /// - /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases. - pub editor: Entity, - /// An optional icon that is displayed at the start of the text field. - /// - /// For example, a magnifying glass icon in a search field. - start_icon: Option, - /// Whether the text field is disabled. - disabled: bool, - /// The minimum width of for the input - min_width: Length, -} - -impl Focusable for SingleLineInput { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl SingleLineInput { - pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into) -> Self { - let placeholder_text = placeholder.into(); - - let editor = cx.new(|cx| { - let mut input = Editor::single_line(window, cx); - input.set_placeholder_text(&placeholder_text, window, cx); - input - }); - - Self { - label: None, - label_size: LabelSize::Small, - placeholder: placeholder_text, - editor, - start_icon: None, - disabled: false, - min_width: px(192.).into(), - } - } - - pub fn start_icon(mut self, icon: IconName) -> Self { - self.start_icon = Some(icon); - self - } - - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn label_min_width(mut self, width: impl Into) -> Self { - self.min_width = width.into(); - self - } - - pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { - self.disabled = disabled; - self.editor - .update(cx, |editor, _| editor.set_read_only(disabled)) - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.editor().read(cx).text(cx).trim().is_empty() - } - - pub fn editor(&self) -> &Entity { - &self.editor - } - - pub fn text(&self, cx: &App) -> String { - self.editor().read(cx).text(cx) - } - - pub fn set_text(&self, text: impl Into>, window: &mut Window, cx: &mut App) { - self.editor() - .update(cx, |editor, cx| editor.set_text(text, window, cx)) - } -} - -impl Render for SingleLineInput { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let theme_color = cx.theme().colors(); - - let mut style = SingleLineInputStyle { - text_color: theme_color.text, - background_color: theme_color.editor_background, - border_color: theme_color.border_variant, - }; - - if self.disabled { - style.text_color = theme_color.text_disabled; - style.background_color = theme_color.editor_background; - style.border_color = theme_color.border_disabled; - } - - // if self.error_message.is_some() { - // style.text_color = cx.theme().status().error; - // style.border_color = cx.theme().status().error_border - // } - - let text_style = TextStyle { - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - font_style: FontStyle::Normal, - line_height: relative(1.2), - color: style.text_color, - ..Default::default() - }; - - let editor_style = EditorStyle { - background: theme_color.ghost_element_background, - local_player: cx.theme().players().local(), - syntax: cx.theme().syntax().clone(), - text: text_style, - ..Default::default() - }; - - v_flex() - .id(self.placeholder.clone()) - .w_full() - .gap_1() - .when_some(self.label.clone(), |this, label| { - this.child( - Label::new(label) - .size(self.label_size) - .color(if self.disabled { - Color::Disabled - } else { - Color::Default - }), - ) - }) - .child( - h_flex() - .min_w(self.min_width) - .min_h_8() - .w_full() - .px_2() - .py_1p5() - .flex_grow() - .text_color(style.text_color) - .rounded_md() - .bg(style.background_color) - .border_1() - .border_color(style.border_color) - .when_some(self.start_icon, |this, icon| { - this.gap_1() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - }) - .child(EditorElement::new(&self.editor, editor_style)), - ) - } -} - -impl Component for SingleLineInput { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let input_small = - cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Small Label")); - - let input_regular = cx.new(|cx| { - SingleLineInput::new(window, cx, "placeholder") - .label("Regular Label") - .label_size(LabelSize::Default) - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group(vec![ - single_example( - "Small Label (Default)", - div().child(input_small).into_any_element(), - ), - single_example( - "Regular Label", - div().child(input_regular).into_any_element(), - ), - ])]) - .into_any_element(), - ) - } -} diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 75369bbe0324b2fd4bbe9279ed253faac52487c8..153d66f04e0b0aafe4b8d8dd14cedb05f44b496f 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -17,7 +17,7 @@ use persistence::COMPONENT_PREVIEW_DB; use project::Project; use std::{iter::Iterator, ops::Range, sync::Arc}; use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use workspace::{ AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, item::ItemEvent, @@ -99,7 +99,7 @@ struct ComponentPreview { component_map: HashMap, components: Vec, cursor_index: usize, - filter_editor: Entity, + filter_editor: Entity, filter_text: String, focus_handle: FocusHandle, language_registry: Arc, @@ -126,8 +126,7 @@ impl ComponentPreview { let sorted_components = component_registry.sorted_components(); let selected_index = selected_index.into().unwrap_or(0); let active_page = active_page.unwrap_or(PreviewPage::AllComponents); - let filter_editor = - cx.new(|cx| SingleLineInput::new(window, cx, "Find components or usages…")); + let filter_editor = cx.new(|cx| InputField::new(window, cx, "Find components or usages…")); let component_list = ListState::new( sorted_components.len(), diff --git a/crates/zeta2_tools/src/zeta2_tools.rs b/crates/zeta2_tools/src/zeta2_tools.rs index 7b806e2b9a4ba7c7dbda41bb0f5750e5d2b9ff97..dbb7a5af7d84c6cf043451ae6412e4e2cacc6408 100644 --- a/crates/zeta2_tools/src/zeta2_tools.rs +++ b/crates/zeta2_tools/src/zeta2_tools.rs @@ -17,7 +17,7 @@ use language::{Buffer, DiskState}; use ordered_float::OrderedFloat; use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot}; use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*}; -use ui_input::SingleLineInput; +use ui_input::InputField; use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; use workspace::{Item, SplitDirection, Workspace}; use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions}; @@ -65,11 +65,11 @@ pub struct Zeta2Inspector { focus_handle: FocusHandle, project: Entity, last_prediction: Option, - max_excerpt_bytes_input: Entity, - min_excerpt_bytes_input: Entity, - cursor_context_ratio_input: Entity, - max_prompt_bytes_input: Entity, - max_retrieved_declarations: Entity, + max_excerpt_bytes_input: Entity, + min_excerpt_bytes_input: Entity, + cursor_context_ratio_input: Entity, + max_prompt_bytes_input: Entity, + max_retrieved_declarations: Entity, active_view: ActiveView, zeta: Entity, _active_editor_subscription: Option, @@ -225,9 +225,9 @@ impl Zeta2Inspector { label: &'static str, window: &mut Window, cx: &mut Context, - ) -> Entity { + ) -> Entity { let input = cx.new(|cx| { - SingleLineInput::new(window, cx, "") + InputField::new(window, cx, "") .label(label) .label_min_width(px(64.)) }); @@ -241,7 +241,7 @@ impl Zeta2Inspector { }; fn number_input_value( - input: &Entity, + input: &Entity, cx: &App, ) -> T { input From 0c13403fa548713ce8767943468f86b77669773e Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 21 Oct 2025 16:09:49 -0700 Subject: [PATCH 125/202] settings_ui: Add broken file warning banner (#40823) Closes #ISSUE Release Notes: - settings_ui: Added a warning banner when the settings file you are actively editing is in a broken or invalid state. --- crates/settings/src/settings_store.rs | 122 +++++++++++++++++++++++--- crates/settings_ui/src/settings_ui.rs | 33 ++++++- 2 files changed, 141 insertions(+), 14 deletions(-) diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 709b4982706250f91c7aaefc365ddf7613cdf5f4..e971aedd4cd87b4706a465b532846970c2772e23 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -148,9 +148,10 @@ pub struct SettingsStore { _setting_file_updates: Task<()>, setting_file_updates_tx: mpsc::UnboundedSender LocalBoxFuture<'static, Result<()>>>>, + file_errors: BTreeMap, } -#[derive(Clone, PartialEq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum SettingsFile { User, Server, @@ -159,6 +160,34 @@ pub enum SettingsFile { Project((WorktreeId, Arc)), } +impl PartialOrd for SettingsFile { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Sorted in order of precedence +impl Ord for SettingsFile { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + use SettingsFile::*; + use std::cmp::Ordering; + match (self, other) { + (User, User) => Ordering::Equal, + (Server, Server) => Ordering::Equal, + (Default, Default) => Ordering::Equal, + (Project((id1, rel_path1)), Project((id2, rel_path2))) => id1 + .cmp(id2) + .then_with(|| rel_path1.cmp(rel_path2).reverse()), + (Project(_), _) => Ordering::Less, + (_, Project(_)) => Ordering::Greater, + (Server, _) => Ordering::Less, + (_, Server) => Ordering::Greater, + (User, _) => Ordering::Less, + (_, User) => Ordering::Greater, + } + } +} + #[derive(Clone)] pub struct Editorconfig { pub is_root: bool, @@ -228,6 +257,7 @@ impl SettingsStore { (setting_file_update)(cx.clone()).await.log_err(); } }), + file_errors: BTreeMap::default(), } } @@ -586,6 +616,24 @@ impl SettingsStore { (SettingsFile::Default, None) } + + fn handle_potential_file_error( + &mut self, + file: SettingsFile, + result: Result, + ) -> Result { + if let Err(err) = result.as_ref() { + let message = err.to_string(); + self.file_errors.insert(file, message); + } else { + self.file_errors.remove(&file); + } + return result; + } + + pub fn error_for_file(&self, file: SettingsFile) -> Option { + self.file_errors.get(&file).cloned() + } } impl SettingsStore { @@ -658,7 +706,10 @@ impl SettingsStore { let settings: UserSettingsContent = if user_settings_content.is_empty() { parse_json_with_comments("{}")? } else { - parse_json_with_comments(user_settings_content)? + self.handle_potential_file_error( + SettingsFile::User, + parse_json_with_comments(user_settings_content), + )? }; self.user_settings = Some(settings); @@ -691,7 +742,10 @@ impl SettingsStore { let settings: Option = if server_settings_content.is_empty() { None } else { - parse_json_with_comments(server_settings_content)? + self.handle_potential_file_error( + SettingsFile::Server, + parse_json_with_comments(server_settings_content), + )? }; // Rewrite the server settings into a content type @@ -740,20 +794,24 @@ impl SettingsStore { zed_settings_changed = self .local_settings .remove(&(root_id, directory_path.clone())) - .is_some() + .is_some(); + self.file_errors + .remove(&SettingsFile::Project((root_id, directory_path.clone()))); } (LocalSettingsKind::Editorconfig, None) => { self.raw_editorconfig_settings .remove(&(root_id, directory_path.clone())); } (LocalSettingsKind::Settings, Some(settings_contents)) => { - let new_settings = parse_json_with_comments::( - settings_contents, - ) - .map_err(|e| InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: e.to_string(), - })?; + let new_settings = self + .handle_potential_file_error( + SettingsFile::Project((root_id, directory_path.clone())), + parse_json_with_comments::(settings_contents), + ) + .map_err(|e| InvalidSettingsError::LocalSettings { + path: directory_path.join(local_settings_file_relative_path()), + message: e.to_string(), + })?; match self.local_settings.entry((root_id, directory_path.clone())) { btree_map::Entry::Vacant(v) => { v.insert(SettingsContent { @@ -931,6 +989,7 @@ impl SettingsStore { .to_value() } + // todo -> this function never fails, and should not return a result fn recompute_values( &mut self, changed_local_path: Option<(WorktreeId, &RelPath)>, @@ -2032,4 +2091,45 @@ mod tests { let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child1), get); assert_eq!(overrides, vec![]); } + + #[test] + fn test_file_ord() { + let wt0_root = + SettingsFile::Project((WorktreeId::from_usize(0), RelPath::empty().into_arc())); + let wt0_child1 = + SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child1").into_arc())); + let wt0_child2 = + SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child2").into_arc())); + + let wt1_root = + SettingsFile::Project((WorktreeId::from_usize(1), RelPath::empty().into_arc())); + let wt1_subdir = + SettingsFile::Project((WorktreeId::from_usize(1), rel_path("subdir").into_arc())); + + let mut files = vec![ + &wt1_root, + &SettingsFile::Default, + &wt0_root, + &wt1_subdir, + &wt0_child2, + &SettingsFile::Server, + &wt0_child1, + &SettingsFile::User, + ]; + + files.sort(); + pretty_assertions::assert_eq!( + files, + vec![ + &wt0_child2, + &wt0_child1, + &wt0_root, + &wt1_subdir, + &wt1_root, + &SettingsFile::Server, + &SettingsFile::User, + &SettingsFile::Default, + ] + ) + } } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 5c76008e834b456b29079626813ec80423d53d6f..0485b2608c99f74fbdc86d1cf0807ebb9727c7e8 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -27,9 +27,9 @@ use std::{ }; use title_bar::platform_title_bar::PlatformTitleBar; use ui::{ - ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, KeyBinding, - KeybindingHint, PopoverMenu, Switch, SwitchColor, Tooltip, TreeViewItem, WithScrollbar, - prelude::*, + Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, + KeyBinding, KeybindingHint, PopoverMenu, Switch, SwitchColor, Tooltip, TreeViewItem, + WithScrollbar, prelude::*, }; use ui_input::{NumberField, NumberFieldType}; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; @@ -2436,6 +2436,32 @@ impl SettingsWindow { page_content = (active_page_render_fn)(self, window, cx); } + let mut warning_banner = gpui::Empty.into_any_element(); + if let Some(error) = + SettingsStore::global(cx).error_for_file(self.current_file.to_settings()) + { + warning_banner = v_flex() + .pb_4() + .child( + Banner::new() + .severity(Severity::Warning) + .child( + Label::new("Your Settings File is in an Invalid State. Setting Values May Be Incorrect, and Changes May Be Lost") + .size(LabelSize::Large), + ) + .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)) + .action_slot( + Button::new("fix-in-json", "Fix in settings.json") + .tab_index(0_isize) + .style(ButtonStyle::OutlinedGhost) + .on_click(cx.listener(|this, _, _, cx| { + this.open_current_settings_file(cx); + })), + ), + ) + .into_any_element() + } + return v_flex() .id("Settings-ui-page") .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { @@ -2497,6 +2523,7 @@ impl SettingsWindow { } window.focus_prev(); })) + .child(warning_banner) .child(page_header) .when(sub_page_stack().is_empty(), |this| { this.vertical_scrollbar_for(self.list_state.clone(), window, cx) From 2deafd87066ee3e03b52105ed32391a8483c02e8 Mon Sep 17 00:00:00 2001 From: Nia Date: Wed, 22 Oct 2025 02:24:38 +0200 Subject: [PATCH 126/202] project_panel: Don't show trash dialog on remote connections (#40838) In remote connections, we don't detect whether trashing is possible and generally shouldn't assume it is. This also fixes up some incomplete logic that was attempting to do that. Closes #39212 Release Notes: - No longer show trash option in remote projects --- crates/project/src/project.rs | 68 ++++++++++++++++++++++- crates/project_panel/src/project_panel.rs | 15 +++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d167434c52abc161f81d92e2a51c992d52fb9872..f301c7800a5b098ddc93a7badc1617f7842e62d1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1825,10 +1825,12 @@ impl Project { project } + #[inline] pub fn dap_store(&self) -> Entity { self.dap_store.clone() } + #[inline] pub fn breakpoint_store(&self) -> Entity { self.breakpoint_store.clone() } @@ -1842,50 +1844,62 @@ impl Project { Some((session, active_position.clone())) } + #[inline] pub fn lsp_store(&self) -> Entity { self.lsp_store.clone() } + #[inline] pub fn worktree_store(&self) -> Entity { self.worktree_store.clone() } + #[inline] pub fn context_server_store(&self) -> Entity { self.context_server_store.clone() } + #[inline] pub fn buffer_for_id(&self, remote_id: BufferId, cx: &App) -> Option> { self.buffer_store.read(cx).get(remote_id) } + #[inline] pub fn languages(&self) -> &Arc { &self.languages } + #[inline] pub fn client(&self) -> Arc { self.collab_client.clone() } + #[inline] pub fn remote_client(&self) -> Option> { self.remote_client.clone() } + #[inline] pub fn user_store(&self) -> Entity { self.user_store.clone() } + #[inline] pub fn node_runtime(&self) -> Option<&NodeRuntime> { self.node.as_ref() } + #[inline] pub fn opened_buffers(&self, cx: &App) -> Vec> { self.buffer_store.read(cx).buffers().collect() } + #[inline] pub fn environment(&self) -> &Entity { &self.environment } + #[inline] pub fn cli_environment(&self, cx: &App) -> Option> { self.environment.read(cx).get_cli_environment() } @@ -1916,6 +1930,7 @@ impl Project { }) } + #[inline] pub fn peek_environment_error<'a>( &'a self, cx: &'a App, @@ -1923,6 +1938,7 @@ impl Project { self.environment.read(cx).peek_environment_error() } + #[inline] pub fn pop_environment_error(&mut self, cx: &mut Context) { self.environment.update(cx, |environment, _| { environment.pop_environment_error(); @@ -1930,6 +1946,7 @@ impl Project { } #[cfg(any(test, feature = "test-support"))] + #[inline] pub fn has_open_buffer(&self, path: impl Into, cx: &App) -> bool { self.buffer_store .read(cx) @@ -1937,10 +1954,12 @@ impl Project { .is_some() } + #[inline] pub fn fs(&self) -> &Arc { &self.fs } + #[inline] pub fn remote_id(&self) -> Option { match self.client_state { ProjectClientState::Local => None, @@ -1949,6 +1968,7 @@ impl Project { } } + #[inline] pub fn supports_terminal(&self, _cx: &App) -> bool { if self.is_local() { return true; @@ -1960,18 +1980,21 @@ impl Project { false } + #[inline] pub fn remote_connection_state(&self, cx: &App) -> Option { self.remote_client .as_ref() .map(|remote| remote.read(cx).connection_state()) } + #[inline] pub fn remote_connection_options(&self, cx: &App) -> Option { self.remote_client .as_ref() .map(|remote| remote.read(cx).connection_options()) } + #[inline] pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, @@ -1985,14 +2008,17 @@ impl Project { } } + #[inline] pub fn task_store(&self) -> &Entity { &self.task_store } + #[inline] pub fn snippets(&self) -> &Entity { &self.snippets } + #[inline] pub fn search_history(&self, kind: SearchInputKind) -> &SearchHistory { match kind { SearchInputKind::Query => &self.search_history, @@ -2001,6 +2027,7 @@ impl Project { } } + #[inline] pub fn search_history_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistory { match kind { SearchInputKind::Query => &mut self.search_history, @@ -2009,14 +2036,17 @@ impl Project { } } + #[inline] pub fn collaborators(&self) -> &HashMap { &self.collaborators } + #[inline] pub fn host(&self) -> Option<&Collaborator> { self.collaborators.values().find(|c| c.is_host) } + #[inline] pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool, cx: &mut App) { self.worktree_store.update(cx, |store, _| { store.set_worktrees_reordered(worktrees_reordered); @@ -2024,6 +2054,7 @@ impl Project { } /// Collect all worktrees, including ones that don't appear in the project panel + #[inline] pub fn worktrees<'a>( &self, cx: &'a App, @@ -2032,6 +2063,7 @@ impl Project { } /// Collect all user-visible worktrees, the ones that appear in the project panel. + #[inline] pub fn visible_worktrees<'a>( &'a self, cx: &'a App, @@ -2039,16 +2071,19 @@ impl Project { self.worktree_store.read(cx).visible_worktrees(cx) } + #[inline] pub fn worktree_for_root_name(&self, root_name: &str, cx: &App) -> Option> { self.visible_worktrees(cx) .find(|tree| tree.read(cx).root_name() == root_name) } + #[inline] pub fn worktree_root_names<'a>(&'a self, cx: &'a App) -> impl Iterator { self.visible_worktrees(cx) .map(|tree| tree.read(cx).root_name().as_unix_str()) } + #[inline] pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option> { self.worktree_store.read(cx).worktree_for_id(id, cx) } @@ -2063,12 +2098,14 @@ impl Project { .worktree_for_entry(entry_id, cx) } + #[inline] pub fn worktree_id_for_entry(&self, entry_id: ProjectEntryId, cx: &App) -> Option { self.worktree_for_entry(entry_id, cx) .map(|worktree| worktree.read(cx).id()) } /// Checks if the entry is the root of a worktree. + #[inline] pub fn entry_is_worktree_root(&self, entry_id: ProjectEntryId, cx: &App) -> bool { self.worktree_for_entry(entry_id, cx) .map(|worktree| { @@ -2080,6 +2117,7 @@ impl Project { .unwrap_or(false) } + #[inline] pub fn project_path_git_status( &self, project_path: &ProjectPath, @@ -2090,6 +2128,7 @@ impl Project { .project_path_git_status(project_path, cx) } + #[inline] pub fn visibility_for_paths( &self, paths: &[PathBuf], @@ -2141,6 +2180,7 @@ impl Project { }) } + #[inline] pub fn copy_entry( &mut self, entry_id: ProjectEntryId, @@ -2219,6 +2259,7 @@ impl Project { }) } + #[inline] pub fn delete_file( &mut self, path: ProjectPath, @@ -2229,6 +2270,7 @@ impl Project { self.delete_entry(entry.id, trash, cx) } + #[inline] pub fn delete_entry( &mut self, entry_id: ProjectEntryId, @@ -2242,6 +2284,7 @@ impl Project { }) } + #[inline] pub fn expand_entry( &mut self, worktree_id: WorktreeId, @@ -2393,6 +2436,7 @@ impl Project { Ok(()) } + #[inline] pub fn unshare(&mut self, cx: &mut Context) -> Result<()> { self.unshare_internal(cx)?; cx.emit(Event::RemoteIdChanged(None)); @@ -2489,10 +2533,12 @@ impl Project { } } + #[inline] pub fn close(&mut self, cx: &mut Context) { cx.emit(Event::Closed); } + #[inline] pub fn is_disconnected(&self, cx: &App) -> bool { match &self.client_state { ProjectClientState::Remote { @@ -2506,6 +2552,7 @@ impl Project { } } + #[inline] fn remote_client_is_disconnected(&self, cx: &App) -> bool { self.remote_client .as_ref() @@ -2513,6 +2560,7 @@ impl Project { .unwrap_or(false) } + #[inline] pub fn capability(&self) -> Capability { match &self.client_state { ProjectClientState::Remote { capability, .. } => *capability, @@ -2520,10 +2568,12 @@ impl Project { } } + #[inline] pub fn is_read_only(&self, cx: &App) -> bool { self.is_disconnected(cx) || self.capability() == Capability::ReadOnly } + #[inline] pub fn is_local(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { @@ -2533,6 +2583,8 @@ impl Project { } } + /// Whether this project is a remote server (not counting collab). + #[inline] pub fn is_via_remote_server(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { @@ -2542,6 +2594,8 @@ impl Project { } } + /// Whether this project is from collab (not counting remote servers). + #[inline] pub fn is_via_collab(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => false, @@ -2549,6 +2603,17 @@ impl Project { } } + /// `!self.is_local()` + #[inline] + pub fn is_remote(&self) -> bool { + debug_assert_eq!( + !self.is_local(), + self.is_via_collab() || self.is_via_remote_server() + ); + !self.is_local() + } + + #[inline] pub fn create_buffer( &mut self, searchable: bool, @@ -2559,6 +2624,7 @@ impl Project { }) } + #[inline] pub fn create_local_buffer( &mut self, text: &str, @@ -2566,7 +2632,7 @@ impl Project { project_searchable: bool, cx: &mut Context, ) -> Entity { - if self.is_via_collab() || self.is_via_remote_server() { + if self.is_remote() { panic!("called create_local_buffer on a remote project") } self.buffer_store.update(cx, |buffer_store, cx| { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 98ff619588d42f80ec53e9e91133d47e63cfcbee..67d01a4459bde943e1bdcbaf3d15b3db0f56ce3e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -615,8 +615,11 @@ impl ProjectPanel { .detach(); let trash_action = [TypeId::of::()]; - let is_remote = project.read(cx).is_via_collab(); + let is_remote = project.read(cx).is_remote(); + // Make sure the trash option is never displayed anywhere on remote + // hosts since they may not support trashing. May want to dynamically + // detect this in the future. if is_remote { CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&trash_action); @@ -981,7 +984,7 @@ impl ProjectPanel { let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree); let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree); let is_read_only = project.is_read_only(cx); - let is_remote = project.is_via_collab(); + let is_remote = project.is_remote(); let is_local = project.is_local(); let settings = ProjectPanelSettings::get_global(cx); @@ -1045,13 +1048,13 @@ impl ProjectPanel { .when(!should_hide_rename, |menu| { menu.action("Rename", Box::new(Rename)) }) - .when(!is_root & !is_remote, |menu| { + .when(!is_root && !is_remote, |menu| { menu.action("Trash", Box::new(Trash { skip_prompt: false })) }) .when(!is_root, |menu| { menu.action("Delete", Box::new(Delete { skip_prompt: false })) }) - .when(!is_remote & is_root, |menu| { + .when(!is_remote && is_root, |menu| { menu.separator() .action( "Add Folder to Project…", @@ -5458,11 +5461,13 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::new_directory)) .on_action(cx.listener(Self::rename)) .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::trash)) .on_action(cx.listener(Self::cut)) .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) + .when(!project.is_remote(), |el| { + el.on_action(cx.listener(Self::trash)) + }) }) .when(project.is_local(), |el| { el.on_action(cx.listener(Self::reveal_in_finder)) From 2338164c100722c6e44050c372206dc40663bea6 Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 21 Oct 2025 19:54:46 -0500 Subject: [PATCH 127/202] Revert "title_bar: Add configurable window controls position (#38834)" (#40839) This reverts commit b479d1ef49120c5b7d8a319d918ac18c398d1d3b. This PR was accidentally merged prematurely. https://github.com/zed-industries/zed/pull/38834#issuecomment-3424186051 cc @danilo-leal Release Notes: - N/A --- crates/settings/src/settings_content.rs | 30 ----- crates/title_bar/src/platform_title_bar.rs | 103 +++++------------- .../title_bar/src/platforms/platform_linux.rs | 78 ++++--------- crates/title_bar/src/title_bar_settings.rs | 4 +- 4 files changed, 53 insertions(+), 162 deletions(-) diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs index 1c5b392c4b352da75d8818daaa83a08ee4f2147d..42b88bd3654159ca3ad55dfecffbe3d4e2b547d0 100644 --- a/crates/settings/src/settings_content.rs +++ b/crates/settings/src/settings_content.rs @@ -260,32 +260,6 @@ impl strum::VariantNames for BaseKeymapContent { ]; } -/// Position of window control buttons on Linux. -/// -/// Valid values: "left" (macOS style) or "right" (Windows/Linux style) -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - Default, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum WindowControlsPosition { - /// Window controls on the left side (macOS style) - Left, - /// Window controls on the right side (Windows style) - #[default] - Right, -} - #[skip_serializing_none] #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] pub struct TitleBarSettingsContent { @@ -317,10 +291,6 @@ pub struct TitleBarSettingsContent { /// /// Default: false pub show_menus: Option, - /// Position of window control buttons (minimize, maximize, close) on Linux. - /// - /// Default: right - pub window_controls_position: Option, } /// Configuration of audio in Zed. diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs index f078777c2c05bb10afbcd400736c6a428473eeed..fd03e764629454411c9726ef7dcf055d54582d7e 100644 --- a/crates/title_bar/src/platform_title_bar.rs +++ b/crates/title_bar/src/platform_title_bar.rs @@ -2,7 +2,6 @@ use gpui::{ AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, }; -use settings::{Settings, WindowControlsPosition}; use smallvec::SmallVec; use std::mem; use ui::prelude::*; @@ -10,7 +9,6 @@ use ui::prelude::*; use crate::{ platforms::{platform_linux, platform_mac, platform_windows}, system_window_tabs::SystemWindowTabs, - title_bar_settings::TitleBarSettings, }; pub struct PlatformTitleBar { @@ -136,78 +134,35 @@ impl Render for PlatformTitleBar { PlatformStyle::Mac => title_bar, PlatformStyle::Linux => { if matches!(decorations, Decorations::Client { .. }) { - let title_bar_settings = TitleBarSettings::get(None, cx); - match title_bar_settings.window_controls_position { - WindowControlsPosition::Left => h_flex() - .w_full() - .bg(titlebar_color) - .child(platform_linux::LinuxWindowControls::new(close_action)) - .child(title_bar) - .when(supported_controls.window_menu, |titlebar| { - titlebar.on_mouse_down( - MouseButton::Right, - move |ev, window, _| { - window.show_window_menu(ev.position) - }, - ) - }) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.should_move { - this.should_move = false; - window.start_window_move(); - } - })) - .on_mouse_down_out(cx.listener( - move |this, _ev, _window, _cx| { - this.should_move = false; - }, - )) - .on_mouse_up( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = true; - }), - ), - WindowControlsPosition::Right => title_bar - .child(platform_linux::LinuxWindowControls::new(close_action)) - .when(supported_controls.window_menu, |titlebar| { - titlebar.on_mouse_down( - MouseButton::Right, - move |ev, window, _| { - window.show_window_menu(ev.position) - }, - ) - }) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.should_move { - this.should_move = false; - window.start_window_move(); - } - })) - .on_mouse_down_out(cx.listener( - move |this, _ev, _window, _cx| { - this.should_move = false; - }, - )) - .on_mouse_up( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = true; - }), - ), - } + title_bar + .child(platform_linux::LinuxWindowControls::new(close_action)) + .when(supported_controls.window_menu, |titlebar| { + titlebar + .on_mouse_down(MouseButton::Right, move |ev, window, _| { + window.show_window_menu(ev.position) + }) + }) + .on_mouse_move(cx.listener(move |this, _ev, window, _| { + if this.should_move { + this.should_move = false; + window.start_window_move(); + } + })) + .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { + this.should_move = false; + })) + .on_mouse_up( + MouseButton::Left, + cx.listener(move |this, _ev, _window, _cx| { + this.should_move = false; + }), + ) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _ev, _window, _cx| { + this.should_move = true; + }), + ) } else { title_bar } diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs index 306d689a7c57f618cdc318c73d4f6bc962dc5a0f..0e7af80f80e8dcbea03a3b3375f1e4dfd7ca2f37 100644 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ b/crates/title_bar/src/platforms/platform_linux.rs @@ -1,6 +1,4 @@ -use crate::title_bar_settings::TitleBarSettings; use gpui::{Action, Hsla, MouseButton, prelude::*, svg}; -use settings::{Settings, WindowControlsPosition}; use ui::prelude::*; #[derive(IntoElement)] @@ -16,62 +14,33 @@ impl LinuxWindowControls { } } -impl LinuxWindowControls { - /// Builds the window controls based on the position setting. - fn build_controls( - position: WindowControlsPosition, - window: &Window, - close_action: Box, - cx: &mut App, - ) -> Vec { - let maximize_type = if window.is_maximized() { - WindowControlType::Restore - } else { - WindowControlType::Maximize - }; - - match position { - WindowControlsPosition::Left => { - // Left side: Close, Minimize, Maximize (left to right) - vec![ - WindowControl::new_close("close", WindowControlType::Close, close_action, cx), - WindowControl::new("minimize", WindowControlType::Minimize, cx), - WindowControl::new("maximize-or-restore", maximize_type, cx), - ] - } - WindowControlsPosition::Right => { - // Right side: Minimize, Maximize, Close (left to right) - vec![ - WindowControl::new("minimize", WindowControlType::Minimize, cx), - WindowControl::new("maximize-or-restore", maximize_type, cx), - WindowControl::new_close("close", WindowControlType::Close, close_action, cx), - ] - } - } - } -} - impl RenderOnce for LinuxWindowControls { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let title_bar_settings = TitleBarSettings::get(None, cx); - let controls = Self::build_controls( - title_bar_settings.window_controls_position, - window, - self.close_window_action, - cx, - ); - - let mut element = h_flex() + h_flex() .id("generic-window-controls") .px_3() .gap_3() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()); - - for control in controls { - element = element.child(control); - } - - element + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .child(WindowControl::new( + "minimize", + WindowControlType::Minimize, + cx, + )) + .child(WindowControl::new( + "maximize-or-restore", + if window.is_maximized() { + WindowControlType::Restore + } else { + WindowControlType::Maximize + }, + cx, + )) + .child(WindowControl::new_close( + "close", + WindowControlType::Close, + self.close_window_action, + cx, + )) } } @@ -111,7 +80,7 @@ impl WindowControlStyle { let colors = cx.theme().colors(); Self { - background: colors.title_bar_background, + background: colors.ghost_element_background, background_hover: colors.ghost_element_hover, icon: colors.icon, icon_hover: colors.icon_muted, @@ -216,7 +185,6 @@ impl RenderOnce for WindowControl { .rounded_2xl() .w_5() .h_5() - .bg(self.style.background) .hover(|this| this.bg(self.style.background_hover)) .active(|this| this.bg(self.style.background_hover)) .child(icon) diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs index a02f80fbd6e5a6f8a54d0599ccb8e04369a6b76f..bc9b1acbaa06cf60396e61ff68470c8a544e3f5d 100644 --- a/crates/title_bar/src/title_bar_settings.rs +++ b/crates/title_bar/src/title_bar_settings.rs @@ -1,4 +1,4 @@ -use settings::{Settings, SettingsContent, WindowControlsPosition}; +use settings::{Settings, SettingsContent}; #[derive(Copy, Clone, Debug)] pub struct TitleBarSettings { @@ -9,7 +9,6 @@ pub struct TitleBarSettings { pub show_project_items: bool, pub show_sign_in: bool, pub show_menus: bool, - pub window_controls_position: WindowControlsPosition, } impl Settings for TitleBarSettings { @@ -23,7 +22,6 @@ impl Settings for TitleBarSettings { show_project_items: content.show_project_items.unwrap(), show_sign_in: content.show_sign_in.unwrap(), show_menus: content.show_menus.unwrap(), - window_controls_position: content.window_controls_position.unwrap_or_default(), } } } From 2903a06e5ca0c56e923674a80577e1dcc00fe357 Mon Sep 17 00:00:00 2001 From: John Tur Date: Tue, 21 Oct 2025 21:55:02 -0400 Subject: [PATCH 128/202] Fix nightly upload on Windows (#40843) Release Notes: - N/A --- .github/workflows/release_nightly.yml | 6 ++-- script/upload-nightly.ps1 | 46 ++++++--------------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index a6cfd9c43b55c2cd57bb4b87485ddc1b82f0b82a..a4f86de39e3eed20dac969a9542b4d2b9edef822 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -281,11 +281,11 @@ jobs: - name: Build Zed installer working-directory: ${{ env.ZED_WORKSPACE }} - run: script/bundle-windows.ps1 + run: script/bundle-windows.ps1 -Architecture x86_64 - name: Upload Zed Nightly working-directory: ${{ env.ZED_WORKSPACE }} - run: script/upload-nightly.ps1 windows + run: script/upload-nightly.ps1 -Architecture x86_64 bundle-windows-arm64: timeout-minutes: 60 @@ -328,7 +328,7 @@ jobs: - name: Upload Zed Nightly working-directory: ${{ env.ZED_WORKSPACE }} - run: script/upload-nightly.ps1 windows + run: script/upload-nightly.ps1 -Architecture aarch64 update-nightly-tag: name: Update nightly tag diff --git a/script/upload-nightly.ps1 b/script/upload-nightly.ps1 index 94f00ae9084a991669201281bdcd6110521fb50a..deec4baecc9274381b4d3f99e611190ab0865636 100644 --- a/script/upload-nightly.ps1 +++ b/script/upload-nightly.ps1 @@ -1,32 +1,13 @@ +[CmdletBinding()] +Param( + [Parameter()][string]$Architecture +) + # Based on the template in: https://docs.digitalocean.com/reference/api/spaces-api/ $ErrorActionPreference = "Stop" . "$PSScriptRoot\lib\blob-store.ps1" . "$PSScriptRoot\lib\workspace.ps1" -$allowedTargets = @("windows") - -function Test-AllowedTarget { - param ( - [string]$Target - ) - - return $allowedTargets -contains $Target -} - -# Process arguments -if ($args.Count -gt 0) { - $target = $args[0] - if (Test-AllowedTarget $target) { - # Valid target - } else { - Write-Error "Error: Target '$target' is not allowed.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]" - exit 1 - } -} else { - Write-Error "Error: Target is not specified.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]" - exit 1 -} - ParseZedWorkspace Write-Host "Uploading nightly for target: $target" @@ -44,17 +25,8 @@ $sha | Out-File -FilePath "target/latest-sha" -NoNewline # Remove-Item -Path $file.FullName # } -switch ($target) { - "windows" { - UploadToBlobStore -BucketName $bucketName -FileToUpload $env:SETUP_PATH -BlobStoreKey "nightly/Zed-x86_64.exe" - UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows" - - Remove-Item -Path $env:SETUP_PATH -ErrorAction SilentlyContinue - Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue - } +UploadToBlobStore -BucketName $bucketName -FileToUpload "target/Zed-$Architecture.exe" -BlobStoreKey "nightly/Zed-$Architecture.exe" +UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows" - default { - Write-Error "Error: Unknown target '$target'" - exit 1 - } -} +Remove-Item -Path "target/Zed-$Architecture.exe" -ErrorAction SilentlyContinue +Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue From e468edd3893c11597bda6d7ebba8f1204e8c074f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 21 Oct 2025 18:55:54 -0700 Subject: [PATCH 129/202] Fix extraction of font runs from text runs (#40840) Fixes a bug in https://github.com/zed-industries/zed/pull/39928 The bug caused all completions to appear in bold-face Release Notes: - Fixed a bug where bold-face font was applied to the wrong characters in items in the autocomplete menu Co-authored-by: Mikayla Maki --- crates/gpui/examples/text_layout.rs | 10 ++++++++-- crates/gpui/src/text_system.rs | 6 ++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/gpui/examples/text_layout.rs b/crates/gpui/examples/text_layout.rs index c4cbcd4e5edc142dde58a1dd5d9b61a1daee0c3a..8929955ba824c36c90951ece2cf9ba710259ddac 100644 --- a/crates/gpui/examples/text_layout.rs +++ b/crates/gpui/examples/text_layout.rs @@ -1,6 +1,6 @@ use gpui::{ - App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, - size, + App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds, + WindowOptions, div, prelude::*, px, size, }; struct HelloWorld {} @@ -71,6 +71,12 @@ impl Render for HelloWorld { .child("100%"), ), ) + .child(div().flex().gap_2().justify_between().child( + StyledText::new("ABCD").with_highlights([ + (0..1, FontWeight::EXTRA_BOLD.into()), + (2..3, FontStyle::Italic.into()), + ]), + )) } } diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index cd665aae1b475dda8501ca459492fbf68e71c17f..85a3133ca6c9e559c1cae76f595426d702bfd3f3 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -426,7 +426,6 @@ impl WindowTextSystem { font_runs.clear(); let line_end = line_start + line_text.len(); - let mut last_font: Option = None; let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); let mut run_start = line_start; while run_start < line_end { @@ -455,14 +454,13 @@ impl WindowTextSystem { true }; + let font_id = self.resolve_font(&run.font); if let Some(font_run) = font_runs.last_mut() - && Some(font_run.font_id) == last_font + && font_id == font_run.font_id && !decoration_changed { font_run.len += run_len_within_line; } else { - let font_id = self.resolve_font(&run.font); - last_font = Some(font_id); font_runs.push(FontRun { len: run_len_within_line, font_id, From 221637ea82c26146d37bfc417c90da2e4596cbc1 Mon Sep 17 00:00:00 2001 From: John Tur Date: Tue, 21 Oct 2025 22:21:43 -0400 Subject: [PATCH 130/202] Fix code signing for Windows installer (#40847) Release Notes: - N/A Co-authored-by: Mikayla Maki Co-authored-by: Conrad Irwin --- crates/zed/resources/windows/zed.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss index c25888becb10779b2328b1ceb4cee42601210bfa..6971aa0e3822cff8564e33764a43cd1114161ec4 100644 --- a/crates/zed/resources/windows/zed.iss +++ b/crates/zed/resources/windows/zed.iss @@ -31,7 +31,7 @@ WizardStyle=modern CloseApplications=force -#ifdef DefaultSign +#ifdef Defaultsign SignTool=Defaultsign #endif From 9c71a7f43cd47bd1ec9a7260416b51346dfdceee Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 21 Oct 2025 20:03:02 -0700 Subject: [PATCH 131/202] Revert arm64 runners (#40852) Release Notes: - N/A --- .github/workflows/ci.yml | 61 +---------------- .github/workflows/release_nightly.yml | 50 +------------- crates/zed/resources/windows/zed.iss | 3 - script/bundle-windows.ps1 | 99 +++++++-------------------- script/upload-nightly.ps1 | 46 ++++++++++--- 5 files changed, 68 insertions(+), 191 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e594cdcfff4e5ba2383cee4d2b4551ea86d9e8d8..2ebbcaba49823787aafe40e5f3dd80eb67478b42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -789,7 +789,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 - name: Create a Windows installer for x86_64 + name: Create a Windows installer runs-on: [self-32vcpu-windows-2022] if: | ( startsWith(github.ref, 'refs/tags/v') @@ -844,70 +844,13 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - bundle-windows-aarch64: - timeout-minutes: 120 - name: Create a Windows installer for aarch64 - runs-on: [self-32vcpu-windows-2022] - if: | - ( startsWith(github.ref, 'refs/tags/v') - || contains(github.event.pull_request.labels.*.name, 'run-bundling') ) - needs: [windows_tests] - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Setup Sentry CLI - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 - with: - token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} - - - name: Determine version and release channel - working-directory: ${{ env.ZED_WORKSPACE }} - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - run: | - # This exports RELEASE_CHANNEL into env (GITHUB_ENV) - script/determine-release-channel.ps1 - - - name: Build Zed installer - working-directory: ${{ env.ZED_WORKSPACE }} - run: script/bundle-windows.ps1 -Architecture aarch64 - - - name: Upload installer (aarch64) to Workflow - zed (run-bundling) - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - if: contains(github.event.pull_request.labels.*.name, 'run-bundling') - with: - name: Zed_${{ github.event.pull_request.head.sha || github.sha }}-aarch64.exe - path: ${{ env.SETUP_PATH }} - - - name: Upload Artifacts to release - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 - if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) }} - with: - draft: true - prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} - files: ${{ env.SETUP_PATH }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - auto-release-preview: name: Auto release preview if: | false && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') - needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, bundle-windows-aarch64] + needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64] runs-on: - self-mini-macos steps: diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index a4f86de39e3eed20dac969a9542b4d2b9edef822..2026ee7b730698cd7e40eebcd141f5b8a6ee9d04 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -246,7 +246,7 @@ jobs: bundle-windows-x64: timeout-minutes: 60 - name: Create a Windows installer for x86_64 + name: Create a Windows installer if: github.repository_owner == 'zed-industries' runs-on: [self-32vcpu-windows-2022] needs: windows-tests @@ -281,54 +281,11 @@ jobs: - name: Build Zed installer working-directory: ${{ env.ZED_WORKSPACE }} - run: script/bundle-windows.ps1 -Architecture x86_64 + run: script/bundle-windows.ps1 - name: Upload Zed Nightly working-directory: ${{ env.ZED_WORKSPACE }} - run: script/upload-nightly.ps1 -Architecture x86_64 - - bundle-windows-arm64: - timeout-minutes: 60 - name: Create a Windows installer for aarch64 - if: github.repository_owner == 'zed-industries' - runs-on: [self-32vcpu-windows-2022] - needs: windows-tests - env: - AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} - AZURE_CLIENT_ID: ${{ secrets.AZURE_SIGNING_CLIENT_ID }} - AZURE_CLIENT_SECRET: ${{ secrets.AZURE_SIGNING_CLIENT_SECRET }} - ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} - CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} - ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - FILE_DIGEST: SHA256 - TIMESTAMP_DIGEST: SHA256 - TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" - steps: - - name: Checkout repo - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - clean: false - - - name: Set release channel to nightly - working-directory: ${{ env.ZED_WORKSPACE }} - run: | - $ErrorActionPreference = "Stop" - $version = git rev-parse --short HEAD - Write-Host "Publishing version: $version on release channel nightly" - "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - - - name: Setup Sentry CLI - uses: matbour/setup-sentry-cli@3e938c54b3018bdd019973689ef984e033b0454b #v2 - with: - token: ${{ SECRETS.SENTRY_AUTH_TOKEN }} - - - name: Build Zed installer - working-directory: ${{ env.ZED_WORKSPACE }} - run: script/bundle-windows.ps1 -Architecture aarch64 - - - name: Upload Zed Nightly - working-directory: ${{ env.ZED_WORKSPACE }} - run: script/upload-nightly.ps1 -Architecture aarch64 + run: script/upload-nightly.ps1 windows update-nightly-tag: name: Update nightly tag @@ -339,7 +296,6 @@ jobs: - bundle-linux-x86 - bundle-linux-arm - bundle-windows-x64 - - bundle-windows-arm64 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss index 6971aa0e3822cff8564e33764a43cd1114161ec4..b726bb1c2117b1d53f560aaff83acb370c2f2cd4 100644 --- a/crates/zed/resources/windows/zed.iss +++ b/crates/zed/resources/windows/zed.iss @@ -31,10 +31,7 @@ WizardStyle=modern CloseApplications=force -#ifdef Defaultsign SignTool=Defaultsign -#endif - DefaultDirName={autopf}\{#AppName} PrivilegesRequired=lowest diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 889d3e1390828f09b070f702db2d5f5ea8e9d63c..f6f44307ff7c2be960b40cd837739d2657095ab2 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -2,7 +2,6 @@ Param( [Parameter()][Alias('i')][switch]$Install, [Parameter()][Alias('h')][switch]$Help, - [Parameter()][Alias('a')][string]$Architecture, [Parameter()][string]$Name ) @@ -15,44 +14,12 @@ $PSNativeCommandUseErrorActionPreference = $true $buildSuccess = $false -$OSArchitecture = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) { - "X64" { "x86_64" } - "Arm64" { "aarch64" } - default { throw "Unsupported architecture" } -} - -$Architecture = if ($Architecture) { - $Architecture -} else { - $OSArchitecture -} - -$CargoOutDir = "./target/$Architecture-pc-windows-msvc/release" - -function Get-VSArch { - param( - [string]$Arch - ) - - switch ($Arch) { - "x86_64" { "amd64" } - "aarch64" { "arm64" } - } -} - -Push-Location -& "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch (Get-VSArch -Arch $Architecture) -HostArch (Get-VSArch -Arch $OSArchitecture) -Pop-Location - -$target = "$Architecture-pc-windows-msvc" - if ($Help) { Write-Output "Usage: test.ps1 [-Install] [-Help]" Write-Output "Build the installer for Windows.\n" Write-Output "Options:" - Write-Output " -Architecture, -a Which architecture to build (x86_64 or aarch64)" - Write-Output " -Install, -i Run the installer after building." - Write-Output " -Help, -h Show this help message." + Write-Output " -Install, -i Run the installer after building." + Write-Output " -Help, -h Show this help message." exit 0 } @@ -63,10 +30,6 @@ $env:RELEASE_CHANNEL = $channel Pop-Location function CheckEnvironmentVariables { - if(-not $env:CI) { - return - } - $requiredVars = @( 'ZED_WORKSPACE', 'RELEASE_VERSION', 'ZED_RELEASE_CHANNEL', 'AZURE_TENANT_ID', 'AZURE_CLIENT_ID', 'AZURE_CLIENT_SECRET', @@ -92,8 +55,6 @@ function PrepareForBundle { New-Item -Path "$innoDir\appx" -ItemType Directory -Force New-Item -Path "$innoDir\bin" -ItemType Directory -Force New-Item -Path "$innoDir\tools" -ItemType Directory -Force - - rustup target add $target } function GenerateLicenses { @@ -106,34 +67,34 @@ function GenerateLicenses { function BuildZedAndItsFriends { Write-Output "Building Zed and its friends, for channel: $channel" # Build zed.exe, cli.exe and auto_update_helper.exe - cargo build --release --package zed --package cli --package auto_update_helper --target $target - Copy-Item -Path ".\$CargoOutDir\zed.exe" -Destination "$innoDir\Zed.exe" -Force - Copy-Item -Path ".\$CargoOutDir\cli.exe" -Destination "$innoDir\cli.exe" -Force - Copy-Item -Path ".\$CargoOutDir\auto_update_helper.exe" -Destination "$innoDir\auto_update_helper.exe" -Force + cargo build --release --package zed --package cli --package auto_update_helper + Copy-Item -Path ".\target\release\zed.exe" -Destination "$innoDir\Zed.exe" -Force + Copy-Item -Path ".\target\release\cli.exe" -Destination "$innoDir\cli.exe" -Force + Copy-Item -Path ".\target\release\auto_update_helper.exe" -Destination "$innoDir\auto_update_helper.exe" -Force # Build explorer_command_injector.dll switch ($channel) { "stable" { - cargo build --release --features stable --no-default-features --package explorer_command_injector --target $target + cargo build --release --features stable --no-default-features --package explorer_command_injector } "preview" { - cargo build --release --features preview --no-default-features --package explorer_command_injector --target $target + cargo build --release --features preview --no-default-features --package explorer_command_injector } default { - cargo build --release --package explorer_command_injector --target $target + cargo build --release --package explorer_command_injector } } - Copy-Item -Path ".\$CargoOutDir\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force + Copy-Item -Path ".\target\release\explorer_command_injector.dll" -Destination "$innoDir\zed_explorer_command_injector.dll" -Force } function ZipZedAndItsFriendsDebug { $items = @( - ".\$CargoOutDir\zed.pdb", - ".\$CargoOutDir\cli.pdb", - ".\$CargoOutDir\auto_update_helper.pdb", - ".\$CargoOutDir\explorer_command_injector.pdb" + ".\target\release\zed.pdb", + ".\target\release\cli.pdb", + ".\target\release\auto_update_helper.pdb", + ".\target\release\explorer_command_injector.pdb" ) - Compress-Archive -Path $items -DestinationPath ".\$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force + Compress-Archive -Path $items -DestinationPath ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" -Force } @@ -148,7 +109,7 @@ function UploadToSentry { return } Write-Output "Uploading zed debug symbols to sentry..." - sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev $CargoOutDir + sentry-cli debug-files upload --include-sources --wait -p zed -o zed-dev .\target\release\ } function MakeAppx { @@ -171,10 +132,6 @@ function MakeAppx { } function SignZedAndItsFriends { - if (-not $env:CI) { - return - } - $files = "$innoDir\Zed.exe,$innoDir\cli.exe,$innoDir\auto_update_helper.exe,$innoDir\zed_explorer_command_injector.dll,$innoDir\zed_explorer_command_injector.appx" & "$innoDir\sign.ps1" $files } @@ -215,7 +172,7 @@ function BuildInstaller { $appIconName = "app-icon" $appName = "Zed" $appDisplayName = "Zed" - $appSetupName = "Zed-$Architecture" + $appSetupName = "Zed-x86_64" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Stable-Instance-Mutex" $appExeName = "Zed" @@ -229,7 +186,7 @@ function BuildInstaller { $appIconName = "app-icon-preview" $appName = "Zed Preview" $appDisplayName = "Zed Preview" - $appSetupName = "Zed-$Architecture" + $appSetupName = "Zed-x86_64" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Preview-Instance-Mutex" $appExeName = "Zed" @@ -243,7 +200,7 @@ function BuildInstaller { $appIconName = "app-icon-nightly" $appName = "Zed Nightly" $appDisplayName = "Zed Nightly" - $appSetupName = "Zed-$Architecture" + $appSetupName = "Zed-x86_64" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Nightly-Instance-Mutex" $appExeName = "Zed" @@ -257,7 +214,7 @@ function BuildInstaller { $appIconName = "app-icon-dev" $appName = "Zed Dev" $appDisplayName = "Zed Dev" - $appSetupName = "Zed-$Architecture" + $appSetupName = "Zed-x86_64" # The mutex name here should match the mutex name in crates\zed\src\zed\windows_only_instance.rs $appMutex = "Zed-Dev-Instance-Mutex" $appExeName = "Zed" @@ -295,16 +252,14 @@ function BuildInstaller { "AppxFullName" = $appAppxFullName } + $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f" + $defs = @() foreach ($key in $definitions.Keys) { $defs += "/d$key=`"$($definitions[$key])`"" } - $innoArgs = @($issFilePath) + $defs - if($env:CI) { - $signTool = "powershell.exe -ExecutionPolicy Bypass -File $innoDir\sign.ps1 `$f" - $innoArgs += "/sDefaultsign=`"$signTool`"" - } + $innoArgs = @($issFilePath) + $defs + "/sDefaultsign=`"$signTool`"" # Execute Inno Setup Write-Host "🚀 Running Inno Setup: $innoSetupPath $innoArgs" @@ -323,7 +278,7 @@ function BuildInstaller { ParseZedWorkspace $innoDir = "$env:ZED_WORKSPACE\inno" -$debugArchive = "$CargoOutDir\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" +$debugArchive = ".\target\release\zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" $debugStoreKey = "$env:ZED_RELEASE_CHANNEL/zed-$env:RELEASE_VERSION-$env:ZED_RELEASE_CHANNEL.dbg.zip" CheckEnvironmentVariables @@ -338,10 +293,8 @@ DownloadConpty CollectFiles BuildInstaller -if($env:CI) { - UploadToBlobStorePublic -BucketName "zed-debug-symbols" -FileToUpload $debugArchive -BlobStoreKey $debugStoreKey - UploadToSentry -} +UploadToBlobStorePublic -BucketName "zed-debug-symbols" -FileToUpload $debugArchive -BlobStoreKey $debugStoreKey +UploadToSentry if ($buildSuccess) { Write-Output "Build successful" diff --git a/script/upload-nightly.ps1 b/script/upload-nightly.ps1 index deec4baecc9274381b4d3f99e611190ab0865636..94f00ae9084a991669201281bdcd6110521fb50a 100644 --- a/script/upload-nightly.ps1 +++ b/script/upload-nightly.ps1 @@ -1,13 +1,32 @@ -[CmdletBinding()] -Param( - [Parameter()][string]$Architecture -) - # Based on the template in: https://docs.digitalocean.com/reference/api/spaces-api/ $ErrorActionPreference = "Stop" . "$PSScriptRoot\lib\blob-store.ps1" . "$PSScriptRoot\lib\workspace.ps1" +$allowedTargets = @("windows") + +function Test-AllowedTarget { + param ( + [string]$Target + ) + + return $allowedTargets -contains $Target +} + +# Process arguments +if ($args.Count -gt 0) { + $target = $args[0] + if (Test-AllowedTarget $target) { + # Valid target + } else { + Write-Error "Error: Target '$target' is not allowed.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]" + exit 1 + } +} else { + Write-Error "Error: Target is not specified.`nUsage: $($MyInvocation.MyCommand.Name) [$($allowedTargets -join ', ')]" + exit 1 +} + ParseZedWorkspace Write-Host "Uploading nightly for target: $target" @@ -25,8 +44,17 @@ $sha | Out-File -FilePath "target/latest-sha" -NoNewline # Remove-Item -Path $file.FullName # } -UploadToBlobStore -BucketName $bucketName -FileToUpload "target/Zed-$Architecture.exe" -BlobStoreKey "nightly/Zed-$Architecture.exe" -UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows" +switch ($target) { + "windows" { + UploadToBlobStore -BucketName $bucketName -FileToUpload $env:SETUP_PATH -BlobStoreKey "nightly/Zed-x86_64.exe" + UploadToBlobStore -BucketName $bucketName -FileToUpload "target/latest-sha" -BlobStoreKey "nightly/latest-sha-windows" + + Remove-Item -Path $env:SETUP_PATH -ErrorAction SilentlyContinue + Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue + } -Remove-Item -Path "target/Zed-$Architecture.exe" -ErrorAction SilentlyContinue -Remove-Item -Path "target/latest-sha" -ErrorAction SilentlyContinue + default { + Write-Error "Error: Unknown target '$target'" + exit 1 + } +} From d7c855550ce8f47cb1232326243eae48df6c907e Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 21 Oct 2025 20:07:02 -0700 Subject: [PATCH 132/202] Update async-tar dependency for GPUI (#40850) Release Notes: - N/A --- Cargo.lock | 42 +++++++++++++++++++++--------------------- Cargo.toml | 3 ++- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7d0fb48c82d8f50a193aafebb1ce1fae60440d1..9fe7793aa0c8d9c0bd933c7796662e354a74b6ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1182,19 +1182,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async-tar" -version = "0.5.0" -source = "git+https://github.com/zed-industries/async-tar?rev=8af312477196311c9ea4097f2a22022f6d609bf6#8af312477196311c9ea4097f2a22022f6d609bf6" -dependencies = [ - "async-std", - "filetime", - "libc", - "pin-project", - "redox_syscall 0.2.16", - "xattr", -] - [[package]] name = "async-task" version = "4.7.1" @@ -1292,7 +1279,6 @@ name = "audio" version = "0.1.0" dependencies = [ "anyhow", - "async-tar", "collections", "crossbeam", "denoise", @@ -1306,6 +1292,7 @@ dependencies = [ "smol", "thiserror 2.0.17", "util", + "zed-async-tar", ] [[package]] @@ -4471,7 +4458,6 @@ dependencies = [ "anyhow", "async-compression", "async-pipe", - "async-tar", "async-trait", "client", "collections", @@ -4498,6 +4484,7 @@ dependencies = [ "tree-sitter", "tree-sitter-go", "util", + "zed-async-tar", "zlog", ] @@ -5762,7 +5749,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", - "async-tar", "async-trait", "collections", "dap", @@ -5785,6 +5771,7 @@ dependencies = [ "util", "wasm-encoder 0.221.3", "wasmparser 0.221.3", + "zed-async-tar", ] [[package]] @@ -5816,7 +5803,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", - "async-tar", "async-trait", "client", "collections", @@ -5857,6 +5843,7 @@ dependencies = [ "wasmparser 0.221.3", "wasmtime", "wasmtime-wasi", + "zed-async-tar", "zlog", ] @@ -6317,7 +6304,6 @@ version = "0.1.0" dependencies = [ "anyhow", "ashpd 0.11.0", - "async-tar", "async-trait", "cocoa 0.26.0", "collections", @@ -6342,6 +6328,7 @@ dependencies = [ "time", "util", "windows 0.61.3", + "zed-async-tar", ] [[package]] @@ -7680,7 +7667,6 @@ dependencies = [ "anyhow", "async-compression", "async-fs", - "async-tar", "bytes 1.10.1", "derive_more 0.99.20", "futures 0.3.31", @@ -7694,6 +7680,7 @@ dependencies = [ "tempfile", "url", "util", + "zed-async-tar", "zed-reqwest", ] @@ -8889,7 +8876,6 @@ dependencies = [ "anyhow", "async-compression", "async-fs", - "async-tar", "async-trait", "chrono", "collections", @@ -8946,6 +8932,7 @@ dependencies = [ "url", "util", "workspace", + "zed-async-tar", ] [[package]] @@ -10189,7 +10176,6 @@ dependencies = [ "anyhow", "async-compression", "async-std", - "async-tar", "async-trait", "futures 0.3.31", "http_client", @@ -10202,6 +10188,7 @@ dependencies = [ "util", "watch", "which 6.0.3", + "zed-async-tar", ] [[package]] @@ -21081,6 +21068,19 @@ dependencies = [ "zlog_settings", ] +[[package]] +name = "zed-async-tar" +version = "0.5.0-zed" +source = "git+https://github.com/zed-industries/async-tar?rev=a307f6bf3e4219c3a457bea0cab198b6d7c36e25#a307f6bf3e4219c3a457bea0cab198b6d7c36e25" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall 0.2.16", + "xattr", +] + [[package]] name = "zed-font-kit" version = "0.14.1-zed" diff --git a/Cargo.toml b/Cargo.toml index f5b9a809de680b89c1d989dc8541c4aa308f24bd..81e30b308af112457a2ce0ca3b479fa6f57707ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -452,7 +452,8 @@ async-fs = "2.1" async-lock = "2.1" async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" -async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "8af312477196311c9ea4097f2a22022f6d609bf6" } +# WARNING: If you change this, you must also publish a new version of zed-async-tar to crates.io +async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "a307f6bf3e4219c3a457bea0cab198b6d7c36e25", package = "zed-async-tar", version = "0.5.0-zed" } async-task = "4.7" async-trait = "0.1" async-tungstenite = "0.31.0" From d096132888a769a20afbc14058b39ace2336c564 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:22:20 -0300 Subject: [PATCH 133/202] docs: Add git stash to git.md (#40834) Closes #ISSUE Release Notes: - Added git stash documentation --- docs/src/git.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/src/git.md b/docs/src/git.md index c18ce1d2bbf958f0f5988c9179fd7ff4276615cc..d56de998c9d1438a1bd160d7e577b146a4ea4da3 100644 --- a/docs/src/git.md +++ b/docs/src/git.md @@ -17,6 +17,7 @@ Here's an overview of all currently supported features: - Git status in the Project Panel - Branch creating and switching - Git blame viewing +- Git stash pop, apply, drop and view ## Git Panel @@ -74,6 +75,41 @@ Zed offers two commit textareas: As soon as you commit in Zed, in the Git Panel, you'll see a bar right under the commit textarea, which will show the recently submitted commit. In there, you can use the "Uncommit" button, which performs the `git reset HEADˆ--soft` command. +## Stashing + +Git stash allows you to temporarily save your uncommitted changes and revert your working directory to a clean state. This is particularly useful when you need to quickly switch branches or pull updates without committing incomplete work. + +### Creating Stashes + +To stash all your current changes, use the {#action git::StashAll} action. This will save both staged and unstaged changes to a new stash entry and clean your working directory. + +### Managing Stashes + +Zed provides a comprehensive stash picker accessible via {#action git::ViewStash}. From the stash picker, you can: + +- **View stash list**: Browse all your saved stashes with their descriptions and timestamps +- **Open diffs**: See exactly what changes are stored in each stash +- **Apply stashes**: Apply stash changes to your working directory while keeping the stash entry +- **Pop stashes**: Apply stash changes and remove the stash entry from the list +- **Drop stashes**: Delete unwanted stash entries without applying them + +### Quick Stash Operations + +For faster workflows, Zed provides direct actions to work with the most recent stash: + +- **Apply latest stash**: Use {#action git::StashApply} to apply the most recent stash without removing it +- **Pop latest stash**: Use {#action git::StashPop} to apply and remove the most recent stash + +### Stash Diff View + +When viewing a specific stash in the diff view, you have additional options available through the interface: + +- Apply the current stash to your working directory +- Pop the current stash (apply and remove) +- Remove the stash without applying changes + +To open the stash diff view, select a stash from the stash picker and use the {#action stash_picker::ShowStashItem} ({#kb stash_picker::ShowStashItem}) keybinding. + ## AI Support in Git Zed currently supports LLM-powered commit message generation. @@ -151,6 +187,10 @@ When viewing files with changes, Zed displays diff hunks that can be expanded or | {#action git::Switch} | {#kb git::Switch} | | {#action git::CheckoutBranch} | {#kb git::CheckoutBranch} | | {#action git::Blame} | {#kb git::Blame} | +| {#action git::StashAll} | {#kb git::StashAll} | +| {#action git::StashPop} | {#kb git::StashPop} | +| {#action git::StashApply} | {#kb git::StashApply} | +| {#action git::ViewStash} | {#kb git::ViewStash} | | {#action editor::ToggleGitBlameInline} | {#kb editor::ToggleGitBlameInline} | | {#action editor::ExpandAllDiffHunks} | {#kb editor::ExpandAllDiffHunks} | | {#action editor::ToggleSelectedDiffHunks} | {#kb editor::ToggleSelectedDiffHunks} | From 08d95ad9d31f616a43dacda8416568d658dca6ae Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 21 Oct 2025 20:43:32 -0700 Subject: [PATCH 134/202] chore: Bump gpui to 0.2.2 (#40856) Release Notes: - N/A --- Cargo.lock | 2 +- crates/gpui/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9fe7793aa0c8d9c0bd933c7796662e354a74b6ad..bb5950741bd4dd857144541d0573d760e5564312 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7144,7 +7144,7 @@ dependencies = [ [[package]] name = "gpui" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "as-raw-xcb-connection", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 3058c0a6406b77ec2861ab1b099eaf3520b507b3..af23a336f6230a16040cd98f1f3377c817af05fb 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gpui" -version = "0.2.1" +version = "0.2.2" edition.workspace = true authors = ["Nathan Sobo "] description = "Zed's GPU-accelerated UI framework" From fd9c2e32f33599fd234c810ec27f9bd0b383cec0 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 00:53:44 -0400 Subject: [PATCH 135/202] Organize release docs (#40860)1 Release Notes: - N/A --- docs/src/development/releases.md | 66 +++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/src/development/releases.md b/docs/src/development/releases.md index c4eb0eb35a4850b88c9950080664984b1dd1c255..9e2cdccfdc01c528a75c85cad5d6ac0fe9ed64e2 100644 --- a/docs/src/development/releases.md +++ b/docs/src/development/releases.md @@ -11,7 +11,7 @@ Credentials for various services used in this process can be found in 1Password. Use the `releases` Slack channel to notify the team that releases will be starting. This is mostly a formality on Wednesday's minor update releases, but can be beneficial when doing patch releases, as other devs may have landed fixes they'd like to cherry pick. ---- +### Starting the Builds 1. Checkout `main` and ensure your working copy is clean. @@ -19,44 +19,74 @@ This is mostly a formality on Wednesday's minor update releases, but can be bene 1. Run `git fetch --tags --force` to forcibly ensure your local tags are in sync with the remote. -1. Run `./script/get-stable-channel-release-notes`. - - - Follow the instructions at the end of the script and aggregate the release notes into one structure. +1. Run `./script/get-stable-channel-release-notes` and store output locally. 1. Run `./script/bump-zed-minor-versions`. - Push the tags and branches as instructed. -1. Run `./script/get-preview-channel-changes`. +1. Run `./script/get-preview-channel-changes` and store output locally. - - Take the script's output and build release notes by organizing each release note line into a category. - - Use a prior release for the initial outline. - - Make sure to append the `Credit` line, if present, to the end of the release note line. +> **Note:** Always prioritize the stable release. +> If you've completed aggregating stable release notes, you can move on to working on aggregating preview release notes, but once the stable build has finished, work through the rest of the stable steps to fully publish. +> Preview can be finished up after. -1. Once release drafts are up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste both preview and stable release notes into each and **save**. +### Stable Release - - **Do not publish the drafts!** +1. Aggregate stable release notes. -1. Check the release assets. + - Follow the instructions at the end of the script and aggregate the release notes into one structure. - - Ensure the stable and preview release jobs have finished without error. - - Ensure each draft has the proper number of assets—releases currently have 11 assets each. - - Download the artifacts for each release draft and test that you can run them locally. +1. Once the stable release draft is up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste the stable release notes into it and **save**. + + - **Do not publish the draft!** + +1. Check the stable release assets. -1. Publish the drafts. + - Ensure the stable release job has finished without error. + - Ensure the draft has the proper number of assets—releases currently have 11 assets each. + - Download the artifacts for the stable release draft and test that you can run them locally. - - Publish stable and preview drafts, one at a time. - - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild. - The release will be public once the rebuild has completed. +1. Publish the stable draft on [GitHub Releases](https://github.com/zed-industries/zed/releases). + + - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild. + The release will be public once the rebuild has completed. 1. Post the stable release notes to social media. - Bluesky and X posts will already be built as drafts in [Buffer](https://buffer.com). + - Double-check links. - Publish both, one at a time, ensuring both are posted to each respective platform. 1. Send the stable release notes email. - The email broadcast will already be built as a draft in [Kit](https://kit.com). + - Double-check links. + - Publish the email. + +### Preview Release + +1. Aggregate preview release notes. + + - Take the script's output and build release notes by organizing each release note line into a category. + - Use a prior release for the initial outline. + - Make sure to append the `Credit` line, if present, to the end of the release note line. + +1. Once the preview release draft is up on [GitHub Releases](https://github.com/zed-industries/zed/releases), paste the preview release notes into it and **save**. + + - **Do not publish the draft!** + +1. Check the preview release assets. + + - Ensure the preview release job has finished without error. + - Ensure the draft has the proper number of assets—releases currently have 11 assets each. + - Download the artifacts for the preview release draft and test that you can run them locally. + +1. Publish the preview draft on [GitHub Releases](https://github.com/zed-industries/zed/releases). + - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild. + The release will be public once the rebuild has completed. + +### Prep Content for Next Week's Stable Release 1. Build social media posts based on the popular items in preview. From 3d3e9130a882ff4f6604d7df28e23585e77c89a9 Mon Sep 17 00:00:00 2001 From: Be Date: Wed, 22 Oct 2025 02:37:20 -0500 Subject: [PATCH 136/202] collab: Pin sea-orm-macro crate version together with sea-orm (#40846) Currently running `cargo update` on Zed will break the collab crate because the versions of sea-orm and sea-orm-macros will not match. This results in a bunch of noisy warnings from rust-analyzer. Release Notes: - N/A --- Cargo.lock | 1 + Cargo.toml | 1 + crates/collab/Cargo.toml | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bb5950741bd4dd857144541d0573d760e5564312..27848216c6931b8db41500a1d835d997dafd4cbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3379,6 +3379,7 @@ dependencies = [ "rpc", "scrypt", "sea-orm", + "sea-orm-macros", "semantic_version", "semver", "serde", diff --git a/Cargo.toml b/Cargo.toml index 81e30b308af112457a2ce0ca3b479fa6f57707ba..bd0083d122018c6769ca161c31e0a9b3ef4ec898 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -901,4 +901,5 @@ ignored = [ "serde", "component", "documented", + "sea-orm-macros", ] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index d3ded583d6a8147f40ddc23f65114b666727a8d3..52dbe46107501325e305a7e8e6e7bd9bb483affb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -47,7 +47,9 @@ reqwest = { version = "0.11", features = ["json"] } reqwest_client.workspace = true rpc.workspace = true scrypt = "0.11" +# sea-orm and sea-orm-macros versions must match exactly. sea-orm = { version = "=1.1.10", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] } +sea-orm-macros = "=1.1.10" semantic_version.workspace = true semver.workspace = true serde.workspace = true From 762af0982ce56712531fa5016acbce2fde8cac6a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 03:55:49 -0400 Subject: [PATCH 137/202] Add more troubleshooting information (#40864) Release Notes: - N/A --- docs/src/SUMMARY.md | 1 - docs/src/troubleshooting.md | 57 +++++++++++++++++++++++++++++-- docs/src/workspace-persistence.md | 31 ----------------- 3 files changed, 55 insertions(+), 34 deletions(-) delete mode 100644 docs/src/workspace-persistence.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1620998baf2b575da9d9e990d467f152f988c5fa..0b9fc289c540e43e9bef89b2c561c97a5c1928ef 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,7 +8,6 @@ - [Linux](./linux.md) - [Windows](./windows.md) - [Telemetry](./telemetry.md) -- [Workspace Persistence](./workspace-persistence.md) - [Troubleshooting](./troubleshooting.md) - [Additional Learning Materials](./additional-learning-materials.md) diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md index 50ce1bcc40680cfaa08f56a9c7e44ce5d4df893d..4972da0befb97f21cd5e0465d8b16dfac7b4b834 100644 --- a/docs/src/troubleshooting.md +++ b/docs/src/troubleshooting.md @@ -1,10 +1,16 @@ # Troubleshooting +This guide covers common troubleshooting techniques for Zed. +Sometimes you'll be able to identify and resolve issues on your own using this information. +Other times, troubleshooting means gathering the right information—logs, profiles, or reproduction steps—to help us diagnose and fix the problem. + ## Zed Log Often, a good first place to look when troubleshooting any issue in Zed is the Zed log, which might contain clues about what's going wrong. You can review the most recent 1000 lines of the log by running the {#action zed::OpenLog} command from the command palette (`cmd-shift-p` on macOS or `ctrl-shift-p` on Windows/Linux). -If you want to view the full file, you can find it at the respective location on each operating system: +If you want to view the full file, you can reveal it in your operating system's native file manager via {#action zed::RevealLogInFileManager}. + +You'll find the Zed log in the respective location on each operating system: - macOS: `~/Library/Logs/Zed/Zed.log` - Windows: `C:\Users\YOU\AppData\Local\Zed\logs\Zed.log` @@ -13,4 +19,51 @@ If you want to view the full file, you can find it at the respective location on > Note: In some cases, it might be useful to monitor the log live, such as when [developing a Zed extension](https://zed.dev/docs/extensions/developing-extensions). > Example: `tail -f ~/Library/Logs/Zed/Zed.log` -The log may contain enough context to help you debug the issue yourself, or you may find specific errors that are useful when filing a [GitHub Issue](https://github.com/zed-industries/zed/issues/new/choose) or when talking to Zed staff in our [Discord server](https://zed.dev/community-links#forums-and-discussions). +The log may contain enough context to help you debug the issue yourself, or you may find specific errors that are useful when filing a [GitHub issue](https://github.com/zed-industries/zed/issues/new/choose) or when talking to Zed staff in our [Discord server](https://zed.dev/community-links#forums-and-discussions). + +## Performance Issues (Profiling) + +If you're running into performance issues in Zed—such as hitches, hangs, or general unresponsiveness—having a performance profile attached to your issue will help us zero in on what is getting stuck, so we can fix it. + +### macOS + +Xcode Instruments (which comes bundled with your [Xcode](https://apps.apple.com/us/app/xcode/id497799835) download) is the standard tool for profiling on macOS. + +1. With Zed running, open Instruments +1. Select `Time Profiler` as the profiling template +1. In the `Time Profiler` configuration, set the target to the running Zed process +1. Start recording +1. If the performance issue occurs when performing a specific action in Zed, perform that action now +1. Stop recording +1. Save the trace file +1. Compress the trace file into a zip archive +1. File a [GitHub issue](https://github.com/zed-industries/zed/issues/new/choose) with the trace zip attached + + + + + +## Startup and Workspace Issues + +Zed creates local SQLite databases to persist data relating to its workspace and your projects. These databases store, for instance, the tabs and panes you have open in a project, the scroll position of each open file, the list of all projects you've opened (for the recent projects modal picker), etc. You can find and explore these databases in the following locations: + +- macOS: `~/Library/Application Support/Zed` +- Linux and FreeBSD: `~/.local/share/zed` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`) +- Windows: `%LOCALAPPDATA%\Zed` + +The naming convention of these databases takes on the form of `0-`: + +- Stable: `0-stable` +- Preview: `0-preview` + +While rare, we've seen a few cases where workspace databases became corrupted, which prevented Zed from starting. +If you're experiencing startup issues, you can test whether it's workspace-related by temporarily moving the database from its location, then trying to start Zed again. + +> **Note**: Moving the workspace database will cause Zed to create a fresh one. +> You will lose your recent projects, open tabs, and cursor locations in active files. + +If your issue persists after regenerating the database, please [file an issue](https://github.com/zed-industries/zed/issues/new/choose). + +## Language Server Issues + +If you're experiencing language-server related issues, such as stale diagnostics or issues jumping to definitions, restarting the language server via {#action editor::RestartLanguageServer} can be a quick fix. diff --git a/docs/src/workspace-persistence.md b/docs/src/workspace-persistence.md deleted file mode 100644 index 3122aa15bd68e5985e54dedc4987705784bc8ae5..0000000000000000000000000000000000000000 --- a/docs/src/workspace-persistence.md +++ /dev/null @@ -1,31 +0,0 @@ -# Workspace Persistence - -Zed creates local SQLite databases to persist data relating to its workspace and your projects. These databases store, for instance, the tabs and panes you have open in a project, the scroll position of each open file, the list of all projects you've opened (for the recent projects modal picker), etc. You can find and explore these databases in the following locations: - -- macOS: `~/Library/Application Support/Zed` -- Linux and FreeBSD: `~/.local/share/zed` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`) -- Windows: `%LOCALAPPDATA%\Zed` - -The naming convention of these databases takes on the form of `0-`: - -- Stable: `0-stable` -- Preview: `0-preview` - -**If you encounter workspace persistence issues in Zed, deleting the database and restarting Zed often resolves the problem, as the database may have been corrupted at some point.** If your issue continues after restarting Zed and regenerating a new database, please [file an issue](https://github.com/zed-industries/zed/issues/new?template=10_bug_report.yml). - -## Settings - -You can customize workspace restoration behavior with the following settings: - -```json [settings] -{ - // Workspace restoration behavior. - // All workspaces ("last_session"), last workspace ("last_workspace") or "none" - "restore_on_startup": "last_session", - // Whether to attempt to restore previous file's state when opening it again. - // E.g. for editors, selections, folds and scroll positions are restored - "restore_on_file_reopen": true, - // Whether to automatically close files that have been deleted on disk. - "close_on_file_delete": false -} -``` From 8bad2cbd83bdcd9997123b43373a571d9e3cbfed Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 03:58:03 -0400 Subject: [PATCH 138/202] Add another informational blog post to docs (#40865) Release Notes: - N/A --- docs/src/additional-learning-materials.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/additional-learning-materials.md b/docs/src/additional-learning-materials.md index 66ff935abf1134f4ff703ef83d03eb4772975398..9ff7b3bc5c02e207d0bbf44443d03c0523729833 100644 --- a/docs/src/additional-learning-materials.md +++ b/docs/src/additional-learning-materials.md @@ -1,3 +1,4 @@ # Additional Learning Materials - [Text Manipulation Kung Fu for the Aspiring Black Belt](https://zed.dev/blog/text-manipulation) +- [Hidden Gems: Team Edition Part 1](https://zed.dev/blog/hidden-gems-team-edition-part-1) From 25de2acfdb3e1bb16ab75970fe1523256c93e223 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 04:27:59 -0400 Subject: [PATCH 139/202] Correct workspace db directory paths (#40868) Release Notes: - N/A --- docs/src/troubleshooting.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md index 4972da0befb97f21cd5e0465d8b16dfac7b4b834..9238cbb445362188eca9511f64d3cdf40dd8e095 100644 --- a/docs/src/troubleshooting.md +++ b/docs/src/troubleshooting.md @@ -47,14 +47,16 @@ Xcode Instruments (which comes bundled with your [Xcode](https://apps.apple.com/ Zed creates local SQLite databases to persist data relating to its workspace and your projects. These databases store, for instance, the tabs and panes you have open in a project, the scroll position of each open file, the list of all projects you've opened (for the recent projects modal picker), etc. You can find and explore these databases in the following locations: -- macOS: `~/Library/Application Support/Zed` -- Linux and FreeBSD: `~/.local/share/zed` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`) -- Windows: `%LOCALAPPDATA%\Zed` +- macOS: `~/Library/Application Support/Zed/db` +- Linux and FreeBSD: `~/.local/share/zed/db` (or within `XDG_DATA_HOME` or `FLATPAK_XDG_DATA_HOME`) +- Windows: `%LOCALAPPDATA%\Zed\db` The naming convention of these databases takes on the form of `0-`: - Stable: `0-stable` - Preview: `0-preview` +- Nightly: `0-nightly` +- Dev: `0-dev` While rare, we've seen a few cases where workspace databases became corrupted, which prevented Zed from starting. If you're experiencing startup issues, you can test whether it's workspace-related by temporarily moving the database from its location, then trying to start Zed again. From 8ceb2f2c61366c970bc122a24acfbda23a59c458 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 05:04:05 -0400 Subject: [PATCH 140/202] Add more tweaks to the troubleshooting docs (#40870) Release Notes: - N/A --- docs/src/troubleshooting.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md index 9238cbb445362188eca9511f64d3cdf40dd8e095..4aeeda6e3da5ad0f6494a5064f17cff8be98c330 100644 --- a/docs/src/troubleshooting.md +++ b/docs/src/troubleshooting.md @@ -4,11 +4,20 @@ This guide covers common troubleshooting techniques for Zed. Sometimes you'll be able to identify and resolve issues on your own using this information. Other times, troubleshooting means gathering the right information—logs, profiles, or reproduction steps—to help us diagnose and fix the problem. +> **Note**: To open the command palette, use `cmd-shift-p` on macOS or `ctrl-shift-p` on Windows / Linux. + +## Retrieve Zed and System Information + +When reporting issues or seeking help, it's useful to know your Zed version and system specifications. You can retrieve this information using the following actions from the command palette: + +- {#action zed::About}: Find your Zed version number +- {#action zed::CopySystemSpecsIntoClipboard}: Populate your clipboard with Zed version number, operating system version, and hardware specs + ## Zed Log Often, a good first place to look when troubleshooting any issue in Zed is the Zed log, which might contain clues about what's going wrong. -You can review the most recent 1000 lines of the log by running the {#action zed::OpenLog} command from the command palette (`cmd-shift-p` on macOS or `ctrl-shift-p` on Windows/Linux). -If you want to view the full file, you can reveal it in your operating system's native file manager via {#action zed::RevealLogInFileManager}. +You can review the most recent 1000 lines of the log by running the {#action zed::OpenLog} action from the command palette. +If you want to view the full file, you can reveal it in your operating system's native file manager via {#action zed::RevealLogInFileManager} from the command palette. You'll find the Zed log in the respective location on each operating system: @@ -62,10 +71,10 @@ While rare, we've seen a few cases where workspace databases became corrupted, w If you're experiencing startup issues, you can test whether it's workspace-related by temporarily moving the database from its location, then trying to start Zed again. > **Note**: Moving the workspace database will cause Zed to create a fresh one. -> You will lose your recent projects, open tabs, and cursor locations in active files. +> Your recent projects, open tabs, etc. will be reset to "factory". If your issue persists after regenerating the database, please [file an issue](https://github.com/zed-industries/zed/issues/new/choose). ## Language Server Issues -If you're experiencing language-server related issues, such as stale diagnostics or issues jumping to definitions, restarting the language server via {#action editor::RestartLanguageServer} can be a quick fix. +If you're experiencing language-server related issues, such as stale diagnostics or issues jumping to definitions, restarting the language server via {#action editor::RestartLanguageServer} from the command palette will often resolve the issue. From bc0bace81f6c5f35b8730045dbffcbda64662bc0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 22 Oct 2025 12:07:32 +0300 Subject: [PATCH 141/202] Add basic ico support (#40822) Closes https://github.com/zed-industries/zed/discussions/40763 Screenshot 2025-10-21 at 23 14 47 Also improves error handling on image open failure: Screenshot 2025-10-21 at 23 14 30 Release Notes: - Added basic ico support, improved unsupported image handling --- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/items.rs | 6 +- crates/gpui/src/platform.rs | 5 + .../gpui/src/platform/linux/x11/clipboard.rs | 2 + crates/gpui/src/platform/mac/platform.rs | 6 + crates/image_viewer/src/image_viewer.rs | 16 +++ crates/project/src/image_store.rs | 1 + .../src/invalid_item_view.rs} | 13 +- crates/repl/src/outputs/image.rs | 1 + crates/workspace/src/invalid_item_view.rs | 117 ++++++++++++++++++ crates/workspace/src/item.rs | 4 +- crates/workspace/src/pane.rs | 6 +- crates/workspace/src/workspace.rs | 2 +- 13 files changed, 166 insertions(+), 17 deletions(-) rename crates/{workspace/src/invalid_buffer_view.rs => project/src/invalid_item_view.rs} (94%) create mode 100644 crates/workspace/src/invalid_item_view.rs diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 3e520f4e2901ff378c64f405d082ad460349ec82..7a085d4f4fe0701b1dc9117144c819aeccd9005e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -62,7 +62,7 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, register_project_item, }; @@ -26251,7 +26251,7 @@ async fn test_non_utf_8_opens(cx: &mut TestAppContext) { assert_eq!( handle.to_any().entity_type(), - TypeId::of::() + TypeId::of::() ); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3a037899f4273c2af8b1f54aa704d9850d76243e..6a5552f8c7a689e310c548e11c3a516fb9aedbe3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,7 +42,7 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, item::{FollowableItem, Item, ItemBufferKind, ItemEvent, ProjectItem, SaveOptions}, searchable::{ Direction, FilteredSearchRange, SearchEvent, SearchableItem, SearchableItemHandle, @@ -1392,8 +1392,8 @@ impl ProjectItem for Editor { e: &anyhow::Error, window: &mut Window, cx: &mut App, - ) -> Option { - Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) + ) -> Option { + Some(InvalidItemView::new(abs_path, is_local, e, window, cx)) } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 047a005548ba0c7c34f6641a91333723883203cc..a5f4a368e377d0c43c43c101532feb3f34ae9fd6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1644,6 +1644,8 @@ pub enum ImageFormat { Bmp, /// .tif or .tiff Tiff, + /// .ico + Ico, } impl ImageFormat { @@ -1657,6 +1659,7 @@ impl ImageFormat { ImageFormat::Svg => "image/svg+xml", ImageFormat::Bmp => "image/bmp", ImageFormat::Tiff => "image/tiff", + ImageFormat::Ico => "image/ico", } } @@ -1670,6 +1673,7 @@ impl ImageFormat { "image/svg+xml" => Some(Self::Svg), "image/bmp" => Some(Self::Bmp), "image/tiff" | "image/tif" => Some(Self::Tiff), + "image/ico" => Some(Self::Ico), _ => None, } } @@ -1776,6 +1780,7 @@ impl Image { ImageFormat::Webp => frames_for_image(&self.bytes, image::ImageFormat::WebP)?, ImageFormat::Bmp => frames_for_image(&self.bytes, image::ImageFormat::Bmp)?, ImageFormat::Tiff => frames_for_image(&self.bytes, image::ImageFormat::Tiff)?, + ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?, ImageFormat::Svg => { let pixmap = svg_renderer.render_pixmap(&self.bytes, SvgSize::ScaleFactor(1.0))?; diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/crates/gpui/src/platform/linux/x11/clipboard.rs index 65ad16e82bf103c4ef08e79c692196d3fae58777..3be5008505446e8ca6c6fd93b559fec4779ae85c 100644 --- a/crates/gpui/src/platform/linux/x11/clipboard.rs +++ b/crates/gpui/src/platform/linux/x11/clipboard.rs @@ -86,6 +86,7 @@ x11rb::atom_manager! { SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(), BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(), TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(), + ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(), // This is just some random name for the property on our window, into which // the clipboard owner writes the data we requested. @@ -1003,6 +1004,7 @@ impl Clipboard { ImageFormat::Svg => self.inner.atoms.SVG__MIME, ImageFormat::Bmp => self.inner.atoms.BMP__MIME, ImageFormat::Tiff => self.inner.atoms.TIFF_MIME, + ImageFormat::Ico => self.inner.atoms.ICO__MIME, }; let data = vec![ClipboardData { bytes: image.bytes, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index ee393fbced4924d0cd0556a8a4e6de21a012501a..244350169caffef10ea2740a30e36772506e6145 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1607,6 +1607,7 @@ impl From for UTType { ImageFormat::Gif => Self::gif(), ImageFormat::Bmp => Self::bmp(), ImageFormat::Svg => Self::svg(), + ImageFormat::Ico => Self::ico(), } } } @@ -1645,6 +1646,11 @@ impl UTType { Self(unsafe { ns_string("public.svg-image") }) } + pub fn ico() -> Self { + // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/ico + Self(unsafe { ns_string("com.microsoft.ico") }) + } + pub fn tiff() -> Self { // https://developer.apple.com/documentation/uniformtypeidentifiers/uttype-swift.struct/tiff Self(unsafe { NSPasteboardTypeTIFF }) // This is a rare case where there's a built-in NSPasteboardType diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index fab0166efea5d4199973b66dd63f68b8bb0f2e1c..17259d15f1d81ac2f46e027fcf7889cdbbe9d011 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -1,6 +1,8 @@ mod image_info; mod image_viewer_settings; +use std::path::Path; + use anyhow::Context as _; use editor::{EditorSettings, items::entry_git_aware_label_color}; use file_icons::FileIcons; @@ -18,6 +20,7 @@ use ui::prelude::*; use util::paths::PathExt; use workspace::{ ItemId, ItemSettings, Pane, ToolbarItemLocation, Workspace, WorkspaceId, delete_unloaded_items, + invalid_item_view::InvalidItemView, item::{BreadcrumbText, Item, ProjectItem, SerializableItem, TabContentParams}, }; @@ -389,6 +392,19 @@ impl ProjectItem for ImageView { { Self::new(item, project, window, cx) } + + fn for_broken_project_item( + abs_path: &Path, + is_local: bool, + e: &anyhow::Error, + window: &mut Window, + cx: &mut App, + ) -> Option + where + Self: Sized, + { + Some(InvalidItemView::new(abs_path, is_local, e, window, cx)) + } } pub fn init(cx: &mut App) { diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs index 71394ead2eb27067706023d4870c78c557c3747b..8fcf9c8a6172f866d819e34cbf3b0b4810a8fc8d 100644 --- a/crates/project/src/image_store.rs +++ b/crates/project/src/image_store.rs @@ -687,6 +687,7 @@ fn create_gpui_image(content: Vec) -> anyhow::Result> { image::ImageFormat::Gif => gpui::ImageFormat::Gif, image::ImageFormat::Bmp => gpui::ImageFormat::Bmp, image::ImageFormat::Tiff => gpui::ImageFormat::Tiff, + image::ImageFormat::Ico => gpui::ImageFormat::Ico, format => anyhow::bail!("Image format {format:?} not supported"), }, content, diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/project/src/invalid_item_view.rs similarity index 94% rename from crates/workspace/src/invalid_buffer_view.rs rename to crates/project/src/invalid_item_view.rs index 05f409653b69e76654fa11d70b57d61fd6c0b73b..fdcdd16a69ce73d8471f8387d55cf91576f114af 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/project/src/invalid_item_view.rs @@ -11,7 +11,8 @@ use zed_actions::workspace::OpenWithSystem; use crate::Item; /// A view to display when a certain buffer fails to open. -pub struct InvalidBufferView { +#[derive(Debug)] +pub struct InvalidItemView { /// Which path was attempted to open. pub abs_path: Arc, /// An error message, happened when opening the buffer. @@ -20,7 +21,7 @@ pub struct InvalidBufferView { focus_handle: FocusHandle, } -impl InvalidBufferView { +impl InvalidItemView { pub fn new( abs_path: &Path, is_local: bool, @@ -37,7 +38,7 @@ impl InvalidBufferView { } } -impl Item for InvalidBufferView { +impl Item for InvalidItemView { type Event = (); fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { @@ -66,15 +67,15 @@ impl Item for InvalidBufferView { } } -impl EventEmitter<()> for InvalidBufferView {} +impl EventEmitter<()> for InvalidItemView {} -impl Focusable for InvalidBufferView { +impl Focusable for InvalidItemView { fn focus_handle(&self, _: &App) -> FocusHandle { self.focus_handle.clone() } } -impl Render for InvalidBufferView { +impl Render for InvalidItemView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); v_flex() diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs index 0cabbbbae4715181a76b3730bf492b481d0a6e1b..fefdbec2fa2770baa279a832bd55278bd502380d 100644 --- a/crates/repl/src/outputs/image.rs +++ b/crates/repl/src/outputs/image.rs @@ -51,6 +51,7 @@ impl ImageView { image::ImageFormat::WebP => ImageFormat::Webp, image::ImageFormat::Tiff => ImageFormat::Tiff, image::ImageFormat::Bmp => ImageFormat::Bmp, + image::ImageFormat::Ico => ImageFormat::Ico, format => { anyhow::bail!("unsupported image format {format:?}"); } diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..897190e9aecb97152434c695b823e0aee3148dcb --- /dev/null +++ b/crates/workspace/src/invalid_item_view.rs @@ -0,0 +1,117 @@ +use std::{path::Path, sync::Arc}; + +use gpui::{EventEmitter, FocusHandle, Focusable}; +use ui::{ + App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, + KeyBinding, Label, LabelCommon, LabelSize, ParentElement, Render, SharedString, Styled as _, + Window, h_flex, v_flex, +}; +use zed_actions::workspace::OpenWithSystem; + +use crate::Item; + +/// A view to display when a certain buffer/image/other item fails to open. +pub struct InvalidItemView { + /// Which path was attempted to open. + pub abs_path: Arc, + /// An error message, happened when opening the item. + pub error: SharedString, + is_local: bool, + focus_handle: FocusHandle, +} + +impl InvalidItemView { + pub fn new( + abs_path: &Path, + is_local: bool, + e: &anyhow::Error, + _: &mut Window, + cx: &mut App, + ) -> Self { + Self { + is_local, + abs_path: Arc::from(abs_path), + error: format!("{}", e.root_cause()).into(), + focus_handle: cx.focus_handle(), + } + } +} + +impl Item for InvalidItemView { + type Event = (); + + fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { + // Ensure we always render at least the filename. + detail += 1; + + let path = self.abs_path.as_ref(); + + let mut prefix = path; + while detail > 0 { + if let Some(parent) = prefix.parent() { + prefix = parent; + detail -= 1; + } else { + break; + } + } + + let path = if detail > 0 { + path + } else { + path.strip_prefix(prefix).unwrap_or(path) + }; + + SharedString::new(path.to_string_lossy()) + } +} + +impl EventEmitter<()> for InvalidItemView {} + +impl Focusable for InvalidItemView { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for InvalidItemView { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { + let abs_path = self.abs_path.clone(); + v_flex() + .size_full() + .track_focus(&self.focus_handle(cx)) + .flex_none() + .justify_center() + .overflow_hidden() + .key_context("InvalidItem") + .child( + h_flex().size_full().justify_center().child( + v_flex() + .justify_center() + .gap_2() + .child(h_flex().justify_center().child("Could not open file")) + .child( + h_flex() + .justify_center() + .child(Label::new(self.error.clone()).size(LabelSize::Small)), + ) + .when(self.is_local, |contents| { + contents.child( + h_flex().justify_center().child( + Button::new("open-with-system", "Open in Default App") + .on_click(move |_, _, cx| { + cx.open_with_system(&abs_path); + }) + .style(ButtonStyle::Outlined) + .key_binding(KeyBinding::for_action( + &OpenWithSystem, + window, + cx, + )), + ), + ) + }), + ), + ) + } +} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 42d452a68ee72491a53e9da535d7713c735912f5..a328bb67924f9be7bcbdc457153699a672fea08b 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,7 +1,7 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -1062,7 +1062,7 @@ pub trait ProjectItem: Item { _e: &anyhow::Error, _window: &mut Window, _cx: &mut App, - ) -> Option + ) -> Option where Self: Sized, { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3e39a8d18faa367798db7a65689bcca59b07a5ae..68900e1156c56e03dcc1b335a93502da771bdc33 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,7 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, - invalid_buffer_view::InvalidBufferView, + invalid_item_view::InvalidItemView, item::{ ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, @@ -992,11 +992,11 @@ impl Pane { let new_item = build_item(self, window, cx); // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. - if let Some(invalid_buffer_view) = new_item.downcast::() { + if let Some(invalid_buffer_view) = new_item.downcast::() { let mut already_open_view = None; let mut views_to_close = HashSet::default(); for existing_error_view in self - .items_of_type::() + .items_of_type::() .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) { if already_open_view.is_none() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2c0a024158733860b6adf956e617382b16e74b16..c0e59060bd9ca963343761b77f5b25b18dd8b302 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,6 +1,6 @@ pub mod dock; pub mod history_manager; -pub mod invalid_buffer_view; +pub mod invalid_item_view; pub mod item; mod modal_layer; pub mod notifications; From 252b75e493ead19919bd5fdc21f917f398e6c8ab Mon Sep 17 00:00:00 2001 From: Owen Law <81528246+someone13574@users.noreply.github.com> Date: Wed, 22 Oct 2025 05:54:54 -0400 Subject: [PATCH 142/202] Disable slang-server for verilog extension (#40442) Should be merged with https://github.com/zed-industries/extensions/pull/3584, which adds `slang-server` as a new language server, but should be disabled by default due to an issue with it not initializing on Windows and being a relatively new language server in general. Release Notes: - N/A --- assets/settings/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5107a7f64f3a57bfb612476797fb3c6659f79b4e..a99261e60abd9b4e2b83325447bf0e9da38cd62a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1820,6 +1820,7 @@ }, "SystemVerilog": { "format_on_save": "off", + "language_servers": ["!slang", "..."], "use_on_type_format": false }, "Vue.js": { From 77dbe08d9d9c26dca41f53b420f26d2c5d74cc35 Mon Sep 17 00:00:00 2001 From: Samuel Oldham <77629938+SO9010@users.noreply.github.com> Date: Wed, 22 Oct 2025 11:20:09 +0100 Subject: [PATCH 143/202] project_panel: Fix buffer focus when canceling filename edit (#40747) Closes #37726 Release Notes: - Fixed an issue where the buffer would not regain focus when clicked while a filename edit was in progress in the project panel. --------- Co-authored-by: Smit Barmase --- crates/project_panel/src/project_panel.rs | 21 +++------ .../project_panel/src/project_panel_tests.rs | 46 +++++++++---------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 67d01a4459bde943e1bdcbaf3d15b3db0f56ce3e..e33dbfd9d0142f335d827b01a07ea66c10efe45a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -491,10 +491,6 @@ impl ProjectPanel { let project_panel = cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); - cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { - this.focus_out(window, cx); - }) - .detach(); cx.subscribe_in( &git_store, @@ -646,7 +642,7 @@ impl ProjectPanel { .as_ref() .is_some_and(|state| state.processing_filename.is_none()) { - match project_panel.confirm_edit(window, cx) { + match project_panel.confirm_edit(false, window, cx) { Some(task) => { task.detach_and_notify_err(window, cx); } @@ -950,12 +946,6 @@ impl ProjectPanel { } } - fn focus_out(&mut self, window: &mut Window, cx: &mut Context) { - if !self.focus_handle.is_focused(window) { - self.confirm(&Confirm, window, cx); - } - } - fn deploy_context_menu( &mut self, position: Point, @@ -1424,7 +1414,7 @@ impl ProjectPanel { } fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { - if let Some(task) = self.confirm_edit(window, cx) { + if let Some(task) = self.confirm_edit(true, window, cx) { task.detach_and_notify_err(window, cx); } } @@ -1556,6 +1546,7 @@ impl ProjectPanel { fn confirm_edit( &mut self, + refocus: bool, window: &mut Window, cx: &mut Context, ) -> Option>> { @@ -1609,7 +1600,7 @@ impl ProjectPanel { filename.clone() }; if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) { - if existing.id == entry.id { + if existing.id == entry.id && refocus { window.focus(&self.focus_handle); } return None; @@ -1620,7 +1611,9 @@ impl ProjectPanel { }); }; - window.focus(&self.focus_handle); + if refocus { + window.focus(&self.focus_handle); + } edit_state.processing_filename = Some(filename); cx.notify(); diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 890041728988bab2914ad7f00ae11637cd9291eb..b6cd1da132ad5c1633001bd53fe365f24870cd7c 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -556,7 +556,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text("the-new-filename", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -616,7 +616,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text("another-filename.txt", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }) .await .unwrap(); @@ -676,7 +676,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { editor.set_text("a-different-filename.tar.gz", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -765,7 +765,7 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { panel .filename_editor .update(cx, |editor, cx| editor.set_text("new-dir", window, cx)); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); panel.update_in(cx, |panel, window, cx| { panel.select_next(&Default::default(), window, cx) @@ -863,11 +863,11 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text("", window, cx); }); - assert!(panel.confirm_edit(window, cx).is_none()); + assert!(panel.confirm_edit(true, window, cx).is_none()); panel.filename_editor.update(cx, |editor, cx| { editor.set_text(" ", window, cx); }); - assert!(panel.confirm_edit(window, cx).is_none()); + assert!(panel.confirm_edit(true, window, cx).is_none()); panel.cancel(&menu::Cancel, window, cx); panel.update_visible_entries(None, false, false, window, cx); }); @@ -986,7 +986,7 @@ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text("/bdir1/dir2/the-new-filename", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); assert_eq!( @@ -1082,7 +1082,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { panel .filename_editor .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx)); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); assert_eq!( @@ -1115,7 +1115,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { panel .filename_editor .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx)); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); @@ -1140,7 +1140,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { panel .filename_editor .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx)); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); @@ -1232,7 +1232,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) { "Should select the file name disambiguation until the extension" ); }); - assert!(panel.confirm_edit(window, cx).is_none()); + assert!(panel.confirm_edit(true, window, cx).is_none()); }); panel.update_in(cx, |panel, window, cx| { @@ -1253,7 +1253,7 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) { ); panel.update_in(cx, |panel, window, cx| { - assert!(panel.confirm_edit(window, cx).is_none()) + assert!(panel.confirm_edit(true, window, cx).is_none()) }); } @@ -1672,7 +1672,7 @@ async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { panel .filename_editor .update(cx, |editor, cx| editor.set_text("c", window, cx)); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -2060,7 +2060,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { .filename_editor .update(cx, |editor, cx| editor.set_text("test", window, cx)); assert!( - panel.confirm_edit(window, cx).is_none(), + panel.confirm_edit(true, window, cx).is_none(), "Should not allow to confirm on conflicting new directory name" ); }); @@ -2116,7 +2116,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { .filename_editor .update(cx, |editor, cx| editor.set_text("first.rs", window, cx)); assert!( - panel.confirm_edit(window, cx).is_none(), + panel.confirm_edit(true, window, cx).is_none(), "Should not allow to confirm on conflicting new file name" ); }); @@ -2174,7 +2174,7 @@ async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { .filename_editor .update(cx, |editor, cx| editor.set_text("second.rs", window, cx)); assert!( - panel.confirm_edit(window, cx).is_none(), + panel.confirm_edit(true, window, cx).is_none(), "Should not allow to confirm on conflicting file rename" ) }); @@ -3041,7 +3041,7 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { panel .filename_editor .update(cx, |editor, cx| editor.set_text("new_root1", window, cx)); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); @@ -4173,7 +4173,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text(excluded_file_path, window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }) .await .unwrap(); @@ -4229,7 +4229,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text(excluded_file_path, window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }) .await .unwrap(); @@ -4273,7 +4273,7 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text(excluded_dir_path, window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }) .await .unwrap(); @@ -5694,7 +5694,7 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { panel.filename_editor.update(cx, |editor, cx| { editor.set_text("hello_from_no_selections", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }) .await .unwrap(); @@ -5792,7 +5792,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC panel.filename_editor.update(cx, |editor, cx| { editor.set_text("new_file_at_root.txt", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); @@ -5843,7 +5843,7 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC panel.filename_editor.update(cx, |editor, cx| { editor.set_text("new_dir_at_root", window, cx) }); - panel.confirm_edit(window, cx).unwrap() + panel.confirm_edit(true, window, cx).unwrap() }); confirm.await.unwrap(); cx.run_until_parked(); From bd69124f2b4f14911483135b4cbd42e0766bea3d Mon Sep 17 00:00:00 2001 From: Nia Date: Wed, 22 Oct 2025 13:04:58 +0200 Subject: [PATCH 144/202] Add option to disable crash handler (#40799) Extra info needed for #39289. To be tested out next nightly build... Release Notes: - N/A Co-authored-by: Cole Miller --- crates/crashes/src/crashes.rs | 36 ++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index 4a45961970cf009ef2a47d5562c1df1792a90aec..62455d552e860b1b00ec0c770d61dd6f03f908fd 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -33,17 +33,31 @@ const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); static PANIC_THREAD_ID: AtomicU32 = AtomicU32::new(0); pub async fn init(crash_init: InitCrashHandler) { - if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() { - let old_hook = panic::take_hook(); - panic::set_hook(Box::new(move |info| { - unsafe { env::set_var("RUST_BACKTRACE", "1") }; - old_hook(info); - // prevent the macOS crash dialog from popping up - std::process::exit(1); - })); - return; - } else { - panic::set_hook(Box::new(panic_hook)); + let gen_var = match env::var("ZED_GENERATE_MINIDUMPS") { + Ok(v) => { + if v == "false" || v == "0" { + Some(false) + } else { + Some(true) + } + } + Err(_) => None, + }; + + match (gen_var, *RELEASE_CHANNEL) { + (Some(false), _) | (None, ReleaseChannel::Dev) => { + let old_hook = panic::take_hook(); + panic::set_hook(Box::new(move |info| { + unsafe { env::set_var("RUST_BACKTRACE", "1") }; + old_hook(info); + // prevent the macOS crash dialog from popping up + std::process::exit(1); + })); + return; + } + (Some(true), _) | (None, _) => { + panic::set_hook(Box::new(panic_hook)); + } } let exe = env::current_exe().expect("unable to find ourselves"); From c81ffaffb61b232501074373ec2d92005aac26cf Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 22 Oct 2025 13:05:32 +0200 Subject: [PATCH 145/202] editor: Use unbounded shifts for chunk bitmaps (#40879) This simplifies some code and is also more correct in some others (I believe some of these might've overflowed causing panics in sentry) Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/editor/src/display_map/block_map.rs | 8 +- .../src/display_map/custom_highlights.rs | 32 ++--- crates/editor/src/display_map/fold_map.rs | 9 +- crates/editor/src/display_map/inlay_map.rs | 43 +++---- crates/editor/src/display_map/tab_map.rs | 89 +++++++------- crates/editor/src/display_map/wrap_map.rs | 17 +-- crates/language/src/buffer.rs | 28 ++--- crates/multi_buffer/src/multi_buffer.rs | 8 +- crates/rope/src/chunk.rs | 111 +++++++++--------- crates/rope/src/rope.rs | 47 ++------ 12 files changed, 168 insertions(+), 226 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27848216c6931b8db41500a1d835d997dafd4cbd..3d57553190ec0e659dab8de9b60a57eb87fc6356 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5354,6 +5354,7 @@ dependencies = [ "rand 0.9.2", "regex", "release_channel", + "rope", "rpc", "schemars 1.0.4", "serde", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 4631643db924d1211fc01d662999cb3fc1298faa..62226f5dec2aa88f0ccdb6ad59935f6bdfe6536e 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -64,6 +64,7 @@ project.workspace = true rand.workspace = true regex.workspace = true rpc.workspace = true +rope.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4b8a48ac2d9d2b35fbd15724c06032056e05ca67..8bfcd2f063c663c7ab7bbbc9cefaf50cc0f53192 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -26,8 +26,8 @@ use sum_tree::{Bias, ContextLessSummary, Dimensions, SumTree, TreeMap}; use text::{BufferId, Edit}; use ui::ElementId; -const NEWLINES: &[u8; u128::BITS as usize] = &[b'\n'; _]; -const BULLETS: &[u8; u128::BITS as usize] = &[b'*'; _]; +const NEWLINES: &[u8; rope::Chunk::MASK_BITS] = &[b'\n'; _]; +const BULLETS: &[u8; rope::Chunk::MASK_BITS] = &[b'*'; _]; /// Tracks custom blocks such as diagnostics that should be displayed within buffer. /// @@ -1783,11 +1783,11 @@ impl<'a> Iterator for BlockChunks<'a> { if self.masked { // Not great for multibyte text because to keep cursor math correct we - // need to have the same number of bytes in the input as output. + // need to have the same number of chars in the input as output. let chars_count = prefix.chars().count(); let bullet_len = chars_count; prefix = unsafe { std::str::from_utf8_unchecked(&BULLETS[..bullet_len]) }; - chars = 1u128.unbounded_shl(bullet_len as u32) - 1; + chars = 1u128.unbounded_shl(bullet_len as u32).wrapping_sub(1); tabs = 0; } diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs index b7518af59c28dbc95a36d24b36a7eae2862916b6..c6b22bb0b8247420200c2bb8d9e22f55d638386d 100644 --- a/crates/editor/src/display_map/custom_highlights.rs +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -132,37 +132,31 @@ impl<'a> Iterator for CustomHighlightsChunks<'a> { } } - let chunk = self - .buffer_chunk - .get_or_insert_with(|| self.buffer_chunks.next().unwrap_or_default()); - if chunk.text.is_empty() { + let chunk = match &mut self.buffer_chunk { + Some(it) => it, + slot => slot.insert(self.buffer_chunks.next()?), + }; + while chunk.text.is_empty() { *chunk = self.buffer_chunks.next()?; } let split_idx = chunk.text.len().min(next_highlight_endpoint - self.offset); let (prefix, suffix) = chunk.text.split_at(split_idx); - - let (chars, tabs) = if split_idx == 128 { - let output = (chunk.chars, chunk.tabs); - chunk.chars = 0; - chunk.tabs = 0; - output - } else { - let mask = (1 << split_idx) - 1; - let output = (chunk.chars & mask, chunk.tabs & mask); - chunk.chars = chunk.chars >> split_idx; - chunk.tabs = chunk.tabs >> split_idx; - output - }; - - chunk.text = suffix; self.offset += prefix.len(); + + let mask = 1u128.unbounded_shl(split_idx as u32).wrapping_sub(1); + let chars = chunk.chars & mask; + let tabs = chunk.tabs & mask; let mut prefix = Chunk { text: prefix, chars, tabs, ..chunk.clone() }; + + chunk.chars = chunk.chars.unbounded_shr(split_idx as u32); + chunk.tabs = chunk.tabs.unbounded_shr(split_idx as u32); + chunk.text = suffix; if !self.active_highlights.is_empty() { prefix.highlight_style = self .active_highlights diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index d3bc7acfd303d7952cc46001306067dabb5b089f..d93f5acbc65a9a39a95df51469a3bcc02989426c 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1436,14 +1436,15 @@ impl<'a> Iterator for FoldChunks<'a> { let transform_end = self.transform_cursor.end().1; let chunk_end = buffer_chunk_end.min(transform_end); - chunk.text = &chunk.text - [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0]; + let bit_start = (self.inlay_offset - buffer_chunk_start).0; + let bit_end = (chunk_end - buffer_chunk_start).0; + chunk.text = &chunk.text[bit_start..bit_end]; let bit_end = (chunk_end - buffer_chunk_start).0; let mask = 1u128.unbounded_shl(bit_end as u32).wrapping_sub(1); - chunk.tabs = (chunk.tabs >> (self.inlay_offset - buffer_chunk_start).0) & mask; - chunk.chars = (chunk.chars >> (self.inlay_offset - buffer_chunk_start).0) & mask; + chunk.tabs = (chunk.tabs >> bit_start) & mask; + chunk.chars = (chunk.chars >> bit_start) & mask; if chunk_end == transform_end { self.transform_cursor.next(); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 8bda58da173ad45c524c5062bed6daf8309d114d..050cd06a9781db5812cf129968471561e2abd095 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -325,21 +325,16 @@ impl<'a> Iterator for InlayChunks<'a> { }; let (prefix, suffix) = chunk.text.split_at(split_index); + self.output_offset.0 += prefix.len(); - let (chars, tabs) = if split_index == 128 { - let output = (chunk.chars, chunk.tabs); - chunk.chars = 0; - chunk.tabs = 0; - output - } else { - let mask = (1 << split_index) - 1; - let output = (chunk.chars & mask, chunk.tabs & mask); - chunk.chars = chunk.chars >> split_index; - chunk.tabs = chunk.tabs >> split_index; - output - }; + let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1); + let chars = chunk.chars & mask; + let tabs = chunk.tabs & mask; + + chunk.chars = chunk.chars.unbounded_shr(split_index as u32); + chunk.tabs = chunk.tabs.unbounded_shr(split_index as u32); chunk.text = suffix; - self.output_offset.0 += prefix.len(); + InlayChunk { chunk: Chunk { text: prefix, @@ -457,18 +452,12 @@ impl<'a> Iterator for InlayChunks<'a> { let (chunk, remainder) = inlay_chunk.split_at(split_index); *inlay_chunk = remainder; - let (chars, tabs) = if split_index == 128 { - let output = (*chars, *tabs); - *chars = 0; - *tabs = 0; - output - } else { - let mask = (1 << split_index as u32) - 1; - let output = (*chars & mask, *tabs & mask); - *chars = *chars >> split_index; - *tabs = *tabs >> split_index; - output - }; + let mask = 1u128.unbounded_shl(split_index as u32).wrapping_sub(1); + let new_chars = *chars & mask; + let new_tabs = *tabs & mask; + + *chars = chars.unbounded_shr(split_index as u32); + *tabs = tabs.unbounded_shr(split_index as u32); if inlay_chunk.is_empty() { self.inlay_chunk = None; @@ -479,8 +468,8 @@ impl<'a> Iterator for InlayChunks<'a> { InlayChunk { chunk: Chunk { text: chunk, - chars, - tabs, + chars: new_chars, + tabs: new_tabs, highlight_style, is_inlay: true, ..Chunk::default() diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index b37d81c66614030ab574244f3de0277d3fd8bee9..567533aef556c10a966bc2574a0056c3a115f916 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -11,7 +11,7 @@ use sum_tree::Bias; const MAX_EXPANSION_COLUMN: u32 = 256; // Handles a tab width <= 128 -const SPACES: &[u8; u128::BITS as usize] = &[b' '; _]; +const SPACES: &[u8; rope::Chunk::MASK_BITS] = &[b' '; _]; const MAX_TABS: NonZeroU32 = NonZeroU32::new(SPACES.len() as u32).unwrap(); /// Keeps track of hard tabs in a text buffer. @@ -569,56 +569,47 @@ impl<'a> Iterator for TabChunks<'a> { //todo(improve performance by using tab cursor) for (ix, c) in self.chunk.text.char_indices() { match c { + '\t' if ix > 0 => { + let (prefix, suffix) = self.chunk.text.split_at(ix); + + let mask = 1u128.unbounded_shl(ix as u32).wrapping_sub(1); + let chars = self.chunk.chars & mask; + let tabs = self.chunk.tabs & mask; + self.chunk.tabs = self.chunk.tabs.unbounded_shr(ix as u32); + self.chunk.chars = self.chunk.chars.unbounded_shr(ix as u32); + self.chunk.text = suffix; + return Some(Chunk { + text: prefix, + chars, + tabs, + ..self.chunk.clone() + }); + } '\t' => { - if ix > 0 { - let (prefix, suffix) = self.chunk.text.split_at(ix); - - let (chars, tabs) = if ix == 128 { - let output = (self.chunk.chars, self.chunk.tabs); - self.chunk.chars = 0; - self.chunk.tabs = 0; - output - } else { - let mask = (1 << ix) - 1; - let output = (self.chunk.chars & mask, self.chunk.tabs & mask); - self.chunk.chars = self.chunk.chars >> ix; - self.chunk.tabs = self.chunk.tabs >> ix; - output - }; - - self.chunk.text = suffix; - return Some(Chunk { - text: prefix, - chars, - tabs, - ..self.chunk.clone() - }); + self.chunk.text = &self.chunk.text[1..]; + self.chunk.tabs >>= 1; + self.chunk.chars >>= 1; + let tab_size = if self.input_column < self.max_expansion_column { + self.tab_size.get() } else { - self.chunk.text = &self.chunk.text[1..]; - self.chunk.tabs >>= 1; - self.chunk.chars >>= 1; - let tab_size = if self.input_column < self.max_expansion_column { - self.tab_size.get() - } else { - 1 - }; - let mut len = tab_size - self.column % tab_size; - let next_output_position = cmp::min( - self.output_position + Point::new(0, len), - self.max_output_position, - ); - len = next_output_position.column - self.output_position.column; - self.column += len; - self.input_column += 1; - self.output_position = next_output_position; - return Some(Chunk { - text: unsafe { std::str::from_utf8_unchecked(&SPACES[..len as usize]) }, - is_tab: true, - chars: 1u128.unbounded_shl(len) - 1, - tabs: 0, - ..self.chunk.clone() - }); - } + 1 + }; + let mut len = tab_size - self.column % tab_size; + let next_output_position = cmp::min( + self.output_position + Point::new(0, len), + self.max_output_position, + ); + len = next_output_position.column - self.output_position.column; + self.column += len; + self.input_column += 1; + self.output_position = next_output_position; + return Some(Chunk { + text: unsafe { std::str::from_utf8_unchecked(&SPACES[..len as usize]) }, + is_tab: true, + chars: 1u128.unbounded_shl(len) - 1, + tabs: 0, + ..self.chunk.clone() + }); } '\n' => { self.column = 0; diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 6c1ee61de09327d06c17ed977c5f236c2d7232e8..e79e5555a61d0ddb8a93a1708c676554f191c3f6 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -972,18 +972,11 @@ impl<'a> Iterator for WrapChunks<'a> { let (prefix, suffix) = self.input_chunk.text.split_at(input_len); - let (chars, tabs) = if input_len == 128 { - let output = (self.input_chunk.chars, self.input_chunk.tabs); - self.input_chunk.chars = 0; - self.input_chunk.tabs = 0; - output - } else { - let mask = (1 << input_len) - 1; - let output = (self.input_chunk.chars & mask, self.input_chunk.tabs & mask); - self.input_chunk.chars = self.input_chunk.chars >> input_len; - self.input_chunk.tabs = self.input_chunk.tabs >> input_len; - output - }; + let mask = 1u128.unbounded_shl(input_len as u32).wrapping_sub(1); + let chars = self.input_chunk.chars & mask; + let tabs = self.input_chunk.tabs & mask; + self.input_chunk.tabs = self.input_chunk.tabs.unbounded_shr(input_len as u32); + self.input_chunk.chars = self.input_chunk.chars.unbounded_shr(input_len as u32); self.input_chunk.text = suffix; Some(Chunk { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3b90ae6ba5df5484c79f90cf91649b9f363e92b6..973add14f33a3a9554df4a20c55aff3eb3453683 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -506,15 +506,15 @@ pub struct Chunk<'a> { pub highlight_style: Option, /// The severity of diagnostic associated with this chunk, if any. pub diagnostic_severity: Option, - /// Whether this chunk of text is marked as unnecessary. - pub is_unnecessary: bool, - /// Whether this chunk of text was originally a tab character. - pub is_tab: bool, /// A bitset of which characters are tabs in this string. pub tabs: u128, /// Bitmap of character indices in this chunk pub chars: u128, + /// Whether this chunk of text is marked as unnecessary. + pub is_unnecessary: bool, /// Whether this chunk of text was originally a tab character. + pub is_tab: bool, + /// Whether this chunk of text was originally an inlay. pub is_inlay: bool, /// Whether to underline the corresponding text range in the editor. pub underline: bool, @@ -4982,7 +4982,7 @@ impl<'a> Iterator for BufferChunks<'a> { text: chunk, chars: chars_map, tabs, - }) = self.chunks.peek_tabs() + }) = self.chunks.peek_with_bitmaps() { let chunk_start = self.range.start; let mut chunk_end = (self.chunks.offset() + chunk.len()) @@ -4995,18 +4995,14 @@ impl<'a> Iterator for BufferChunks<'a> { chunk_end = chunk_end.min(*parent_capture_end); highlight_id = Some(*parent_highlight_id); } - - let slice = - &chunk[chunk_start - self.chunks.offset()..chunk_end - self.chunks.offset()]; + let bit_start = chunk_start - self.chunks.offset(); let bit_end = chunk_end - self.chunks.offset(); - let mask = if bit_end >= 128 { - u128::MAX - } else { - (1u128 << bit_end) - 1 - }; - let tabs = (tabs >> (chunk_start - self.chunks.offset())) & mask; - let chars_map = (chars_map >> (chunk_start - self.chunks.offset())) & mask; + let slice = &chunk[bit_start..bit_end]; + + let mask = 1u128.unbounded_shl(bit_end as u32).wrapping_sub(1); + let tabs = (tabs >> bit_start) & mask; + let chars = (chars_map >> bit_start) & mask; self.range.start = chunk_end; if self.range.start == self.chunks.offset() + chunk.len() { @@ -5020,7 +5016,7 @@ impl<'a> Iterator for BufferChunks<'a> { diagnostic_severity: self.current_diagnostic_severity(), is_unnecessary: self.current_code_is_unnecessary(), tabs, - chars: chars_map, + chars, ..Chunk::default() }) } else { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index a5526e823aae6da34dbb1f6ff8d869bad9624b60..e9e3b6f62c2bd5ec4a40ea8329aaf05110f91173 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -51,7 +51,7 @@ use text::{ use theme::SyntaxTheme; use util::{post_inc, rel_path::RelPath}; -const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; +const NEWLINES: &[u8] = &[b'\n'; rope::Chunk::MASK_BITS]; #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct ExcerptId(u32); @@ -7730,7 +7730,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { let split_idx = diff_transform_end - self.range.start; let (before, after) = chunk.text.split_at(split_idx); self.range.start = diff_transform_end; - let mask = (1 << split_idx) - 1; + let mask = 1u128.unbounded_shl(split_idx as u32).wrapping_sub(1); let chars = chunk.chars & mask; let tabs = chunk.tabs & mask; @@ -7882,7 +7882,9 @@ impl<'a> Iterator for ExcerptChunks<'a> { if self.footer_height > 0 { let text = unsafe { str::from_utf8_unchecked(&NEWLINES[..self.footer_height]) }; - let chars = (1 << self.footer_height) - 1; + let chars = 1u128 + .unbounded_shl(self.footer_height as u32) + .wrapping_sub(1); self.footer_height = 0; return Some(Chunk { text, diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 91e61a517144f5b2408902173a8c34ccb865e9d5..515ae8c768657681de1614ba03fdce6d176b96ca 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -5,29 +5,36 @@ use sum_tree::Bias; use unicode_segmentation::GraphemeCursor; use util::debug_panic; -pub(crate) const MIN_BASE: usize = if cfg!(test) { 6 } else { 64 }; -pub(crate) const MAX_BASE: usize = MIN_BASE * 2; +#[cfg(not(all(test, not(rust_analyzer))))] +pub(crate) type Bitmap = u128; +#[cfg(all(test, not(rust_analyzer)))] +pub(crate) type Bitmap = u16; + +pub(crate) const MIN_BASE: usize = MAX_BASE / 2; +pub(crate) const MAX_BASE: usize = Bitmap::BITS as usize; #[derive(Clone, Debug, Default)] pub struct Chunk { /// If bit[i] is set, then the character at index i is the start of a UTF-8 character in the /// text. - chars: u128, + chars: Bitmap, /// The number of set bits is the number of UTF-16 code units it would take to represent the /// text. /// /// Bit[i] is set if text[i] is the start of a UTF-8 character. If the character would /// take two UTF-16 code units, then bit[i+1] is also set. (Rust chars never take more /// than two UTF-16 code units.) - chars_utf16: u128, + chars_utf16: Bitmap, /// If bit[i] is set, then the character at index i is an ascii newline. - newlines: u128, + newlines: Bitmap, /// If bit[i] is set, then the character at index i is an ascii tab. - pub tabs: u128, + tabs: Bitmap, pub text: ArrayString, } impl Chunk { + pub const MASK_BITS: usize = Bitmap::BITS as usize; + #[inline(always)] pub fn new(text: &str) -> Self { let mut this = Chunk::default(); @@ -41,9 +48,9 @@ impl Chunk { let ix = self.text.len() + char_ix; self.chars |= 1 << ix; self.chars_utf16 |= 1 << ix; - self.chars_utf16 |= (c.len_utf16() as u128) << ix; - self.newlines |= ((c == '\n') as u128) << ix; - self.tabs |= ((c == '\t') as u128) << ix; + self.chars_utf16 |= (c.len_utf16() as Bitmap) << ix; + self.newlines |= ((c == '\n') as Bitmap) << ix; + self.tabs |= ((c == '\t') as Bitmap) << ix; } self.text.push_str(text); } @@ -79,17 +86,21 @@ impl Chunk { } #[inline(always)] - pub fn chars(&self) -> u128 { + pub fn chars(&self) -> Bitmap { self.chars } + + pub fn tabs(&self) -> Bitmap { + self.tabs + } } #[derive(Clone, Copy, Debug)] pub struct ChunkSlice<'a> { - chars: u128, - chars_utf16: u128, - newlines: u128, - tabs: u128, + chars: Bitmap, + chars_utf16: Bitmap, + newlines: Bitmap, + tabs: Bitmap, text: &'a str, } @@ -129,7 +140,7 @@ impl<'a> ChunkSlice<'a> { }; (left, right) } else { - let mask = (1u128 << mid) - 1; + let mask = ((1 as Bitmap) << mid) - 1; let (left_text, right_text) = self.text.split_at(mid); let left = ChunkSlice { chars: self.chars & mask, @@ -151,17 +162,15 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn slice(self, range: Range) -> Self { - let mask = if range.end == MAX_BASE { - u128::MAX - } else { - debug_assert!( - self.is_char_boundary(range.end), - "Invalid range end {} in {:?}", - range.end, - self - ); - (1u128 << range.end) - 1 - }; + debug_assert!( + self.is_char_boundary(range.end), + "Invalid range end {} in {:?}", + range.end, + self + ); + let mask = (1 as Bitmap) + .unbounded_shl(range.end as u32) + .wrapping_sub(1); if range.start == MAX_BASE { Self { chars: 0, @@ -220,7 +229,7 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn lines(&self) -> Point { let row = self.newlines.count_ones(); - let column = self.newlines.leading_zeros() - (u128::BITS - self.text.len() as u32); + let column = self.newlines.leading_zeros() - (Bitmap::BITS - self.text.len() as u32); Point::new(row, column) } @@ -230,7 +239,7 @@ impl<'a> ChunkSlice<'a> { if self.newlines == 0 { self.chars.count_ones() } else { - let mask = (1u128 << self.newlines.trailing_zeros()) - 1; + let mask = ((1 as Bitmap) << self.newlines.trailing_zeros()) - 1; (self.chars & mask).count_ones() } } @@ -241,7 +250,7 @@ impl<'a> ChunkSlice<'a> { if self.newlines == 0 { self.chars.count_ones() } else { - let mask = !(u128::MAX >> self.newlines.leading_zeros()); + let mask = !(Bitmap::MAX >> self.newlines.leading_zeros()); (self.chars & mask).count_ones() } } @@ -252,7 +261,7 @@ impl<'a> ChunkSlice<'a> { if self.newlines == 0 { self.chars_utf16.count_ones() } else { - let mask = !(u128::MAX >> self.newlines.leading_zeros()); + let mask = !(Bitmap::MAX >> self.newlines.leading_zeros()); (self.chars_utf16 & mask).count_ones() } } @@ -295,13 +304,9 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn offset_to_point(&self, offset: usize) -> Point { - let mask = if offset == MAX_BASE { - u128::MAX - } else { - (1u128 << offset) - 1 - }; + let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); let row = (self.newlines & mask).count_ones(); - let newline_ix = u128::BITS - (self.newlines & mask).leading_zeros(); + let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros(); let column = (offset - newline_ix as usize) as u32; Point::new(row, column) } @@ -332,11 +337,7 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - let mask = if offset == MAX_BASE { - u128::MAX - } else { - (1u128 << offset) - 1 - }; + let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); OffsetUtf16((self.chars_utf16 & mask).count_ones() as usize) } @@ -345,7 +346,11 @@ impl<'a> ChunkSlice<'a> { if target.0 == 0 { 0 } else { - let ix = nth_set_bit(self.chars_utf16, target.0) + 1; + #[cfg(not(test))] + let chars_utf16 = self.chars_utf16; + #[cfg(test)] + let chars_utf16 = self.chars_utf16 as u128; + let ix = nth_set_bit(chars_utf16, target.0) + 1; if ix == MAX_BASE { MAX_BASE } else { @@ -360,13 +365,9 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - let mask = if offset == MAX_BASE { - u128::MAX - } else { - (1u128 << offset) - 1 - }; + let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); let row = (self.newlines & mask).count_ones(); - let newline_ix = u128::BITS - (self.newlines & mask).leading_zeros(); + let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros(); let column = if newline_ix as usize == MAX_BASE { 0 } else { @@ -520,7 +521,11 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] fn offset_range_for_row(&self, row: u32) -> Range { let row_start = if row > 0 { - nth_set_bit(self.newlines, row as usize) + 1 + #[cfg(not(test))] + let newlines = self.newlines; + #[cfg(test)] + let newlines = self.newlines as u128; + nth_set_bit(newlines, row as usize) + 1 } else { 0 }; @@ -545,8 +550,8 @@ impl<'a> ChunkSlice<'a> { } pub struct Tabs { - tabs: u128, - chars: u128, + tabs: Bitmap, + chars: Bitmap, } #[derive(Debug, PartialEq, Eq)] @@ -647,8 +652,8 @@ mod tests { // Verify Chunk::chars() bitmap let expected_chars = char_offsets(&text) .into_iter() - .inspect(|i| assert!(*i < 128)) - .fold(0u128, |acc, i| acc | (1 << i)); + .inspect(|i| assert!(*i < MAX_BASE)) + .fold(0 as Bitmap, |acc, i| acc | (1 << i)); assert_eq!(chunk.chars(), expected_chars); for _ in 0..10 { diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index b4bf987e019c424a3ec989454184feea87879750..c710ed86570ef5aadd7f7a31f9c440226b073ae8 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -4,7 +4,6 @@ mod point; mod point_utf16; mod unclipped; -use chunk::Chunk; use rayon::iter::{IntoParallelIterator, ParallelIterator as _}; use smallvec::SmallVec; use std::{ @@ -14,12 +13,14 @@ use std::{ }; use sum_tree::{Bias, Dimension, Dimensions, SumTree}; -pub use chunk::ChunkSlice; +pub use chunk::{Chunk, ChunkSlice}; pub use offset_utf16::OffsetUtf16; pub use point::Point; pub use point_utf16::PointUtf16; pub use unclipped::Unclipped; +use crate::chunk::Bitmap; + #[derive(Clone, Default)] pub struct Rope { chunks: SumTree, @@ -676,9 +677,9 @@ pub struct ChunkBitmaps<'a> { /// A slice of text up to 128 bytes in size pub text: &'a str, /// Bitmap of character locations in text. LSB ordered - pub chars: u128, + pub chars: Bitmap, /// Bitmap of tab locations in text. LSB ordered - pub tabs: u128, + pub tabs: Bitmap, } #[derive(Clone)] @@ -850,39 +851,6 @@ impl<'a> Chunks<'a> { self.offset < initial_offset && self.offset == 0 } - /// Returns bitmaps that represent character positions and tab positions - pub fn peek_with_bitmaps(&self) -> Option> { - if !self.offset_is_valid() { - return None; - } - - let chunk = self.chunks.item()?; - let chunk_start = *self.chunks.start(); - let slice_range = if self.reversed { - let slice_start = cmp::max(chunk_start, self.range.start) - chunk_start; - let slice_end = self.offset - chunk_start; - slice_start..slice_end - } else { - let slice_start = self.offset - chunk_start; - let slice_end = cmp::min(self.chunks.end(), self.range.end) - chunk_start; - slice_start..slice_end - }; - - // slice range has a bounds between 0 and 128 in non test builds - // We use a non wrapping sub because we want to overflow in the case where slice_range.end == 128 - // because that represents a full chunk and the bitmask shouldn't remove anything - let bitmask = (1u128.unbounded_shl(slice_range.end as u32)).wrapping_sub(1); - - let chars = (chunk.chars() & bitmask) >> slice_range.start; - let tabs = (chunk.tabs & bitmask) >> slice_range.start; - - Some(ChunkBitmaps { - text: &chunk.text[slice_range], - chars, - tabs, - }) - } - pub fn peek(&self) -> Option<&'a str> { if !self.offset_is_valid() { return None; @@ -903,7 +871,8 @@ impl<'a> Chunks<'a> { Some(&chunk.text[slice_range]) } - pub fn peek_tabs(&self) -> Option> { + /// Returns bitmaps that represent character positions and tab positions + pub fn peek_with_bitmaps(&self) -> Option> { if !self.offset_is_valid() { return None; } @@ -923,7 +892,7 @@ impl<'a> Chunks<'a> { let slice_text = &chunk.text[slice_range]; // Shift the tabs to align with our slice window - let shifted_tabs = chunk.tabs >> chunk_start_offset; + let shifted_tabs = chunk.tabs() >> chunk_start_offset; let shifted_chars = chunk.chars() >> chunk_start_offset; Some(ChunkBitmaps { From 98c7e018ae729eb15896bb2d8ee14e0efd26e79c Mon Sep 17 00:00:00 2001 From: Cameron Mcloughlin Date: Wed, 22 Oct 2025 12:25:50 +0100 Subject: [PATCH 146/202] Add new action and handler for opening a specific setting (#40739) Co-authored-by: Ben Kunkle --- crates/settings_ui/src/page_data.rs | 339 +++++++++++++++++++++++++- crates/settings_ui/src/settings_ui.rs | 153 ++++++++++-- crates/zed/src/main.rs | 12 + crates/zed/src/zed/open_listener.rs | 5 + crates/zed_actions/src/lib.rs | 12 +- 5 files changed, 500 insertions(+), 21 deletions(-) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 7838c99989b625f8c87c0264482dcf36cd6c46d8..5997d14de33bdec4b4739112a02b22ac0f12e2e7 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -31,6 +31,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The displayed name of this project. If left empty, the root directory name will be displayed.", field: Box::new( SettingField { + json_path: Some("project_name"), pick: |settings_content| { settings_content.project.worktree.project_name.as_ref()?.as_ref().or(DEFAULT_EMPTY_STRING) }, @@ -45,6 +46,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "When Closing With No Tabs", description: "What to do when using the 'close active item' action with no tabs.", field: Box::new(SettingField { + json_path: Some("when_closing_with_no_tabs"), pick: |settings_content| { settings_content .workspace @@ -62,6 +64,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "On Last Window Closed", description: "What to do when the last window is closed.", field: Box::new(SettingField { + json_path: Some("on_last_window_closed"), pick: |settings_content| { settings_content.workspace.on_last_window_closed.as_ref() }, @@ -76,6 +79,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Use System Path Prompts", description: "Use native OS dialogs for 'Open' and 'Save As'.", field: Box::new(SettingField { + json_path: Some("use_system_path_prompts"), pick: |settings_content| { settings_content.workspace.use_system_path_prompts.as_ref() }, @@ -90,6 +94,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Use System Prompts", description: "Use native OS dialogs for confirmations.", field: Box::new(SettingField { + json_path: Some("use_system_prompts"), pick: |settings_content| { settings_content.workspace.use_system_prompts.as_ref() }, @@ -104,6 +109,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Redact Private Values", description: "Hide the values of variables in private files.", field: Box::new(SettingField { + json_path: Some("redact_private_values"), pick: |settings_content| { settings_content.editor.redact_private_values.as_ref() }, @@ -119,6 +125,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Globs to match against file paths to determine if a file is private.", field: Box::new( SettingField { + json_path: Some("worktree.private_files"), pick: |settings_content| { settings_content.project.worktree.private_files.as_ref() }, @@ -136,6 +143,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Restore Unsaved Buffers", description: "Whether or not to restore unsaved buffers on restart.", field: Box::new(SettingField { + json_path: Some("session.restore_unsaved_buffers"), pick: |settings_content| { settings_content .session @@ -156,6 +164,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Restore On Startup", description: "What to restore from the previous session when opening Zed.", field: Box::new(SettingField { + json_path: Some("restore_on_startup"), pick: |settings_content| { settings_content.workspace.restore_on_startup.as_ref() }, @@ -174,6 +183,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Which settings should be activated only in Preview build of Zed.", field: Box::new( SettingField { + json_path: Some("use_system_prompts"), pick: |settings_content| { settings_content.workspace.use_system_prompts.as_ref() }, @@ -191,6 +201,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.", field: Box::new( SettingField { + json_path: Some(""), pick: |settings_content| { settings_content.workspace.use_system_prompts.as_ref() }, @@ -207,6 +218,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Telemetry Diagnostics", description: "Send debug information like crash reports.", field: Box::new(SettingField { + json_path: Some("telemetry.diagnostics"), pick: |settings_content| { settings_content .telemetry @@ -227,6 +239,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Telemetry Metrics", description: "Send anonymized usage data like what languages you're using Zed with.", field: Box::new(SettingField { + json_path: Some("telemetry.metrics"), pick: |settings_content| { settings_content .telemetry @@ -245,6 +258,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Update", description: "Whether or not to automatically check for updates.", field: Box::new(SettingField { + json_path: Some("auto_update"), pick: |settings_content| settings_content.auto_update.as_ref(), write: |settings_content, value| { settings_content.auto_update = value; @@ -265,6 +279,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Theme Mode", description: "Choose a static, fixed theme or dynamically select themes based on appearance and light/dark modes.", field: Box::new(SettingField { + json_path: Some("theme$"), pick: |settings_content| { Some(&dynamic_variants::()[ settings_content @@ -322,6 +337,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Theme Name", description: "The name of your selected theme.", field: Box::new(SettingField { + json_path: Some("theme"), pick: |settings_content| { match settings_content.theme.theme.as_ref() { Some(settings::ThemeSelection::Static(name)) => Some(name), @@ -349,6 +365,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Mode", description: "Choose whether to use the selected light or dark theme or to follow your OS appearance configuration.", field: Box::new(SettingField { + json_path: Some("theme.mode"), pick: |settings_content| { match settings_content.theme.theme.as_ref() { Some(settings::ThemeSelection::Dynamic { mode, ..}) => Some(mode), @@ -374,6 +391,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Light Theme", description: "The theme to use when mode is set to light, or when mode is set to system and it is in light mode.", field: Box::new(SettingField { + json_path: Some("theme.light"), pick: |settings_content| { match settings_content.theme.theme.as_ref() { Some(settings::ThemeSelection::Dynamic { light, ..}) => Some(light), @@ -399,6 +417,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Dark Theme", description: "The theme to use when mode is set to dark, or when mode is set to system and it is in dark mode.", field: Box::new(SettingField { + json_path: Some("theme.dark"), pick: |settings_content| { match settings_content.theme.theme.as_ref() { Some(settings::ThemeSelection::Dynamic { dark, ..}) => Some(dark), @@ -429,6 +448,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Icon Theme", description: "The custom set of icons Zed will associate with files and directories.", field: Box::new(SettingField { + json_path: Some("icon_theme$"), pick: |settings_content| { Some(&dynamic_variants::()[ settings_content @@ -486,6 +506,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Icon Theme Name", description: "The name of your selected icon theme.", field: Box::new(SettingField { + json_path: Some("icon_theme$string"), pick: |settings_content| { match settings_content.theme.icon_theme.as_ref() { Some(settings::IconThemeSelection::Static(name)) => Some(name), @@ -513,6 +534,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Mode", description: "Choose whether to use the selected light or dark icon theme or to follow your OS appearance configuration.", field: Box::new(SettingField { + json_path: Some("icon_theme"), pick: |settings_content| { match settings_content.theme.icon_theme.as_ref() { Some(settings::IconThemeSelection::Dynamic { mode, ..}) => Some(mode), @@ -538,6 +560,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Light Icon Theme", description: "The icon theme to use when mode is set to light, or when mode is set to system and it is in light mode.", field: Box::new(SettingField { + json_path: Some("icon_theme.light"), pick: |settings_content| { match settings_content.theme.icon_theme.as_ref() { Some(settings::IconThemeSelection::Dynamic { light, ..}) => Some(light), @@ -563,6 +586,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Dark Icon Theme", description: "The icon theme to use when mode is set to dark, or when mode is set to system and it is in dark mode.", field: Box::new(SettingField { + json_path: Some("icon_theme.dark"), pick: |settings_content| { match settings_content.theme.icon_theme.as_ref() { Some(settings::IconThemeSelection::Dynamic { dark, ..}) => Some(dark), @@ -592,6 +616,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Family", description: "Font family for editor text.", field: Box::new(SettingField { + json_path: Some("buffer_font_family"), pick: |settings_content| settings_content.theme.buffer_font_family.as_ref(), write: |settings_content, value|{ settings_content.theme.buffer_font_family = value;}, }), @@ -602,6 +627,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Size", description: "Font size for editor text.", field: Box::new(SettingField { + json_path: Some("buffer_font_size"), pick: |settings_content| settings_content.theme.buffer_font_size.as_ref(), write: |settings_content, value|{ settings_content.theme.buffer_font_size = value;}, }), @@ -612,6 +638,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Weight", description: "Font weight for editor text (100-900).", field: Box::new(SettingField { + json_path: Some("buffer_font_weight"), pick: |settings_content| settings_content.theme.buffer_font_weight.as_ref(), write: |settings_content, value|{ settings_content.theme.buffer_font_weight = value;}, }), @@ -624,6 +651,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Line Height", description: "Line height for editor text.", field: Box::new(SettingField { + json_path: Some("buffer_line_height$"), pick: |settings_content| { Some(&dynamic_variants::()[ settings_content @@ -668,6 +696,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Custom Line Height", description: "Custom line height value (must be at least 1.0).", field: Box::new(SettingField { + json_path: Some("buffer_line_height"), pick: |settings_content| { match settings_content.theme.buffer_line_height.as_ref() { Some(settings::BufferLineHeight::Custom(value)) => Some(value), @@ -698,6 +727,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The OpenType features to enable for rendering in text buffers.", field: Box::new( SettingField { + json_path: Some("buffer_font_features"), pick: |settings_content| { settings_content.theme.buffer_font_features.as_ref() }, @@ -716,6 +746,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The font fallbacks to use for rendering in text buffers.", field: Box::new( SettingField { + json_path: Some("buffer_font_fallbacks"), pick: |settings_content| { settings_content.theme.buffer_font_fallbacks.as_ref() }, @@ -733,6 +764,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Family", description: "Font family for UI elements.", field: Box::new(SettingField { + json_path: Some("ui_font_family"), pick: |settings_content| settings_content.theme.ui_font_family.as_ref(), write: |settings_content, value|{ settings_content.theme.ui_font_family = value;}, }), @@ -743,6 +775,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Size", description: "Font size for UI elements.", field: Box::new(SettingField { + json_path: Some("ui_font_size"), pick: |settings_content| settings_content.theme.ui_font_size.as_ref(), write: |settings_content, value|{ settings_content.theme.ui_font_size = value;}, }), @@ -753,6 +786,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Weight", description: "Font weight for UI elements (100-900).", field: Box::new(SettingField { + json_path: Some("ui_font_weight"), pick: |settings_content| settings_content.theme.ui_font_weight.as_ref(), write: |settings_content, value|{ settings_content.theme.ui_font_weight = value;}, }), @@ -765,6 +799,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The Opentype features to enable for rendering in UI elements.", field: Box::new( SettingField { + json_path: Some("ui_font_features"), pick: |settings_content| { settings_content.theme.ui_font_features.as_ref() }, @@ -783,6 +818,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The font fallbacks to use for rendering in the UI.", field: Box::new( SettingField { + json_path: Some("ui_font_fallbacks"), pick: |settings_content| { settings_content.theme.ui_font_fallbacks.as_ref() }, @@ -800,6 +836,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "UI Font Size", description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.", field: Box::new(SettingField { + json_path: Some("agent_ui_font_size"), pick: |settings_content| { settings_content .theme @@ -816,6 +853,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Buffer Font Size", description: "Font size for user messages text in the agent panel.", field: Box::new(SettingField { + json_path: Some("agent_buffer_font_size"), pick: |settings_content| { settings_content .theme @@ -836,6 +874,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Multi Cursor Modifier", description: "Modifier key for adding multiple cursors.", field: Box::new(SettingField { + json_path: Some("multi_cursor_modifier"), pick: |settings_content| { settings_content.editor.multi_cursor_modifier.as_ref() }, @@ -851,6 +890,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Cursor Blink", description: "Whether the cursor blinks in the editor.", field: Box::new(SettingField { + json_path: Some("cursor_blink"), pick: |settings_content| settings_content.editor.cursor_blink.as_ref(), write: |settings_content, value|{ settings_content.editor.cursor_blink = value;}, }), @@ -861,6 +901,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Cursor Shape", description: "Cursor shape for the editor.", field: Box::new(SettingField { + json_path: Some("cursor_shape"), pick: |settings_content| settings_content.editor.cursor_shape.as_ref(), write: |settings_content, value|{ settings_content.editor.cursor_shape = value;}, }), @@ -871,6 +912,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Hide Mouse", description: "When to hide the mouse cursor.", field: Box::new(SettingField { + json_path: Some("hide_mouse"), pick: |settings_content| settings_content.editor.hide_mouse.as_ref(), write: |settings_content, value|{ settings_content.editor.hide_mouse = value;}, }), @@ -882,6 +924,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Unnecessary Code Fade", description: "How much to fade out unused code (0.0 - 0.9).", field: Box::new(SettingField { + json_path: Some("unnecessary_code_fade"), pick: |settings_content| { settings_content.theme.unnecessary_code_fade.as_ref() }, @@ -897,6 +940,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Current Line Highlight", description: "How to highlight the current line.", field: Box::new(SettingField { + json_path: Some("current_line_highlight"), pick: |settings_content| { settings_content.editor.current_line_highlight.as_ref() }, @@ -912,6 +956,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Selection Highlight", description: "Highlight all occurrences of selected text.", field: Box::new(SettingField { + json_path: Some("selection_highlight"), pick: |settings_content| { settings_content.editor.selection_highlight.as_ref() }, @@ -927,6 +972,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Rounded Selection", description: "Whether the text selection should have rounded corners.", field: Box::new(SettingField { + json_path: Some("rounded_selection"), pick: |settings_content| settings_content.editor.rounded_selection.as_ref(), write: |settings_content, value|{ settings_content.editor.rounded_selection = value;}, }), @@ -937,6 +983,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Minimum Contrast For Highlights", description: "The minimum APCA perceptual contrast to maintain when rendering text over highlight backgrounds.", field: Box::new(SettingField { + json_path: Some("minimum_contrast_for_highlights"), pick: |settings_content| { settings_content .editor @@ -956,6 +1003,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Wrap Guides", description: "Show wrap guides (vertical rulers).", field: Box::new(SettingField { + json_path: Some("show_wrap_guides"), pick: |settings_content| { settings_content .project @@ -982,6 +1030,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Character counts at which to show wrap guides.", field: Box::new( SettingField { + json_path: Some("wrap_guides"), pick: |settings_content| { settings_content .project @@ -992,7 +1041,6 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }, write: |settings_content, value| { settings_content.project.all_languages.defaults.wrap_guides = value; - }, } .unimplemented(), @@ -1010,6 +1058,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Base Keymap", description: "The name of a base set of key bindings to use.", field: Box::new(SettingField { + json_path: Some("base_keymap"), pick: |settings_content| settings_content.base_keymap.as_ref(), write: |settings_content, value| { settings_content.base_keymap = value; @@ -1028,6 +1077,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Vim Mode", description: "Enable Vim mode and key bindings.", field: Box::new(SettingField { + json_path: Some("vim_mode"), pick: |settings_content| settings_content.vim_mode.as_ref(), write: |settings_content, value| { settings_content.vim_mode = value; @@ -1040,6 +1090,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Helix Mode", description: "Enable Helix mode and key bindings.", field: Box::new(SettingField { + json_path: Some("helix_mode"), pick: |settings_content| settings_content.helix_mode.as_ref(), write: |settings_content, value| { settings_content.helix_mode = value; @@ -1061,6 +1112,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Save Mode", description: "When to auto save buffer changes.", field: Box::new(SettingField { + json_path: Some("autosave$"), pick: |settings_content| { Some(&dynamic_variants::()[ settings_content @@ -1110,6 +1162,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Delay (milliseconds)", description: "Save after inactivity period (in milliseconds).", field: Box::new(SettingField { + json_path: Some("autosave.after_delay.milliseconds"), pick: |settings_content| { match settings_content.workspace.autosave.as_ref() { Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => Some(milliseconds), @@ -1141,6 +1194,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Double Click In Multibuffer", description: "What to do when multibuffer is double-clicked in some of its excerpts.", field: Box::new(SettingField { + json_path: Some("double_click_in_multibuffer"), pick: |settings_content| { settings_content.editor.double_click_in_multibuffer.as_ref() }, @@ -1155,6 +1209,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Expand Excerpt Lines", description: "How many lines to expand the multibuffer excerpts by default.", field: Box::new(SettingField { + json_path: Some("expand_excerpt_lines"), pick: |settings_content| { settings_content.editor.expand_excerpt_lines.as_ref() }, @@ -1169,6 +1224,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Excerpt Context Lines", description: "How many lines of context to provide in multibuffer excerpts by default.", field: Box::new(SettingField { + json_path: Some("excerpt_context_lines"), pick: |settings_content| { settings_content.editor.excerpt_context_lines.as_ref() }, @@ -1183,6 +1239,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Expand Outlines With Depth", description: "Default depth to expand outline items in the current file.", field: Box::new(SettingField { + json_path: Some("outline_panel.expand_outlines_with_depth"), pick: |settings_content| { settings_content .outline_panel @@ -1206,6 +1263,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Scroll Beyond Last Line", description: "Whether the editor will scroll beyond the last line.", field: Box::new(SettingField { + json_path: Some("scroll_beyond_last_line"), pick: |settings_content| { settings_content.editor.scroll_beyond_last_line.as_ref() }, @@ -1220,6 +1278,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Vertical Scroll Margin", description: "The number of lines to keep above/below the cursor when auto-scrolling.", field: Box::new(SettingField { + json_path: Some("vertical_scroll_margin"), pick: |settings_content| { settings_content.editor.vertical_scroll_margin.as_ref() }, @@ -1234,6 +1293,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Horizontal Scroll Margin", description: "The number of characters to keep on either side when scrolling with the mouse.", field: Box::new(SettingField { + json_path: Some("horizontal_scroll_margin"), pick: |settings_content| { settings_content.editor.horizontal_scroll_margin.as_ref() }, @@ -1248,6 +1308,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Scroll Sensitivity", description: "Scroll sensitivity multiplier for both horizontal and vertical scrolling.", field: Box::new(SettingField { + json_path: Some("scroll_sensitivity"), pick: |settings_content| { settings_content.editor.scroll_sensitivity.as_ref() }, @@ -1262,6 +1323,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Fast Scroll Sensitivity", description: "Fast scroll sensitivity multiplier for both horizontal and vertical scrolling.", field: Box::new(SettingField { + json_path: Some("fast_scroll_sensitivity"), pick: |settings_content| { settings_content.editor.fast_scroll_sensitivity.as_ref() }, @@ -1276,6 +1338,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Autoscroll On Clicks", description: "Whether to scroll when clicking near the edge of the visible text area.", field: Box::new(SettingField { + json_path: Some("autoscroll_on_clicks"), pick: |settings_content| { settings_content.editor.autoscroll_on_clicks.as_ref() }, @@ -1291,6 +1354,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Signature Help", description: "Automatically show a signature help pop-up.", field: Box::new(SettingField { + json_path: Some("auto_signature_help"), pick: |settings_content| { settings_content.editor.auto_signature_help.as_ref() }, @@ -1305,6 +1369,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Signature Help After Edits", description: "Show the signature help pop-up after completions or bracket pairs are inserted.", field: Box::new(SettingField { + json_path: Some("show_signature_help_after_edits"), pick: |settings_content| { settings_content .editor @@ -1322,6 +1387,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Snippet Sort Order", description: "Determines how snippets are sorted relative to other completion items.", field: Box::new(SettingField { + json_path: Some("snippet_sort_order"), pick: |settings_content| { settings_content.editor.snippet_sort_order.as_ref() }, @@ -1337,6 +1403,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enabled", description: "Show the informational hover box when moving the mouse over symbols in the editor.", field: Box::new(SettingField { + json_path: Some("hover_popover_enabled"), pick: |settings_content| { settings_content.editor.hover_popover_enabled.as_ref() }, @@ -1352,6 +1419,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Delay", description: "Time to wait in milliseconds before showing the informational hover box.", field: Box::new(SettingField { + json_path: Some("hover_popover_enabled"), pick: |settings_content| { settings_content.editor.hover_popover_delay.as_ref() }, @@ -1367,6 +1435,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enabled", description: "Enable drag and drop selection.", field: Box::new(SettingField { + json_path: Some("drag_and_drop_selection.enabled"), pick: |settings_content| { settings_content .editor @@ -1389,6 +1458,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Delay", description: "Delay in milliseconds before drag and drop selection starts.", field: Box::new(SettingField { + json_path: Some("drag_and_drop_selection.delay"), pick: |settings_content| { settings_content .editor @@ -1412,6 +1482,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Line Numbers", description: "Show line numbers in the gutter.", field: Box::new(SettingField { + json_path: Some("gutter.line_numbers"), pick: |settings_content| { settings_content .editor @@ -1434,6 +1505,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Relative Line Numbers", description: "Whether the line numbers in the editor's gutter are relative or not.", field: Box::new(SettingField { + json_path: Some("relative_line_numbers"), pick: |settings_content| { settings_content.editor.relative_line_numbers.as_ref() }, @@ -1448,6 +1520,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Runnables", description: "Show runnable buttons in the gutter.", field: Box::new(SettingField { + json_path: Some("gutter.runnables"), pick: |settings_content| { settings_content .editor @@ -1470,6 +1543,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Breakpoints", description: "Show breakpoints in the gutter.", field: Box::new(SettingField { + json_path: Some("gutter.breakpoints"), pick: |settings_content| { settings_content .editor @@ -1492,6 +1566,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Folds", description: "Show code folding controls in the gutter.", field: Box::new(SettingField { + json_path: Some("gutter.folds"), pick: |settings_content| { settings_content .editor @@ -1511,6 +1586,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Min Line Number Digits", description: "Minimum number of characters to reserve space for in the gutter.", field: Box::new(SettingField { + json_path: Some("gutter.min_line_number_digits"), pick: |settings_content| { settings_content .editor @@ -1533,6 +1609,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Inline Code Actions", description: "Show code action button at start of buffer line.", field: Box::new(SettingField { + json_path: Some("inline_code_actions"), pick: |settings_content| { settings_content.editor.inline_code_actions.as_ref() }, @@ -1548,6 +1625,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show", description: "When to show the scrollbar in the editor.", field: Box::new(SettingField { + json_path: Some("scrollbar"), pick: |settings_content| { settings_content.editor.scrollbar.as_ref()?.show.as_ref() }, @@ -1566,6 +1644,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Cursors", description: "Show cursor positions in the scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.cursors"), pick: |settings_content| { settings_content.editor.scrollbar.as_ref()?.cursors.as_ref() }, @@ -1584,6 +1663,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Diff", description: "Show Git diff indicators in the scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.git_diff"), pick: |settings_content| { settings_content .editor @@ -1607,6 +1687,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Search Results", description: "Show buffer search result indicators in the scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.search_results"), pick: |settings_content| { settings_content .editor @@ -1630,6 +1711,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Selected Text", description: "Show selected text occurrences in the scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.selected_text"), pick: |settings_content| { settings_content .editor @@ -1653,6 +1735,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Selected Symbol", description: "Show selected symbol occurrences in the scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.selected_symbol"), pick: |settings_content| { settings_content .editor @@ -1676,6 +1759,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Diagnostics", description: "Which diagnostic indicators to show in the scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.diagnostics"), pick: |settings_content| { settings_content .editor @@ -1699,6 +1783,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Horizontal Scrollbar", description: "When false, forcefully disables the horizontal scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.axes.horizontal"), pick: |settings_content| { settings_content .editor @@ -1726,6 +1811,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Vertical Scrollbar", description: "When false, forcefully disables the vertical scrollbar.", field: Box::new(SettingField { + json_path: Some("scrollbar.axes.vertical"), pick: |settings_content| { settings_content .editor @@ -1754,6 +1840,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show", description: "When to show the minimap in the editor.", field: Box::new(SettingField { + json_path: Some("minimap.show"), pick: |settings_content| { settings_content.editor.minimap.as_ref()?.show.as_ref() }, @@ -1769,6 +1856,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Display In", description: "Where to show the minimap in the editor.", field: Box::new(SettingField { + json_path: Some("minimap.display_in"), pick: |settings_content| { settings_content .editor @@ -1792,6 +1880,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Thumb", description: "When to show the minimap thumb.", field: Box::new(SettingField { + json_path: Some("minimap.thumb"), pick: |settings_content| { settings_content.editor.minimap.as_ref()?.thumb.as_ref() }, @@ -1810,6 +1899,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Thumb Border", description: "Border style for the minimap's scrollbar thumb.", field: Box::new(SettingField { + json_path: Some("minimap.thumb_border"), pick: |settings_content| { settings_content .editor @@ -1833,6 +1923,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Current Line Highlight", description: "How to highlight the current line in the minimap.", field: Box::new(SettingField { + json_path: Some("minimap.current_line_highlight"), pick: |settings_content| { settings_content .editor @@ -1856,6 +1947,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Max Width Columns", description: "Maximum number of columns to display in the minimap.", field: Box::new(SettingField { + json_path: Some("minimap.max_width_columns"), pick: |settings_content| { settings_content .editor @@ -1880,6 +1972,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Breadcrumbs", description: "Show breadcrumbs.", field: Box::new(SettingField { + json_path: Some("toolbar.breadcrumbs"), pick: |settings_content| { settings_content .editor @@ -1903,6 +1996,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Quick Actions", description: "Show quick action buttons (e.g., search, selection, editor controls, etc.).", field: Box::new(SettingField { + json_path: Some("toolbar.quick_actions"), pick: |settings_content| { settings_content .editor @@ -1926,6 +2020,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Selections Menu", description: "Show the selections menu in the editor toolbar.", field: Box::new(SettingField { + json_path: Some("toolbar.selections_menu"), pick: |settings_content| { settings_content .editor @@ -1949,6 +2044,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Agent Review", description: "Show agent review buttons in the editor toolbar.", field: Box::new(SettingField { + json_path: Some("toolbar.agent_review"), pick: |settings_content| { settings_content .editor @@ -1972,6 +2068,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Code Actions", description: "Show code action buttons in the editor toolbar.", field: Box::new(SettingField { + json_path: Some("toolbar.code_actions"), pick: |settings_content| { settings_content .editor @@ -2008,6 +2105,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "A mapping from languages to files and file extensions that should be treated as that language.", field: Box::new( SettingField { + json_path: Some("file_types"), pick: |settings_content| { settings_content.project.all_languages.file_types.as_ref() }, @@ -2029,6 +2127,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Max Severity", description: "Which level to use to filter out diagnostics displayed in the editor.", field: Box::new(SettingField { + json_path: Some("diagnostics_max_severity"), pick: |settings_content| settings_content.editor.diagnostics_max_severity.as_ref(), write: |settings_content, value| { settings_content.editor.diagnostics_max_severity = value; @@ -2042,6 +2141,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Include Warnings", description: "Whether to show warnings or not by default.", field: Box::new(SettingField { + json_path: Some("diagnostics.include_warnings"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.include_warnings.as_ref() }, @@ -2062,6 +2162,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enabled", description: "Whether to show diagnostics inline or not.", field: Box::new(SettingField { + json_path: Some("diagnostics.inline.enabled"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.inline.as_ref()?.enabled.as_ref() }, @@ -2083,6 +2184,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Update Debounce", description: "The delay in milliseconds to show inline diagnostics after the last diagnostic update.", field: Box::new(SettingField { + json_path: Some("diagnostics.inline.update_debounce_ms"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.inline.as_ref()?.update_debounce_ms.as_ref() }, @@ -2104,6 +2206,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Padding", description: "The amount of padding between the end of the source line and the start of the inline diagnostic.", field: Box::new(SettingField { + json_path: Some("diagnostics.inline.padding"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.inline.as_ref()?.padding.as_ref() }, @@ -2125,6 +2228,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Minimum Column", description: "The minimum column at which to display inline diagnostics.", field: Box::new(SettingField { + json_path: Some("diagnostics.inline.min_column"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.inline.as_ref()?.min_column.as_ref() }, @@ -2147,6 +2251,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enabled", description: "Whether to pull for language server-powered diagnostics or not.", field: Box::new(SettingField { + json_path: Some("diagnostics.lsp_pull_diagnostics.enabled"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.lsp_pull_diagnostics.as_ref()?.enabled.as_ref() }, @@ -2169,6 +2274,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Debounce", description: "Minimum time to wait before pulling diagnostics from the language server(s).", field: Box::new(SettingField { + json_path: Some("diagnostics.lsp_pull_diagnostics.debounce_ms"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.lsp_pull_diagnostics.as_ref()?.debounce_ms.as_ref() }, @@ -2191,10 +2297,10 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Debounce", description: "The debounce delay before querying highlights from the language.", field: Box::new(SettingField { + json_path: Some("lsp_highlight_debounce"), pick: |settings_content| settings_content.editor.lsp_highlight_debounce.as_ref(), write: |settings_content, value| { settings_content.editor.lsp_highlight_debounce = value; - }, }), metadata: None, @@ -2234,6 +2340,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Whole Word", description: "Search for whole words by default.", field: Box::new(SettingField { + json_path: Some("search.whole_word"), pick: |settings_content| { settings_content.editor.search.as_ref()?.whole_word.as_ref() }, @@ -2252,6 +2359,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Case Sensitive", description: "Search case-sensitively by default.", field: Box::new(SettingField { + json_path: Some("search.case_sensitive"), pick: |settings_content| { settings_content .editor @@ -2275,6 +2383,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Use Smartcase Search", description: "Whether to automatically enable case-sensitive search based on the search query.", field: Box::new(SettingField { + json_path: Some("use_smartcase_search"), pick: |settings_content| { settings_content.editor.use_smartcase_search.as_ref() }, @@ -2289,6 +2398,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Include Ignored", description: "Include ignored files in search results by default.", field: Box::new(SettingField { + json_path: Some("search.include_ignored"), pick: |settings_content| { settings_content .editor @@ -2312,6 +2422,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Regex", description: "Use regex search by default.", field: Box::new(SettingField { + json_path: Some("search.regex"), pick: |settings_content| { settings_content.editor.search.as_ref()?.regex.as_ref() }, @@ -2326,6 +2437,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Search Wrap", description: "Whether the editor search results will loop.", field: Box::new(SettingField { + json_path: Some("search_wrap"), pick: |settings_content| settings_content.editor.search_wrap.as_ref(), write: |settings_content, value| { settings_content.editor.search_wrap = value; @@ -2338,6 +2450,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Seed Search Query From Cursor", description: "When to populate a new search's query based on the text under the cursor.", field: Box::new(SettingField { + json_path: Some("seed_search_query_from_cursor"), pick: |settings_content| { settings_content .editor @@ -2358,6 +2471,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Use gitignored files when searching.", field: Box::new( SettingField { + json_path: Some("file_finder.include_ignored"), pick: |settings_content| { settings_content .file_finder @@ -2380,6 +2494,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "File Icons", description: "Show file icons in the file finder.", field: Box::new(SettingField { + json_path: Some("file_finder.file_icons"), pick: |settings_content| { settings_content.file_finder.as_ref()?.file_icons.as_ref() }, @@ -2397,6 +2512,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Modal Max Width", description: "Determines how much space the file finder can take up in relation to the available window width.", field: Box::new(SettingField { + json_path: Some("file_finder.modal_max_width"), pick: |settings_content| { settings_content .file_finder @@ -2418,6 +2534,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Skip Focus For Active In Search", description: "Whether the file finder should skip focus for the active file in search results.", field: Box::new(SettingField { + json_path: Some("file_finder.skip_focus_for_active_in_search"), pick: |settings_content| { settings_content .file_finder @@ -2439,6 +2556,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Status", description: "Show the Git status in the file finder.", field: Box::new(SettingField { + json_path: Some("file_finder.git_status"), pick: |settings_content| { settings_content.file_finder.as_ref()?.git_status.as_ref() }, @@ -2458,6 +2576,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Files or globs of files that will be excluded by Zed entirely. They will be skipped during file scans, file searches, and not be displayed in the project file tree. Takes precedence over \"File Scan Inclusions\"", field: Box::new( SettingField { + json_path: Some("file_scan_exclusions"), pick: |settings_content| { settings_content .project @@ -2479,15 +2598,16 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Files or globs of files that will be included by Zed, even when ignored by git. This is useful for files that are not tracked by git, but are still important to your project. Note that globs that are overly broad can slow down Zed's file scanning. \"File Scan Exclusions\" takes precedence over these inclusions", field: Box::new( SettingField { + json_path: Some("file_scan_inclusions"), pick: |settings_content| { settings_content .project .worktree - .file_scan_exclusions + .file_scan_inclusions .as_ref() }, write: |settings_content, value| { - settings_content.project.worktree.file_scan_exclusions = value; + settings_content.project.worktree.file_scan_inclusions = value; }, } .unimplemented(), @@ -2499,6 +2619,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Restore File State", description: "Restore previous file state when reopening.", field: Box::new(SettingField { + json_path: Some("restore_on_file_reopen"), pick: |settings_content| { settings_content.workspace.restore_on_file_reopen.as_ref() }, @@ -2513,6 +2634,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Close on File Delete", description: "Automatically close files that have been deleted.", field: Box::new(SettingField { + json_path: Some("close_on_file_delete"), pick: |settings_content| { settings_content.workspace.close_on_file_delete.as_ref() }, @@ -2533,6 +2655,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Project Panel Button", description: "Show the project panel button in the status bar.", field: Box::new(SettingField { + json_path: Some("project_panel.button"), pick: |settings_content| { settings_content.project_panel.as_ref()?.button.as_ref() }, @@ -2550,6 +2673,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Active Language Button", description: "Show the active language button in the status bar.", field: Box::new(SettingField { + json_path: Some("status_bar.active_language_button"), pick: |settings_content| { settings_content .status_bar @@ -2571,6 +2695,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Cursor Position Button", description: "Show the cursor position button in the status bar.", field: Box::new(SettingField { + json_path: Some("status_bar.cursor_position_button"), pick: |settings_content| { settings_content .status_bar @@ -2592,6 +2717,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Terminal Button", description: "Show the terminal button in the status bar.", field: Box::new(SettingField { + json_path: Some("terminal.button"), pick: |settings_content| { settings_content.terminal.as_ref()?.button.as_ref() }, @@ -2606,6 +2732,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Diagnostics Button", description: "Show the project diagnostics button in the status bar.", field: Box::new(SettingField { + json_path: Some("diagnostics.button"), pick: |settings_content| { settings_content.diagnostics.as_ref()?.button.as_ref() }, @@ -2620,6 +2747,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Project Search Button", description: "Show the project search button in the status bar.", field: Box::new(SettingField { + json_path: Some("search.button"), pick: |settings_content| { settings_content.editor.search.as_ref()?.button.as_ref() }, @@ -2638,6 +2766,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Debugger Button", description: "Show the debugger button in the status bar.", field: Box::new(SettingField { + json_path: Some("debugger.button"), pick: |settings_content| { settings_content.debugger.as_ref()?.button.as_ref() }, @@ -2653,6 +2782,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Branch Icon", description: "Show the branch icon beside branch switcher in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_branch_icon"), pick: |settings_content| { settings_content .title_bar @@ -2674,6 +2804,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Branch Name", description: "Show the branch name button in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_branch_name"), pick: |settings_content| { settings_content .title_bar @@ -2695,6 +2826,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Project Items", description: "Show the project host and name in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_project_items"), pick: |settings_content| { settings_content .title_bar @@ -2716,6 +2848,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Onboarding Banner", description: "Show banners announcing new features in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_onboarding_banner"), pick: |settings_content| { settings_content .title_bar @@ -2737,6 +2870,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show User Picture", description: "Show user picture in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_user_picture"), pick: |settings_content| { settings_content .title_bar @@ -2758,6 +2892,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Sign In", description: "Show the sign in button in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_sign_in"), pick: |settings_content| { settings_content.title_bar.as_ref()?.show_sign_in.as_ref() }, @@ -2775,6 +2910,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Menus", description: "Show the menus in the titlebar.", field: Box::new(SettingField { + json_path: Some("title_bar.show_menus"), pick: |settings_content| { settings_content.title_bar.as_ref()?.show_menus.as_ref() }, @@ -2793,6 +2929,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Tab Bar", description: "Show the tab bar in the editor.", field: Box::new(SettingField { + json_path: Some("tab_bar.show"), pick: |settings_content| settings_content.tab_bar.as_ref()?.show.as_ref(), write: |settings_content, value| { settings_content.tab_bar.get_or_insert_default().show = value; @@ -2805,6 +2942,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Git Status In Tabs", description: "Show the Git file status on a tab item.", field: Box::new(SettingField { + json_path: Some("tabs.git_status"), pick: |settings_content| { settings_content.tabs.as_ref()?.git_status.as_ref() }, @@ -2819,6 +2957,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show File Icons In Tabs", description: "Show the file icon for a tab.", field: Box::new(SettingField { + json_path: Some("tabs.file_icons"), pick: |settings_content| { settings_content.tabs.as_ref()?.file_icons.as_ref() }, @@ -2833,6 +2972,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Tab Close Position", description: "Position of the close button in a tab.", field: Box::new(SettingField { + json_path: Some("tabs.close_position"), pick: |settings_content| { settings_content.tabs.as_ref()?.close_position.as_ref() }, @@ -2851,6 +2991,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { // is complex, so I'm going to come back to this later field: Box::new( SettingField { + json_path: Some("max_tabs"), pick: |settings_content| settings_content.workspace.max_tabs.as_ref(), write: |settings_content, value| { settings_content.workspace.max_tabs = value; @@ -2864,6 +3005,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Navigation History Buttons", description: "Show the navigation history buttons in the tab bar.", field: Box::new(SettingField { + json_path: Some("tab_bar.show_nav_history_buttons"), pick: |settings_content| { settings_content .tab_bar @@ -2886,6 +3028,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Activate On Close", description: "What to do after closing the current tab.", field: Box::new(SettingField { + json_path: Some("tabs.activate_on_close"), pick: |settings_content| { settings_content.tabs.as_ref()?.activate_on_close.as_ref() }, @@ -2903,6 +3046,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Tab Show Diagnostics", description: "Which files containing diagnostic errors/warnings to mark in the tabs.", field: Box::new(SettingField { + json_path: Some("tabs.show_diagnostics"), pick: |settings_content| { settings_content.tabs.as_ref()?.show_diagnostics.as_ref() }, @@ -2920,6 +3064,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Close Button", description: "Controls the appearance behavior of the tab's close button.", field: Box::new(SettingField { + json_path: Some("tabs.show_close_button"), pick: |settings_content| { settings_content.tabs.as_ref()?.show_close_button.as_ref() }, @@ -2938,6 +3083,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Preview Tabs Enabled", description: "Show opened editors as Preview tabs.", field: Box::new(SettingField { + json_path: Some("preview_tabs.enabled"), pick: |settings_content| { settings_content.preview_tabs.as_ref()?.enabled.as_ref() }, @@ -2955,6 +3101,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enable Preview From File Finder", description: "Whether to open tabs in Preview mode when selected from the file finder.", field: Box::new(SettingField { + json_path: Some("preview_tabs.enable_preview_from_file_finder"), pick: |settings_content| { settings_content .preview_tabs @@ -2976,6 +3123,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enable Preview From Code Navigation", description: "Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.", field: Box::new(SettingField { + json_path: Some("preview_tabs.enable_preview_from_code_navigation"), pick: |settings_content| { settings_content .preview_tabs @@ -2998,6 +3146,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Bottom Dock Layout", description: "Layout mode for the bottom dock.", field: Box::new(SettingField { + json_path: Some("bottom_dock_layout"), pick: |settings_content| { settings_content.workspace.bottom_dock_layout.as_ref() }, @@ -3013,6 +3162,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Centered Layout Left Padding", description: "Left padding for centered layout.", field: Box::new(SettingField { + json_path: Some("centered_layout.left_padding"), pick: |settings_content| { settings_content .workspace @@ -3036,6 +3186,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Centered Layout Right Padding", description: "Right padding for centered layout.", field: Box::new(SettingField { + json_path: Some("centered_layout.right_padding"), pick: |settings_content| { settings_content .workspace @@ -3060,6 +3211,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Use System Window Tabs", description: "(macOS only) whether to allow Windows to tab together.", field: Box::new(SettingField { + json_path: Some("use_system_window_tabs"), pick: |settings_content| { settings_content.workspace.use_system_window_tabs.as_ref() }, @@ -3075,6 +3227,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Inactive Opacity", description: "Opacity of inactive panels (0.0 - 1.0).", field: Box::new(SettingField { + json_path: Some("active_pane_modifiers.inactive_opacity"), pick: |settings_content| { settings_content .workspace @@ -3098,6 +3251,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Border Size", description: "Size of the border surrounding the active pane.", field: Box::new(SettingField { + json_path: Some("active_pane_modifiers.border_size"), pick: |settings_content| { settings_content .workspace @@ -3121,6 +3275,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Zoomed Padding", description: "Show padding for zoomed panes.", field: Box::new(SettingField { + json_path: Some("zoomed_padding"), pick: |settings_content| settings_content.workspace.zoomed_padding.as_ref(), write: |settings_content, value| { settings_content.workspace.zoomed_padding = value; @@ -3134,6 +3289,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Vertical Split Direction", description: "Direction to split vertically.", field: Box::new(SettingField { + json_path: Some("pane_split_direction_vertical"), pick: |settings_content| { settings_content .workspace @@ -3151,6 +3307,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Horizontal Split Direction", description: "Direction to split horizontally.", field: Box::new(SettingField { + json_path: Some("pane_split_direction_horizontal"), pick: |settings_content| { settings_content .workspace @@ -3174,6 +3331,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Project Panel Dock", description: "Where to dock the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.dock"), pick: |settings_content| { settings_content.project_panel.as_ref()?.dock.as_ref() }, @@ -3188,6 +3346,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Project Panel Default Width", description: "Default width of the project panel in pixels.", field: Box::new(SettingField { + json_path: Some("project_panel.default_width"), pick: |settings_content| { settings_content .project_panel @@ -3209,6 +3368,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Hide .gitignore", description: "Whether to hide the gitignore entries in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.hide_gitignore"), pick: |settings_content| { settings_content .project_panel @@ -3230,6 +3390,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Entry Spacing", description: "Spacing between worktree entries in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.entry_spacing"), pick: |settings_content| { settings_content .project_panel @@ -3251,6 +3412,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "File Icons", description: "Show file icons in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.file_icons"), pick: |settings_content| { settings_content.project_panel.as_ref()?.file_icons.as_ref() }, @@ -3268,6 +3430,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Folder Icons", description: "Whether to show folder icons or chevrons for directories in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.folder_icons"), pick: |settings_content| { settings_content .project_panel @@ -3289,6 +3452,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Status", description: "Show the Git status in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.git_status"), pick: |settings_content| { settings_content.project_panel.as_ref()?.git_status.as_ref() }, @@ -3306,6 +3470,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Indent Size", description: "Amount of indentation for nested items.", field: Box::new(SettingField { + json_path: Some("project_panel.indent_size"), pick: |settings_content| { settings_content .project_panel @@ -3327,6 +3492,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Reveal Entries", description: "Whether to reveal entries in the project panel automatically when a corresponding project entry becomes active.", field: Box::new(SettingField { + json_path: Some("project_panel.auto_reveal_entries"), pick: |settings_content| { settings_content .project_panel @@ -3348,6 +3514,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Starts Open", description: "Whether the project panel should open on startup.", field: Box::new(SettingField { + json_path: Some("project_panel.starts_open"), pick: |settings_content| { settings_content .project_panel @@ -3369,6 +3536,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Fold Directories", description: "Whether to fold directories automatically and show compact folders when a directory has only one subdirectory inside.", field: Box::new(SettingField { + json_path: Some("project_panel.auto_fold_dirs"), pick: |settings_content| { settings_content .project_panel @@ -3390,6 +3558,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Scrollbar", description: "Show the scrollbar in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.scrollbar.show"), pick: |settings_content| { show_scrollbar_or_editor(settings_content, |settings_content| { settings_content @@ -3417,6 +3586,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Diagnostics", description: "Which files containing diagnostic errors/warnings to mark in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.show_diagnostics"), pick: |settings_content| { settings_content .project_panel @@ -3438,6 +3608,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Sticky Scroll", description: "Whether to stick parent directories at top of the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.sticky_scroll"), pick: |settings_content| { settings_content .project_panel @@ -3461,6 +3632,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Show indent guides in the project panel.", field: Box::new( SettingField { + json_path: Some("project_panel.indent_guides.show"), pick: |settings_content| { settings_content .project_panel @@ -3486,6 +3658,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Drag and Drop", description: "Whether to enable drag-and-drop operations in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.drag_and_drop"), pick: |settings_content| { settings_content .project_panel @@ -3507,6 +3680,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Hide Root", description: "Whether to hide the root entry when only one folder is open in the window.", field: Box::new(SettingField { + json_path: Some("project_panel.drag_and_drop"), pick: |settings_content| { settings_content.project_panel.as_ref()?.hide_root.as_ref() }, @@ -3524,6 +3698,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Hide Hidden", description: "Whether to hide the hidden entries in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.hide_hidden"), pick: |settings_content| { settings_content .project_panel @@ -3545,6 +3720,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Open File on Paste", description: "Whether to automatically open files when pasting them in the project panel.", field: Box::new(SettingField { + json_path: Some("project_panel.open_file_on_paste"), pick: |settings_content| { settings_content .project_panel @@ -3567,6 +3743,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Terminal Dock", description: "Where to dock the terminal panel.", field: Box::new(SettingField { + json_path: Some("terminal.dock"), pick: |settings_content| settings_content.terminal.as_ref()?.dock.as_ref(), write: |settings_content, value| { settings_content.terminal.get_or_insert_default().dock = value; @@ -3580,6 +3757,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Outline Panel Button", description: "Show the outline panel button in the status bar.", field: Box::new(SettingField { + json_path: Some("outline_panel.button"), pick: |settings_content| { settings_content.outline_panel.as_ref()?.button.as_ref() }, @@ -3597,6 +3775,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Outline Panel Dock", description: "Where to dock the outline panel.", field: Box::new(SettingField { + json_path: Some("outline_panel.dock"), pick: |settings_content| { settings_content.outline_panel.as_ref()?.dock.as_ref() }, @@ -3611,6 +3790,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Outline Panel Default Width", description: "Default width of the outline panel in pixels.", field: Box::new(SettingField { + json_path: Some("outline_panel.default_width"), pick: |settings_content| { settings_content .outline_panel @@ -3632,6 +3812,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "File Icons", description: "Show file icons in the outline panel.", field: Box::new(SettingField { + json_path: Some("outline_panel.file_icons"), pick: |settings_content| { settings_content.outline_panel.as_ref()?.file_icons.as_ref() }, @@ -3649,6 +3830,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Folder Icons", description: "Whether to show folder icons or chevrons for directories in the outline panel.", field: Box::new(SettingField { + json_path: Some("outline_panel.folder_icons"), pick: |settings_content| { settings_content .outline_panel @@ -3670,6 +3852,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Status", description: "Show the Git status in the outline panel.", field: Box::new(SettingField { + json_path: Some("outline_panel.git_status"), pick: |settings_content| { settings_content.outline_panel.as_ref()?.git_status.as_ref() }, @@ -3687,6 +3870,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Indent Size", description: "Amount of indentation for nested items.", field: Box::new(SettingField { + json_path: Some("outline_panel.indent_size"), pick: |settings_content| { settings_content .outline_panel @@ -3708,6 +3892,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Reveal Entries", description: "Whether to reveal when a corresponding outline entry becomes active.", field: Box::new(SettingField { + json_path: Some("outline_panel.auto_reveal_entries"), pick: |settings_content| { settings_content .outline_panel @@ -3729,6 +3914,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Fold Directories", description: "Whether to fold directories automatically when a directory contains only one subdirectory.", field: Box::new(SettingField { + json_path: Some("outline_panel.auto_fold_dirs"), pick: |settings_content| { settings_content .outline_panel @@ -3752,6 +3938,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "When to show indent guides in the outline panel.", field: Box::new( SettingField { + json_path: Some("outline_panel.indent_guides.show"), pick: |settings_content| { settings_content .outline_panel @@ -3778,6 +3965,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Panel Button", description: "Show the Git panel button in the status bar.", field: Box::new(SettingField { + json_path: Some("git_panel.button"), pick: |settings_content| { settings_content.git_panel.as_ref()?.button.as_ref() }, @@ -3792,6 +3980,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Panel Dock", description: "Where to dock the Git panel.", field: Box::new(SettingField { + json_path: Some("git_panel.dock"), pick: |settings_content| settings_content.git_panel.as_ref()?.dock.as_ref(), write: |settings_content, value| { settings_content.git_panel.get_or_insert_default().dock = value; @@ -3804,6 +3993,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Panel Default Width", description: "Default width of the Git panel in pixels.", field: Box::new(SettingField { + json_path: Some("git_panel.default_width"), pick: |settings_content| { settings_content.git_panel.as_ref()?.default_width.as_ref() }, @@ -3821,6 +4011,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Git Panel Status Style", description: "How entry statuses are displayed.", field: Box::new(SettingField { + json_path: Some("git_panel.status_style"), pick: |settings_content| { settings_content.git_panel.as_ref()?.status_style.as_ref() }, @@ -3838,6 +4029,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Fallback Branch Name", description: "Default branch name will be when init.defaultbranch is not set in Git.", field: Box::new(SettingField { + json_path: Some("git_panel.fallback_branch_name"), pick: |settings_content| { settings_content .git_panel @@ -3859,6 +4051,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Sort By Path", description: "Enable to sort entries in the panel by path, disable to sort by status.", field: Box::new(SettingField { + json_path: Some("git_panel.sort_by_path"), pick: |settings_content| { settings_content.git_panel.as_ref()?.sort_by_path.as_ref() }, @@ -3876,6 +4069,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Collapse Untracked Diff", description: "Whether to collapse untracked files in the diff panel.", field: Box::new(SettingField { + json_path: Some("git_panel.collapse_untracked_diff"), pick: |settings_content| { settings_content .git_panel @@ -3897,6 +4091,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Scroll Bar", description: "How and when the scrollbar should be displayed.", field: Box::new(SettingField { + json_path: Some("git_panel.scrollbar.show"), pick: |settings_content| { show_scrollbar_or_editor(settings_content, |settings_content| { settings_content @@ -3925,6 +4120,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Debugger Panel Dock", description: "The dock position of the debug panel.", field: Box::new(SettingField { + json_path: Some("debugger.dock"), pick: |settings_content| settings_content.debugger.as_ref()?.dock.as_ref(), write: |settings_content, value| { settings_content.debugger.get_or_insert_default().dock = value; @@ -3938,6 +4134,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Notification Panel Button", description: "Show the notification panel button in the status bar.", field: Box::new(SettingField { + json_path: Some("notification_panel.button"), pick: |settings_content| { settings_content .notification_panel @@ -3959,6 +4156,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Notification Panel Dock", description: "Where to dock the notification panel.", field: Box::new(SettingField { + json_path: Some("notification_panel.dock"), pick: |settings_content| { settings_content.notification_panel.as_ref()?.dock.as_ref() }, @@ -3976,6 +4174,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Notification Panel Default Width", description: "Default width of the notification panel in pixels.", field: Box::new(SettingField { + json_path: Some("notification_panel.default_width"), pick: |settings_content| { settings_content .notification_panel @@ -3998,6 +4197,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Collaboration Panel Button", description: "Show the collaboration panel button in the status bar.", field: Box::new(SettingField { + json_path: Some("collaboration_panel.button"), pick: |settings_content| { settings_content .collaboration_panel @@ -4019,6 +4219,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Collaboration Panel Dock", description: "Where to dock the collaboration panel.", field: Box::new(SettingField { + json_path: Some("collaboration_panel.dock"), pick: |settings_content| { settings_content.collaboration_panel.as_ref()?.dock.as_ref() }, @@ -4036,6 +4237,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Collaboration Panel Default Width", description: "Default width of the collaboration panel in pixels.", field: Box::new(SettingField { + json_path: Some("collaboration_panel.dock"), pick: |settings_content| { settings_content .collaboration_panel @@ -4058,6 +4260,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Agent Panel Button", description: "Whether to show the agent panel button in the status bar.", field: Box::new(SettingField { + json_path: Some("agent.button"), pick: |settings_content| settings_content.agent.as_ref()?.button.as_ref(), write: |settings_content, value| { settings_content.agent.get_or_insert_default().button = value; @@ -4070,6 +4273,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Agent Panel Dock", description: "Where to dock the agent panel.", field: Box::new(SettingField { + json_path: Some("agent.dock"), pick: |settings_content| settings_content.agent.as_ref()?.dock.as_ref(), write: |settings_content, value| { settings_content.agent.get_or_insert_default().dock = value; @@ -4082,6 +4286,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Agent Panel Default Width", description: "Default width when the agent panel is docked to the left or right.", field: Box::new(SettingField { + json_path: Some("agent.default_width"), pick: |settings_content| { settings_content.agent.as_ref()?.default_width.as_ref() }, @@ -4096,6 +4301,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Agent Panel Default Height", description: "Default height when the agent panel is docked to the bottom.", field: Box::new(SettingField { + json_path: Some("agent.default_height"), pick: |settings_content| { settings_content.agent.as_ref()?.default_height.as_ref() }, @@ -4119,6 +4325,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Stepping Granularity", description: "Determines the stepping granularity for debug operations.", field: Box::new(SettingField { + json_path: Some("agent.default_height"), pick: |settings_content| { settings_content .debugger @@ -4140,6 +4347,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Save Breakpoints", description: "Whether breakpoints should be reused across Zed sessions.", field: Box::new(SettingField { + json_path: Some("debugger.save_breakpoints"), pick: |settings_content| { settings_content .debugger @@ -4161,6 +4369,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Timeout", description: "Time in milliseconds until timeout error when connecting to a TCP debug adapter.", field: Box::new(SettingField { + json_path: Some("debugger.timeout"), pick: |settings_content| { settings_content.debugger.as_ref()?.timeout.as_ref() }, @@ -4175,6 +4384,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Log DAP Communications", description: "Whether to log messages between active debug adapters and Zed.", field: Box::new(SettingField { + json_path: Some("debugger.log_dap_communications"), pick: |settings_content| { settings_content .debugger @@ -4196,6 +4406,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Format DAP Log Messages", description: "Whether to format DAP messages when adding them to debug adapter logger.", field: Box::new(SettingField { + json_path: Some("debugger.format_dap_log_messages"), pick: |settings_content| { settings_content .debugger @@ -4225,6 +4436,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Shell", description: "What shell to use when opening a terminal.", field: Box::new(SettingField { + json_path: Some("terminal.shell$"), pick: |settings_content| { Some(&dynamic_variants::()[ settings_content @@ -4288,6 +4500,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Program", description: "The shell program to use.", field: Box::new(SettingField { + json_path: Some("terminal.shell.program"), pick: |settings_content| { match settings_content.terminal.as_ref()?.project.shell.as_ref() { Some(settings::Shell::Program(program)) => Some(program), @@ -4317,6 +4530,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Program", description: "The shell program to run.", field: Box::new(SettingField { + json_path: Some("terminal.shell.program"), pick: |settings_content| { match settings_content.terminal.as_ref()?.project.shell.as_ref() { Some(settings::Shell::WithArguments { program, .. }) => Some(program), @@ -4345,6 +4559,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The arguments to pass to the shell program.", field: Box::new( SettingField { + json_path: Some("terminal.shell.args"), pick: |settings_content| { match settings_content.terminal.as_ref()?.project.shell.as_ref() { Some(settings::Shell::WithArguments { args, .. }) => Some(args), @@ -4374,6 +4589,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Title Override", description: "An optional string to override the title of the terminal tab.", field: Box::new(SettingField { + json_path: Some("terminal.shell.title_override"), pick: |settings_content| { match settings_content.terminal.as_ref()?.project.shell.as_ref() { Some(settings::Shell::WithArguments { title_override, .. }) => title_override.as_ref().or(DEFAULT_EMPTY_SHARED_STRING), @@ -4403,6 +4619,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Working Directory", description: "What working directory to use when launching the terminal.", field: Box::new(SettingField { + json_path: Some("terminal.working_directory$"), pick: |settings_content| { Some(&dynamic_variants::()[ settings_content @@ -4459,6 +4676,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Directory", description: "The directory path to use (will be shell expanded).", field: Box::new(SettingField { + json_path: Some("terminal.working_directory.always"), pick: |settings_content| { match settings_content.terminal.as_ref()?.project.working_directory.as_ref() { Some(settings::WorkingDirectory::Always { directory }) => Some(directory), @@ -4488,6 +4706,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Key-value pairs to add to the terminal's environment.", field: Box::new( SettingField { + json_path: Some("terminal.env"), pick: |settings_content| { settings_content.terminal.as_ref()?.project.env.as_ref() }, @@ -4509,6 +4728,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Activates the Python virtual environment, if one is found, in the terminal's working directory.", field: Box::new( SettingField { + json_path: Some("terminal.detect_venv"), pick: |settings_content| { settings_content .terminal @@ -4535,6 +4755,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Size", description: "Font size for terminal text. If not set, defaults to buffer font size.", field: Box::new(SettingField { + json_path: Some("terminal.font_size"), pick: |settings_content| { settings_content .terminal @@ -4553,6 +4774,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Family", description: "Font family for terminal text. If not set, defaults to buffer font family.", field: Box::new(SettingField { + json_path: Some("terminal.font_family"), pick: |settings_content| { settings_content .terminal @@ -4575,6 +4797,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Font fallbacks for terminal text. If not set, defaults to buffer font fallbacks.", field: Box::new( SettingField { + json_path: Some("terminal.font_fallbacks"), pick: |settings_content| { settings_content .terminal @@ -4598,6 +4821,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Font Weight", description: "Font weight for terminal text in CSS weight units (100-900).", field: Box::new(SettingField { + json_path: Some("terminal.font_weight"), pick: |settings_content| { settings_content.terminal.as_ref()?.font_weight.as_ref() }, @@ -4616,6 +4840,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Font features for terminal text.", field: Box::new( SettingField { + json_path: Some("terminal.font_features"), pick: |settings_content| { settings_content .terminal @@ -4641,6 +4866,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "Line height for terminal text.", field: Box::new( SettingField { + json_path: Some("terminal.line_height"), pick: |settings_content| { settings_content.terminal.as_ref()?.line_height.as_ref() }, @@ -4660,6 +4886,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Cursor Shape", description: "Default cursor shape for the terminal (bar, block, underline, or hollow).", field: Box::new(SettingField { + json_path: Some("terminal.cursor_shape"), pick: |settings_content| { settings_content.terminal.as_ref()?.cursor_shape.as_ref() }, @@ -4677,6 +4904,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Cursor Blinking", description: "Sets the cursor blinking behavior in the terminal.", field: Box::new(SettingField { + json_path: Some("terminal.blinking"), pick: |settings_content| { settings_content.terminal.as_ref()?.blinking.as_ref() }, @@ -4691,6 +4919,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Alternate Scroll", description: "Whether alternate scroll mode is active by default (converts mouse scroll to arrow keys in apps like Vim).", field: Box::new(SettingField { + json_path: Some("terminal.alternate_scroll"), pick: |settings_content| { settings_content .terminal @@ -4712,6 +4941,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Minimum Contrast", description: "The minimum APCA perceptual contrast between foreground and background colors (0-106).", field: Box::new(SettingField { + json_path: Some("terminal.minimum_contrast"), pick: |settings_content| { settings_content .terminal @@ -4734,6 +4964,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Option As Meta", description: "Whether the option key behaves as the meta key.", field: Box::new(SettingField { + json_path: Some("terminal.option_as_meta"), pick: |settings_content| { settings_content.terminal.as_ref()?.option_as_meta.as_ref() }, @@ -4751,6 +4982,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Copy On Select", description: "Whether selecting text in the terminal automatically copies to the system clipboard.", field: Box::new(SettingField { + json_path: Some("terminal.copy_on_select"), pick: |settings_content| { settings_content.terminal.as_ref()?.copy_on_select.as_ref() }, @@ -4768,6 +5000,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Keep Selection On Copy", description: "Whether to keep the text selection after copying it to the clipboard.", field: Box::new(SettingField { + json_path: Some("terminal.keep_selection_on_copy"), pick: |settings_content| { settings_content .terminal @@ -4790,6 +5023,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Default Width", description: "Default width when the terminal is docked to the left or right (in pixels).", field: Box::new(SettingField { + json_path: Some("terminal.default_width"), pick: |settings_content| { settings_content.terminal.as_ref()?.default_width.as_ref() }, @@ -4807,6 +5041,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Default Height", description: "Default height when the terminal is docked to the bottom (in pixels).", field: Box::new(SettingField { + json_path: Some("terminal.default_height"), pick: |settings_content| { settings_content.terminal.as_ref()?.default_height.as_ref() }, @@ -4825,6 +5060,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Max Scroll History Lines", description: "Maximum number of lines to keep in scrollback history (max: 100,000; 0 disables scrolling).", field: Box::new(SettingField { + json_path: Some("terminal.max_scroll_history_lines"), pick: |settings_content| { settings_content .terminal @@ -4847,6 +5083,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Breadcrumbs", description: "Display the terminal title in breadcrumbs inside the terminal pane.", field: Box::new(SettingField { + json_path: Some("terminal.toolbar.breadcrumbs"), pick: |settings_content| { settings_content .terminal @@ -4873,6 +5110,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Scrollbar", description: "When to show the scrollbar in the terminal.", field: Box::new(SettingField { + json_path: Some("terminal.scrollbar.show"), pick: |settings_content| { show_scrollbar_or_editor(settings_content, |settings_content| { settings_content @@ -4906,6 +5144,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Visibility", description: "Control whether Git status is shown in the editor's gutter.", field: Box::new(SettingField { + json_path: Some("git.git_gutter"), pick: |settings_content| settings_content.git.as_ref()?.git_gutter.as_ref(), write: |settings_content, value| { settings_content.git.get_or_insert_default().git_gutter = value; @@ -4919,6 +5158,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Debounce", description: "Debounce threshold in milliseconds after which changes are reflected in the Git gutter.", field: Box::new(SettingField { + json_path: Some("git.gutter_debounce"), pick: |settings_content| { settings_content.git.as_ref()?.gutter_debounce.as_ref() }, @@ -4934,6 +5174,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enabled", description: "Whether or not to show Git blame data inline in the currently focused line.", field: Box::new(SettingField { + json_path: Some("git.inline_blame.enabled"), pick: |settings_content| { settings_content .git @@ -4959,6 +5200,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Delay", description: "The delay after which the inline blame information is shown.", field: Box::new(SettingField { + json_path: Some("git.inline_blame.delay_ms"), pick: |settings_content| { settings_content .git @@ -4984,6 +5226,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Padding", description: "Padding between the end of the source line and the start of the inline blame in columns.", field: Box::new(SettingField { + json_path: Some("git.inline_blame.padding"), pick: |settings_content| { settings_content .git @@ -5009,6 +5252,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Minimum Column", description: "The minimum column number at which to show the inline blame information.", field: Box::new(SettingField { + json_path: Some("git.inline_blame.min_column"), pick: |settings_content| { settings_content .git @@ -5034,6 +5278,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Commit Summary", description: "Show commit summary as part of the inline blame.", field: Box::new(SettingField { + json_path: Some("git.inline_blame.show_commit_summary"), pick: |settings_content| { settings_content .git @@ -5060,6 +5305,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Avatar", description: "Show the avatar of the author of the commit.", field: Box::new(SettingField { + json_path: Some("git.blame.show_avatar"), pick: |settings_content| { settings_content .git @@ -5086,6 +5332,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Show Author Name", description: "Show author name as part of the commit information in branch picker.", field: Box::new(SettingField { + json_path: Some("git.branch_picker.show_author_name"), pick: |settings_content| { settings_content .git @@ -5112,6 +5359,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Hunk Style", description: "How Git hunks are displayed visually in the editor.", field: Box::new(SettingField { + json_path: Some("git.hunk_style"), pick: |settings_content| settings_content.git.as_ref()?.hunk_style.as_ref(), write: |settings_content, value| { settings_content.git.get_or_insert_default().hunk_style = value; @@ -5130,6 +5378,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Mute On Join", description: "Whether the microphone should be muted when joining a channel or a call.", field: Box::new(SettingField { + json_path: Some("calls.mute_on_join"), pick: |settings_content| { settings_content.calls.as_ref()?.mute_on_join.as_ref() }, @@ -5144,6 +5393,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Share On Join", description: "Whether your current project should be shared when joining an empty channel.", field: Box::new(SettingField { + json_path: Some("calls.share_on_join"), pick: |settings_content| { settings_content.calls.as_ref()?.share_on_join.as_ref() }, @@ -5159,6 +5409,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Rodio Audio", description: "Opt into the new audio system.", field: Box::new(SettingField { + json_path: Some("audio.experimental.rodio_audio"), pick: |settings_content| { settings_content.audio.as_ref()?.rodio_audio.as_ref() }, @@ -5173,6 +5424,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Microphone Volume", description: "Automatically adjust microphone volume (requires rodio audio).", field: Box::new(SettingField { + json_path: Some("audio.experimental.auto_microphone_volume"), pick: |settings_content| { settings_content .audio @@ -5194,6 +5446,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Auto Speaker Volume", description: "Automatically adjust volume of other call members (requires rodio audio).", field: Box::new(SettingField { + json_path: Some("audio.experimental.auto_speaker_volume"), pick: |settings_content| { settings_content .audio @@ -5215,6 +5468,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Denoise", description: "Remove background noises (requires rodio audio).", field: Box::new(SettingField { + json_path: Some("audio.experimental.denoise"), pick: |settings_content| settings_content.audio.as_ref()?.denoise.as_ref(), write: |settings_content, value| { settings_content.audio.get_or_insert_default().denoise = value; @@ -5227,6 +5481,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Legacy Audio Compatible", description: "Use audio parameters compatible with previous versions (requires rodio audio).", field: Box::new(SettingField { + json_path: Some("audio.experimental.legacy_audio_compatible"), pick: |settings_content| { settings_content .audio @@ -5254,6 +5509,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Disable AI", description: "Whether to disable all AI features in Zed.", field: Box::new(SettingField { + json_path: Some("disable_ai"), pick: |settings_content| settings_content.disable_ai.as_ref(), write: |settings_content, value| { settings_content.disable_ai = value; @@ -5267,6 +5523,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Always Allow Tool Actions", description: "When enabled, the agent can run potentially destructive actions without asking for your confirmation. This setting has no effect on external agents.", field: Box::new(SettingField { + json_path: Some("agent.always_allow_tool_actions"), pick: |settings_content| { settings_content .agent @@ -5288,6 +5545,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Single File Review", description: "When enabled, agent edits will also be displayed in single-file buffers for review.", field: Box::new(SettingField { + json_path: Some("agent.single_file_review"), pick: |settings_content| { settings_content.agent.as_ref()?.single_file_review.as_ref() }, @@ -5305,6 +5563,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Enable Feedback", description: "Show voting thumbs up/down icon buttons for feedback on agent edits.", field: Box::new(SettingField { + json_path: Some("agent.enable_feedback"), pick: |settings_content| { settings_content.agent.as_ref()?.enable_feedback.as_ref() }, @@ -5322,6 +5581,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Notify When Agent Waiting", description: "Where to show notifications when the agent has completed its response or needs confirmation before running a tool action.", field: Box::new(SettingField { + json_path: Some("agent.notify_when_agent_waiting"), pick: |settings_content| { settings_content .agent @@ -5343,6 +5603,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Play Sound When Agent Done", description: "Whether to play a sound when the agent has either completed its response, or needs user input.", field: Box::new(SettingField { + json_path: Some("agent.play_sound_when_agent_done"), pick: |settings_content| { settings_content .agent @@ -5364,6 +5625,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Expand Edit Card", description: "Whether to have edit cards in the agent panel expanded, showing a Preview of the diff.", field: Box::new(SettingField { + json_path: Some("agent.expand_edit_card"), pick: |settings_content| { settings_content.agent.as_ref()?.expand_edit_card.as_ref() }, @@ -5381,6 +5643,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Expand Terminal Card", description: "Whether to have terminal cards in the agent panel expanded, showing the whole command output.", field: Box::new(SettingField { + json_path: Some("agent.expand_terminal_card"), pick: |settings_content| { settings_content .agent @@ -5402,6 +5665,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Use Modifier To Send", description: "Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages.", field: Box::new(SettingField { + json_path: Some("agent.use_modifier_to_send"), pick: |settings_content| { settings_content .agent @@ -5423,6 +5687,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Message Editor Min Lines", description: "Minimum number of lines to display in the agent message editor.", field: Box::new(SettingField { + json_path: Some("agent.message_editor_min_lines"), pick: |settings_content| { settings_content .agent @@ -5452,6 +5717,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { description: "The proxy to use for network requests.", field: Box::new( SettingField { + json_path: Some("proxy"), pick: |settings_content| settings_content.proxy.as_ref(), write: |settings_content, value| { settings_content.proxy = value; @@ -5469,6 +5735,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { title: "Server URL", description: "The URL of the Zed server to connect to.", field: Box::new(SettingField { + json_path: Some("server_url"), pick: |settings_content| settings_content.server_url.as_ref(), write: |settings_content, value| { settings_content.server_url = value; @@ -5535,6 +5802,7 @@ fn language_settings_data() -> Vec { title: "Tab Size", description: "How many columns a tab should occupy.", field: Box::new(SettingField { + json_path: Some("languages.$(language).tab_size"), // TODO(cameron): not JQ syntax because not URL-safe pick: |settings_content| { language_settings_field(settings_content, |language| language.tab_size.as_ref()) }, @@ -5551,6 +5819,7 @@ fn language_settings_data() -> Vec { title: "Hard Tabs", description: "Whether to indent lines using tab characters, as opposed to multiple spaces.", field: Box::new(SettingField { + json_path: Some("languages.$(language).hard_tabs"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.hard_tabs.as_ref() @@ -5569,6 +5838,7 @@ fn language_settings_data() -> Vec { title: "Auto Indent", description: "Whether indentation should be adjusted based on the context whilst typing.", field: Box::new(SettingField { + json_path: Some("languages.$(language).auto_indent"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.auto_indent.as_ref() @@ -5587,6 +5857,7 @@ fn language_settings_data() -> Vec { title: "Auto Indent On Paste", description: "Whether indentation of pasted content should be adjusted based on the context.", field: Box::new(SettingField { + json_path: Some("languages.$(language).auto_indent_on_paste"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.auto_indent_on_paste.as_ref() @@ -5606,6 +5877,7 @@ fn language_settings_data() -> Vec { title: "Soft Wrap", description: "How to soft-wrap long lines of text.", field: Box::new(SettingField { + json_path: Some("languages.$(language).soft_wrap"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.soft_wrap.as_ref() @@ -5624,6 +5896,7 @@ fn language_settings_data() -> Vec { title: "Show Wrap Guides", description: "Show wrap guides in the editor.", field: Box::new(SettingField { + json_path: Some("languages.$(language).show_wrap_guides"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.show_wrap_guides.as_ref() @@ -5642,6 +5915,7 @@ fn language_settings_data() -> Vec { title: "Preferred Line Length", description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled.", field: Box::new(SettingField { + json_path: Some("languages.$(language).preferred_line_length"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.preferred_line_length.as_ref() @@ -5661,6 +5935,7 @@ fn language_settings_data() -> Vec { description: "Character counts at which to show wrap guides in the editor.", field: Box::new( SettingField { + json_path: Some("languages.$(language).wrap_guides"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.wrap_guides.as_ref() @@ -5681,6 +5956,7 @@ fn language_settings_data() -> Vec { title: "Allow Rewrap", description: "Controls where the `editor::rewrap` action is allowed for this language.", field: Box::new(SettingField { + json_path: Some("languages.$(language).allow_rewrap"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.allow_rewrap.as_ref() @@ -5700,6 +5976,7 @@ fn language_settings_data() -> Vec { title: "Enabled", description: "Display indent guides in the editor.", field: Box::new(SettingField { + json_path: Some("languages.$(language).indent_guides.enabled"), pick: |settings_content| { language_settings_field(settings_content, |language| { language @@ -5721,6 +5998,7 @@ fn language_settings_data() -> Vec { title: "Line Width", description: "The width of the indent guides in pixels, between 1 and 10.", field: Box::new(SettingField { + json_path: Some("languages.$(language).indent_guides.line_width"), pick: |settings_content| { language_settings_field(settings_content, |language| { language @@ -5742,6 +6020,7 @@ fn language_settings_data() -> Vec { title: "Active Line Width", description: "The width of the active indent guide in pixels, between 1 and 10.", field: Box::new(SettingField { + json_path: Some("languages.$(language).indent_guides.active_line_width"), pick: |settings_content| { language_settings_field(settings_content, |language| { language @@ -5766,6 +6045,7 @@ fn language_settings_data() -> Vec { title: "Coloring", description: "Determines how indent guides are colored.", field: Box::new(SettingField { + json_path: Some("languages.$(language).indent_guides.coloring"), pick: |settings_content| { language_settings_field(settings_content, |language| { language @@ -5787,6 +6067,7 @@ fn language_settings_data() -> Vec { title: "Background Coloring", description: "Determines how indent guide backgrounds are colored.", field: Box::new(SettingField { + json_path: Some("languages.$(language).indent_guides.background_coloring"), pick: |settings_content| { language_settings_field(settings_content, |language| { language @@ -5814,6 +6095,7 @@ fn language_settings_data() -> Vec { field: Box::new( // TODO(settings_ui): this setting should just be a bool SettingField { + json_path: Some("languages.$(language).format_on_save"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.format_on_save.as_ref() @@ -5833,6 +6115,7 @@ fn language_settings_data() -> Vec { title: "Remove Trailing Whitespace On Save", description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it.", field: Box::new(SettingField { + json_path: Some("languages.$(language).remove_trailing_whitespace_on_save"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.remove_trailing_whitespace_on_save.as_ref() @@ -5851,6 +6134,7 @@ fn language_settings_data() -> Vec { title: "Ensure Final Newline On Save", description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it.", field: Box::new(SettingField { + json_path: Some("languages.$(language).ensure_final_newline_on_save"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.ensure_final_newline_on_save.as_ref() @@ -5870,6 +6154,7 @@ fn language_settings_data() -> Vec { description: "How to perform a buffer format.", field: Box::new( SettingField { + json_path: Some("languages.$(language).formatter"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.formatter.as_ref() @@ -5890,6 +6175,7 @@ fn language_settings_data() -> Vec { title: "Use On Type Format", description: "Whether to use additional LSP queries to format (and amend) the code after every \"trigger\" symbol input, defined by LSP server capabilities", field: Box::new(SettingField { + json_path: Some("languages.$(language).use_on_type_format"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.use_on_type_format.as_ref() @@ -5909,6 +6195,7 @@ fn language_settings_data() -> Vec { description: "Additional code actions to run when formatting.", field: Box::new( SettingField { + json_path: Some("languages.$(language).code_actions_on_format"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.code_actions_on_format.as_ref() @@ -5930,6 +6217,7 @@ fn language_settings_data() -> Vec { title: "Use Autoclose", description: "Whether to automatically type closing characters for you. For example, when you type '(', Zed will automatically add a closing ')' at the correct position.", field: Box::new(SettingField { + json_path: Some("languages.$(language).use_autoclose"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.use_autoclose.as_ref() @@ -5948,6 +6236,7 @@ fn language_settings_data() -> Vec { title: "Use Auto Surround", description: "Whether to automatically surround text with characters for you. For example, when you select text and type '(', Zed will automatically surround text with ().", field: Box::new(SettingField { + json_path: Some("languages.$(language).use_auto_surround"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.use_auto_surround.as_ref() @@ -5966,6 +6255,7 @@ fn language_settings_data() -> Vec { title: "Always Treat Brackets As Autoclosed", description: "Controls whether the closing characters are always skipped over and auto-removed no matter how they were inserted.", field: Box::new(SettingField { + json_path: Some("languages.$(language).always_treat_brackets_as_autoclosed"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.always_treat_brackets_as_autoclosed.as_ref() @@ -5984,6 +6274,7 @@ fn language_settings_data() -> Vec { title: "Jsx Tag Auto Close", description: "Whether to automatically close JSX tags.", field: Box::new(SettingField { + json_path: Some("languages.$(language).jsx_tag_auto_close"), // TODO(settings_ui): this setting should just be a bool pick: |settings_content| { language_settings_field(settings_content, |language| { @@ -6004,6 +6295,7 @@ fn language_settings_data() -> Vec { title: "Show Edit Predictions", description: "Controls whether edit predictions are shown immediately (true) or manually by triggering `editor::showeditprediction` (false).", field: Box::new(SettingField { + json_path: Some("languages.$(language).show_edit_predictions"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.show_edit_predictions.as_ref() @@ -6023,6 +6315,7 @@ fn language_settings_data() -> Vec { description: "Controls whether edit predictions are shown in the given language scopes.", field: Box::new( SettingField { + json_path: Some("languages.$(language).edit_predictions_disabled_in"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.edit_predictions_disabled_in.as_ref() @@ -6044,6 +6337,7 @@ fn language_settings_data() -> Vec { title: "Show Whitespaces", description: "Whether to show tabs and spaces in the editor.", field: Box::new(SettingField { + json_path: Some("languages.$(language).show_whitespaces"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.show_whitespaces.as_ref() @@ -6063,6 +6357,7 @@ fn language_settings_data() -> Vec { description: "Visible character used to render space characters when show_whitespaces is enabled (default: \"•\")", field: Box::new( SettingField { + json_path: Some("languages.$(language).whitespace_map.space"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.whitespace_map.as_ref()?.space.as_ref() @@ -6084,6 +6379,7 @@ fn language_settings_data() -> Vec { description: "Visible character used to render tab characters when show_whitespaces is enabled (default: \"→\")", field: Box::new( SettingField { + json_path: Some("languages.$(language).whitespace_map.tab"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.whitespace_map.as_ref()?.tab.as_ref() @@ -6105,6 +6401,7 @@ fn language_settings_data() -> Vec { title: "Show Completions On Input", description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it.", field: Box::new(SettingField { + json_path: Some("languages.$(language).show_completions_on_input"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.show_completions_on_input.as_ref() @@ -6123,6 +6420,7 @@ fn language_settings_data() -> Vec { title: "Show Completion Documentation", description: "Whether to display inline and alongside documentation for items in the completions menu.", field: Box::new(SettingField { + json_path: Some("languages.$(language).show_completion_documentation"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.show_completion_documentation.as_ref() @@ -6141,6 +6439,7 @@ fn language_settings_data() -> Vec { title: "Words", description: "Controls how words are completed.", field: Box::new(SettingField { + json_path: Some("languages.$(language).completions.words"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.completions.as_ref()?.words.as_ref() @@ -6159,6 +6458,7 @@ fn language_settings_data() -> Vec { title: "Words Min Length", description: "How many characters has to be in the completions query to automatically show the words-based completions.", field: Box::new(SettingField { + json_path: Some("languages.$(language).completions.words_min_length"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.completions.as_ref()?.words_min_length.as_ref() @@ -6181,6 +6481,7 @@ fn language_settings_data() -> Vec { title: "Enabled", description: "Global switch to toggle hints on and off.", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.enabled"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.enabled.as_ref() @@ -6199,6 +6500,7 @@ fn language_settings_data() -> Vec { title: "Show Value Hints", description: "Global switch to toggle inline values on and off when debugging.", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.show_value_hints"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.show_value_hints.as_ref() @@ -6220,6 +6522,7 @@ fn language_settings_data() -> Vec { title: "Show Type Hints", description: "Whether type hints should be shown.", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.show_type_hints"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.show_type_hints.as_ref() @@ -6238,6 +6541,7 @@ fn language_settings_data() -> Vec { title: "Show Parameter Hints", description: "Whether parameter hints should be shown.", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.show_parameter_hints"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.show_parameter_hints.as_ref() @@ -6259,6 +6563,7 @@ fn language_settings_data() -> Vec { title: "Show Other Hints", description: "Whether other hints should be shown.", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.show_other_hints"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.show_other_hints.as_ref() @@ -6280,6 +6585,7 @@ fn language_settings_data() -> Vec { title: "Show Background", description: "Show a background for inlay hints.", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.show_background"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.show_background.as_ref() @@ -6298,6 +6604,7 @@ fn language_settings_data() -> Vec { title: "Edit Debounce Ms", description: "Whether or not to debounce inlay hints updates after buffer edits (set to 0 to disable debouncing).", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.edit_debounce_ms"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.edit_debounce_ms.as_ref() @@ -6319,6 +6626,7 @@ fn language_settings_data() -> Vec { title: "Scroll Debounce Ms", description: "Whether or not to debounce inlay hints updates after buffer scrolls (set to 0 to disable debouncing).", field: Box::new(SettingField { + json_path: Some("languages.$(language).inlay_hints.scroll_debounce_ms"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.inlay_hints.as_ref()?.scroll_debounce_ms.as_ref() @@ -6341,6 +6649,7 @@ fn language_settings_data() -> Vec { description: "Toggles inlay hints (hides or shows) when the user presses the modifiers specified.", field: Box::new( SettingField { + json_path: Some("languages.$(language).inlay_hints.toggle_on_modifiers_press"), pick: |settings_content| { language_settings_field(settings_content, |language| { language @@ -6370,6 +6679,7 @@ fn language_settings_data() -> Vec { title: "LSP Document Colors", description: "How to render LSP color previews in the editor.", field: Box::new(SettingField { + json_path: Some("lsp_document_colors"), pick: |settings_content| settings_content.editor.lsp_document_colors.as_ref(), write: |settings_content, value| { settings_content.editor.lsp_document_colors = value; @@ -6385,6 +6695,7 @@ fn language_settings_data() -> Vec { title: "Enabled", description: "Whether tasks are enabled for this language.", field: Box::new(SettingField { + json_path: Some("languages.$(language).tasks.enabled"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.tasks.as_ref()?.enabled.as_ref() @@ -6405,6 +6716,7 @@ fn language_settings_data() -> Vec { description: "Extra task variables to set for a particular language.", field: Box::new( SettingField { + json_path: Some("languages.$(language).tasks.variables"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.tasks.as_ref()?.variables.as_ref() @@ -6426,6 +6738,7 @@ fn language_settings_data() -> Vec { title: "Prefer LSP", description: "Use LSP tasks over Zed language extension tasks.", field: Box::new(SettingField { + json_path: Some("languages.$(language).tasks.prefer_lsp"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.tasks.as_ref()?.prefer_lsp.as_ref() @@ -6447,6 +6760,7 @@ fn language_settings_data() -> Vec { description: "Preferred debuggers for this language.", field: Box::new( SettingField { + json_path: Some("languages.$(language).debuggers"), pick: |settings_content| { language_settings_field(settings_content, |language| language.debuggers.as_ref()) }, @@ -6466,6 +6780,7 @@ fn language_settings_data() -> Vec { title: "Middle Click Paste", description: "Enable middle-click paste on Linux.", field: Box::new(SettingField { + json_path: Some("languages.$(language).editor.middle_click_paste"), pick: |settings_content| settings_content.editor.middle_click_paste.as_ref(), write: |settings_content, value| {settings_content.editor.middle_click_paste = value;}, }), @@ -6476,6 +6791,7 @@ fn language_settings_data() -> Vec { title: "Extend Comment On Newline", description: "Whether to start a new line with a comment when a previous line is a comment as well.", field: Box::new(SettingField { + json_path: Some("languages.$(language).extend_comment_on_newline"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.extend_comment_on_newline.as_ref() @@ -6499,6 +6815,7 @@ fn language_settings_data() -> Vec { title: "Image Viewer", description: "The unit for image file sizes.", field: Box::new(SettingField { + json_path: Some("image_viewer.unit"), pick: |settings_content| { settings_content.image_viewer.as_ref().and_then(|image_viewer| image_viewer.unit.as_ref()) }, @@ -6514,6 +6831,7 @@ fn language_settings_data() -> Vec { title: "Auto Replace Emoji Shortcode", description: "Whether to automatically replace emoji shortcodes with emoji characters.", field: Box::new(SettingField { + json_path: Some("message_editor.auto_replace_emoji_shortcode"), pick: |settings_content| { settings_content.message_editor.as_ref().and_then(|message_editor| message_editor.auto_replace_emoji_shortcode.as_ref()) }, @@ -6529,6 +6847,7 @@ fn language_settings_data() -> Vec { title: "Drop Size Target", description: "Relative size of the drop target in the editor that will open dropped file as a split pane.", field: Box::new(SettingField { + json_path: Some("drop_target_size"), pick: |settings_content| { settings_content.workspace.drop_target_size.as_ref() }, @@ -6554,6 +6873,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Enable Language Server", description: "Whether to use language servers to provide code intelligence.", field: Box::new(SettingField { + json_path: Some("languages.$(language).enable_language_server"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.enable_language_server.as_ref() @@ -6573,6 +6893,7 @@ fn non_editor_language_settings_data() -> Vec { description: "The list of language servers to use (or disable) for this language.", field: Box::new( SettingField { + json_path: Some("languages.$(language).language_servers"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.language_servers.as_ref() @@ -6593,6 +6914,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Linked Edits", description: "Whether to perform linked edits of associated ranges, if the LS supports it. For example, when editing opening tag, the contents of the closing tag will be edited as well.", field: Box::new(SettingField { + json_path: Some("languages.$(language).linked_edits"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.linked_edits.as_ref() @@ -6611,6 +6933,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Go To Definition Fallback", description: "Whether to follow-up empty Go to definition responses from the language server.", field: Box::new(SettingField { + json_path: Some("go_to_definition_fallback"), pick: |settings_content| settings_content.editor.go_to_definition_fallback.as_ref(), write: |settings_content, value| { settings_content.editor.go_to_definition_fallback = value; @@ -6624,6 +6947,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Enabled", description: "Whether to fetch LSP completions or not.", field: Box::new(SettingField { + json_path: Some("languages.$(language).completions.lsp"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.completions.as_ref()?.lsp.as_ref() @@ -6642,6 +6966,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Fetch Timeout (milliseconds)", description: "When fetching LSP completions, determines how long to wait for a response of a particular server (set to 0 to wait indefinitely).", field: Box::new(SettingField { + json_path: Some("languages.$(language).completions.lsp_fetch_timeout_ms"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.completions.as_ref()?.lsp_fetch_timeout_ms.as_ref() @@ -6663,6 +6988,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Insert Mode", description: "Controls how LSP completions are inserted.", field: Box::new(SettingField { + json_path: Some("languages.$(language).completions.lsp_insert_mode"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.completions.as_ref()?.lsp_insert_mode.as_ref() @@ -6683,6 +7009,7 @@ fn non_editor_language_settings_data() -> Vec { description: "Preferred debuggers for this language.", field: Box::new( SettingField { + json_path: Some("languages.$(language).debuggers"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.debuggers.as_ref() @@ -6704,6 +7031,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Allowed", description: "Enables or disables formatting with Prettier for a given language.", field: Box::new(SettingField { + json_path: Some("languages.$(language).prettier.allowed"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.prettier.as_ref()?.allowed.as_ref() @@ -6722,6 +7050,7 @@ fn non_editor_language_settings_data() -> Vec { title: "Parser", description: "Forces Prettier integration to use a specific parser name when formatting files with the language.", field: Box::new(SettingField { + json_path: Some("languages.$(language).prettier.parser"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.prettier.as_ref()?.parser.as_ref() @@ -6741,6 +7070,7 @@ fn non_editor_language_settings_data() -> Vec { description: "Forces Prettier integration to use specific plugins when formatting files with the language.", field: Box::new( SettingField { + json_path: Some("languages.$(language).prettier.plugins"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.prettier.as_ref()?.plugins.as_ref() @@ -6762,6 +7092,7 @@ fn non_editor_language_settings_data() -> Vec { description: "Default Prettier options, in the format as in package.json section for Prettier.", field: Box::new( SettingField { + json_path: Some("languages.$(language).prettier"), pick: |settings_content| { language_settings_field(settings_content, |language| { language.prettier.as_ref()?.options.as_ref() diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 0485b2608c99f74fbdc86d1cf0807ebb9727c7e8..9fa40418df4792978de1ff7fea074e1334d9dad0 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -34,7 +34,7 @@ use ui::{ use ui_input::{NumberField, NumberFieldType}; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; use workspace::{OpenOptions, OpenVisible, Workspace, client_side_decorations}; -use zed_actions::OpenSettings; +use zed_actions::{OpenSettings, OpenSettingsAt}; use crate::components::{SettingsInputField, font_picker, icon_theme_picker, theme_picker}; @@ -86,6 +86,25 @@ struct FocusFile(pub u32); struct SettingField { pick: fn(&SettingsContent) -> Option<&T>, write: fn(&mut SettingsContent, Option), + + /// A json-path-like string that gives a unique-ish string that identifies + /// where in the JSON the setting is defined. + /// + /// The syntax is `jq`-like, but modified slightly to be URL-safe (and + /// without the leading dot), e.g. `foo.bar`. + /// + /// They are URL-safe (this is important since links are the main use-case + /// for these paths). + /// + /// There are a couple of special cases: + /// - discrimminants are represented with a trailing `$`, for example + /// `terminal.working_directory$`. This is to distinguish the discrimminant + /// setting (i.e. the setting that changes whether the value is a string or + /// an object) from the setting in the case that it is a string. + /// - language-specific settings begin `languages.$(language)`. Links + /// targeting these settings should take the form `languages/Rust/...`, for + /// example, but are not currently supported. + json_path: Option<&'static str>, } impl Clone for SettingField { @@ -116,6 +135,7 @@ impl SettingField { SettingField { pick: |_| Some(&UnimplementedSettingField), write: |_, _| unreachable!(), + json_path: None, } } } @@ -132,6 +152,8 @@ trait AnySettingField { file_set_in: &settings::SettingsFile, cx: &App, ) -> Option>; + + fn json_path(&self) -> Option<&'static str>; } impl AnySettingField for SettingField { @@ -197,6 +219,10 @@ impl AnySettingField for SettingFi .log_err(); })); } + + fn json_path(&self) -> Option<&'static str> { + self.json_path + } } #[derive(Default, Clone)] @@ -344,13 +370,26 @@ impl FeatureFlag for SettingsUiFeatureFlag { pub fn init(cx: &mut App) { init_renderers(cx); + cx.observe_new(|workspace: &mut workspace::Workspace, _, _| { + workspace.register_action( + |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| { + let window_handle = window + .window_handle() + .downcast::() + .expect("Workspaces are root Windows"); + open_settings_editor(workspace, Some(&path), window_handle, cx); + }, + ); + }) + .detach(); + cx.observe_new(|workspace: &mut workspace::Workspace, _, _| { workspace.register_action(|workspace, _: &OpenSettings, window, cx| { let window_handle = window .window_handle() .downcast::() .expect("Workspaces are root Windows"); - open_settings_editor(workspace, window_handle, cx); + open_settings_editor(workspace, None, window_handle, cx); }); }) .detach(); @@ -456,9 +495,61 @@ fn init_renderers(cx: &mut App) { pub fn open_settings_editor( _workspace: &mut Workspace, + path: Option<&str>, workspace_handle: WindowHandle, cx: &mut App, ) { + /// Assumes a settings GUI window is already open + fn open_path( + path: &str, + settings_window: &mut SettingsWindow, + window: &mut Window, + cx: &mut Context, + ) { + if path.starts_with("languages.$(language)") { + log::error!("language-specific settings links are not currently supported"); + return; + } + + settings_window.current_file = SettingsUiFile::User; + settings_window.build_ui(window, cx); + + let mut item_info = None; + 'search: for (nav_entry_index, entry) in settings_window.navbar_entries.iter().enumerate() { + if entry.is_root { + continue; + } + let page_index = entry.page_index; + let header_index = entry + .item_index + .expect("non-root entries should have an item index"); + for item_index in header_index + 1..settings_window.pages[page_index].items.len() { + let item = &settings_window.pages[page_index].items[item_index]; + if let SettingsPageItem::SectionHeader(_) = item { + break; + } + if let SettingsPageItem::SettingItem(item) = item { + if item.field.json_path() == Some(path) { + if !item.files.contains(USER) { + log::error!("Found item {}, but it is not a user setting", path); + return; + } + item_info = Some((item_index, nav_entry_index)); + break 'search; + } + } + } + } + let Some((item_index, navbar_entry_index)) = item_info else { + log::error!("Failed to find item for {}", path); + return; + }; + + settings_window.open_navbar_entry_page(navbar_entry_index); + window.focus(&settings_window.focus_handle_for_content_element(item_index, cx)); + settings_window.scroll_to_content_item(item_index, window, cx); + } + let existing_window = cx .windows() .into_iter() @@ -470,6 +561,9 @@ pub fn open_settings_editor( settings_window.original_window = Some(workspace_handle); settings_window.observe_last_window_close(cx); window.activate_window(); + if let Some(path) = path { + open_path(path, settings_window, window, cx); + } }) .ok(); return; @@ -477,6 +571,7 @@ pub fn open_settings_editor( // We have to defer this to get the workspace off the stack. + let path = path.map(ToOwned::to_owned); cx.defer(move |cx| { let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into(); @@ -508,7 +603,17 @@ pub fn open_settings_editor( window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)), ..Default::default() }, - |window, cx| cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)), + |window, cx| { + let settings_window = + cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)); + settings_window.update(cx, |settings_window, cx| { + if let Some(path) = path { + open_path(&path, settings_window, window, cx); + } + }); + + settings_window + }, ) .log_err(); }); @@ -2112,17 +2217,7 @@ impl SettingsWindow { let entry_item_index = self.navbar_entries[navbar_entry_index] .item_index .expect("Non-root items should have an item index"); - let Some(selected_item_index) = self - .visible_page_items() - .position(|(index, _)| index == entry_item_index) - else { - return; - }; - - self.list_state.scroll_to(gpui::ListOffset { - item_ix: selected_item_index + 1, - offset_in_item: px(0.), - }); + self.scroll_to_content_item(entry_item_index, window, cx); if focus_content { handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx)); } else { @@ -2159,6 +2254,32 @@ impl SettingsWindow { cx.notify(); } + fn scroll_to_content_item( + &self, + content_item_index: usize, + _window: &mut Window, + cx: &mut Context, + ) { + let index = self + .visible_page_items() + .position(|(index, _)| index == content_item_index) + .unwrap_or(0); + if index == 0 { + self.sub_page_scroll_handle + .set_offset(point(px(0.), px(0.))); + self.list_state.scroll_to(gpui::ListOffset { + item_ix: 0, + offset_in_item: px(0.), + }); + return; + } + self.list_state.scroll_to(gpui::ListOffset { + item_ix: index + 1, + offset_in_item: px(0.), + }); + cx.notify(); + } + fn is_nav_entry_visible(&self, nav_entry_index: usize) -> bool { self.visible_navbar_entries() .any(|(index, _)| index == nav_entry_index) @@ -3173,7 +3294,7 @@ fn render_icon_theme_picker( } #[cfg(test)] -mod test { +pub mod test { use super::*; @@ -3194,7 +3315,7 @@ mod test { } } - fn register_settings(cx: &mut App) { + pub fn register_settings(cx: &mut App) { settings::init(cx); theme::init(theme::LoadThemes::JustBase, cx); workspace::init_settings(cx); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 92897bc3344c710f4d694667a21040100a23a3cc..93feb4a71d18164501955b46187a14d6757d861e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -847,6 +847,18 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut .detach(); }); } + OpenRequestKind::Setting { setting_path } => { + // zed://settings/languages/$(language)/tab_size - DONT SUPPORT + // zed://settings/languages/Rust/tab_size - SUPPORT + // languages.$(language).tab_size + // [ languages $(language) tab_size] + workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| { + window.dispatch_action( + Box::new(zed_actions::OpenSettingsAt { path: setting_path }), + cx, + ); + }); + } } return; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 3e0250825860aa358bb43125267dd4be8299b736..618849b3474e60f8a3737facf7c502f6e5f1cf52 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -47,6 +47,7 @@ pub enum OpenRequestKind { AgentPanel, DockMenuAction { index: usize }, BuiltinJsonSchema { schema_path: String }, + Setting { setting_path: String }, } impl OpenRequest { @@ -93,6 +94,10 @@ impl OpenRequest { this.kind = Some(OpenRequestKind::BuiltinJsonSchema { schema_path: schema_path.to_string(), }); + } else if let Some(setting_path) = url.strip_prefix("zed://settings/") { + this.kind = Some(OpenRequestKind::Setting { + setting_path: setting_path.to_string(), + }); } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? } else if let Some(request_path) = parse_zed_link(&url, cx) { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index d62de329c9af63ab8c15e1703b2517ac12594195..521405edc29845f6d459b3924355a5d8ea4a1bf8 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -30,12 +30,12 @@ pub struct OpenZedUrl { actions!( zed, [ + /// Opens the settings editor. #[action(deprecated_aliases = ["zed_actions::OpenSettingsEditor"])] OpenSettings, /// Opens the settings JSON file. #[action(deprecated_aliases = ["zed_actions::OpenSettings"])] OpenSettingsFile, - /// Opens the settings editor. /// Opens the default keymap file. OpenDefaultKeymap, /// Opens the user keymap file. @@ -107,6 +107,16 @@ pub struct IncreaseBufferFontSize { pub persist: bool, } +/// Increases the font size in the editor buffer. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = zed)] +#[serde(deny_unknown_fields)] +pub struct OpenSettingsAt { + /// A path to a specific setting (e.g. `theme.mode`) + #[serde(default)] + pub path: String, +} + /// Resets the buffer font size to the default value. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = zed)] From 14b41b122f38c76374b5285ea2bd4c649544642b Mon Sep 17 00:00:00 2001 From: localcc Date: Wed, 22 Oct 2025 14:04:30 +0200 Subject: [PATCH 147/202] Fix JumpHost on Windows (#40713) Closes #39382 Release Notes: - Fixed Windows specific ssh jumphost connection issues --- crates/remote/src/transport/ssh.rs | 184 ++++++++++++++++++++++------- 1 file changed, 144 insertions(+), 40 deletions(-) diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index e119e3a2edbd166990076820bf8056821555fde8..a1337c2d65c74b882e19dd832359e297a13b9236 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -34,7 +34,7 @@ use util::{ pub(crate) struct SshRemoteConnection { socket: SshSocket, - master_process: Mutex>, + master_process: Mutex>, remote_binary_path: Option>, ssh_platform: RemotePlatform, ssh_path_style: PathStyle, @@ -80,6 +80,129 @@ struct SshSocket { _proxy: askpass::PasswordProxy, } +struct MasterProcess { + process: Child, +} + +#[cfg(not(target_os = "windows"))] +impl MasterProcess { + pub fn new( + askpass_script_path: &std::ffi::OsStr, + additional_args: Vec, + socket_path: &std::path::Path, + url: &str, + ) -> Result { + let args = [ + "-N", + "-o", + "ControlPersist=no", + "-o", + "ControlMaster=yes", + "-o", + ]; + + let mut master_process = util::command::new_smol_command("ssh"); + master_process + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("SSH_ASKPASS_REQUIRE", "force") + .env("SSH_ASKPASS", askpass_script_path) + .args(additional_args) + .args(args); + + master_process.arg(format!("ControlPath={}", socket_path.display())); + + let process = master_process.arg(&url).spawn()?; + + Ok(MasterProcess { process }) + } + + pub async fn wait_connected(&mut self) -> Result<()> { + let Some(mut stdout) = self.process.stdout.take() else { + anyhow::bail!("ssh process stdout capture failed"); + }; + + let mut output = Vec::new(); + stdout.read_to_end(&mut output).await?; + Ok(()) + } +} + +#[cfg(target_os = "windows")] +impl MasterProcess { + const CONNECTION_ESTABLISHED_MAGIC: &str = "ZED_SSH_CONNECTION_ESTABLISHED"; + + pub fn new( + askpass_script_path: &std::ffi::OsStr, + additional_args: Vec, + url: &str, + ) -> Result { + // On Windows, `ControlMaster` and `ControlPath` are not supported: + // https://github.com/PowerShell/Win32-OpenSSH/issues/405 + // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope + // + // Using an ugly workaround to detect connection establishment + // -N doesn't work with JumpHosts as windows openssh never closes stdin in that case + let args = [ + "-t", + &format!("echo '{}'; exec $0", Self::CONNECTION_ESTABLISHED_MAGIC), + ]; + + let mut master_process = util::command::new_smol_command("ssh"); + master_process + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("SSH_ASKPASS_REQUIRE", "force") + .env("SSH_ASKPASS", askpass_script_path) + .args(additional_args) + .arg(url) + .args(args); + + let process = master_process.spawn()?; + + Ok(MasterProcess { process }) + } + + pub async fn wait_connected(&mut self) -> Result<()> { + use smol::io::AsyncBufReadExt; + + let Some(stdout) = self.process.stdout.take() else { + anyhow::bail!("ssh process stdout capture failed"); + }; + + let mut reader = smol::io::BufReader::new(stdout); + + let mut line = String::new(); + + loop { + let n = reader.read_line(&mut line).await?; + if n == 0 { + anyhow::bail!("ssh process exited before connection established"); + } + + if line.contains(Self::CONNECTION_ESTABLISHED_MAGIC) { + return Ok(()); + } + } + } +} + +impl AsRef for MasterProcess { + fn as_ref(&self) -> &Child { + &self.process + } +} + +impl AsMut for MasterProcess { + fn as_mut(&mut self) -> &mut Child { + &mut self.process + } +} + macro_rules! shell_script { ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ format!( @@ -97,8 +220,8 @@ impl RemoteConnection for SshRemoteConnection { let Some(mut process) = self.master_process.lock().take() else { return Ok(()); }; - process.kill().ok(); - process.status().await?; + process.as_mut().kill().ok(); + process.as_mut().status().await?; Ok(()) } @@ -302,45 +425,25 @@ impl SshRemoteConnection { #[cfg(not(target_os = "windows"))] let socket_path = temp_dir.path().join("ssh.sock"); - let mut master_process = { - #[cfg(not(target_os = "windows"))] - let args = [ - "-N", - "-o", - "ControlPersist=no", - "-o", - "ControlMaster=yes", - "-o", - ]; - // On Windows, `ControlMaster` and `ControlPath` are not supported: - // https://github.com/PowerShell/Win32-OpenSSH/issues/405 - // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope - #[cfg(target_os = "windows")] - let args = ["-N"]; - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass.script_path()) - .args(connection_options.additional_args()) - .args(args); - #[cfg(not(target_os = "windows"))] - master_process.arg(format!("ControlPath={}", socket_path.display())); - master_process.arg(&url).spawn()? - }; - // Wait for this ssh process to close its stdout, indicating that authentication - // has completed. - let mut stdout = master_process.stdout.take().unwrap(); - let mut output = Vec::new(); + #[cfg(target_os = "windows")] + let mut master_process = MasterProcess::new( + askpass.script_path().as_ref(), + connection_options.additional_args(), + &url, + )?; + #[cfg(not(target_os = "windows"))] + let mut master_process = MasterProcess::new( + askpass.script_path().as_ref(), + connection_options.additional_args(), + &socket_path, + &url, + )?; let result = select_biased! { result = askpass.run().fuse() => { match result { AskPassResult::CancelledByUser => { - master_process.kill().ok(); + master_process.as_mut().kill().ok(); anyhow::bail!("SSH connection canceled") } AskPassResult::Timedout => { @@ -348,7 +451,7 @@ impl SshRemoteConnection { } } } - _ = stdout.read_to_end(&mut output).fuse() => { + _ = master_process.wait_connected().fuse() => { anyhow::Ok(()) } }; @@ -357,9 +460,10 @@ impl SshRemoteConnection { return Err(e.context("Failed to connect to host")); } - if master_process.try_status()?.is_some() { + if master_process.as_mut().try_status()?.is_some() { + let mut output = Vec::new(); output.clear(); - let mut stderr = master_process.stderr.take().unwrap(); + let mut stderr = master_process.as_mut().stderr.take().unwrap(); stderr.read_to_end(&mut output).await?; let error_message = format!( From d53efe4e9177ce08a55702967fd00555a4ac4724 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 22 Oct 2025 15:35:25 +0200 Subject: [PATCH 148/202] Revert "gpui: Fix uniform list scrolling with vertical padding present" (#40891) Reverts zed-industries/zed#40719 This unveiled some bigger issues with the UniformList size computations, which are more crucial than what was fixed here. Release Notes: - NOTE: BUGFIX "Fixed a rare issue where the extension page would stutter while scrolling." was reverted due to some other issues --- crates/gpui/src/elements/uniform_list.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 3b721d4238785a12e54b79400324807270501e6c..949d4339e616cd9f49b3783f46da0f80424c474f 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -364,7 +364,17 @@ impl Element for UniformList { content_size, window, cx, - |_style, mut scroll_offset, hitbox, window, cx| { + |style, mut scroll_offset, hitbox, window, cx| { + let border = style.border_widths.to_pixels(window.rem_size()); + let padding = style + .padding + .to_pixels(bounds.size.into(), window.rem_size()); + + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top), + bounds.bottom_right() - point(border.right + padding.right, border.bottom), + ); + let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { let scroll_state = scroll_handle.0.borrow(); scroll_state.y_flipped From 88bac4d5fc90f08ad1850a664122d1c796b9b36a Mon Sep 17 00:00:00 2001 From: Ajani Bilby Date: Thu, 23 Oct 2025 01:04:29 +1100 Subject: [PATCH 149/202] docs: Change tab character representation in docs (#40667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suggested `→` appears tiny, and almost looks like just a dot on my monitor, and I got quite confused for a while thinking the `whitespace_map.tab` setting wasn't working properly. Image I think it would be really helpful if `⟶` was suggested instead since that displays properly. Image --- I am using `Fira Code` as my font on windows, however when I remove that config to get the default font, it also still appears the same size. So I don't believe this is just a font issue on my machine. Thought I am using Windows, so I would be willing to believe this a render issue specific to windows Release Notes: - N/A --- docs/src/visual-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index a31f4428cd9d554ce366e182da605a71eefe6eec..b353377dd764d2506abd4cce46352df3ca47dfcb 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -182,7 +182,7 @@ TBD: Centered layout related settings "show_whitespaces": "selection", "whitespace_map": { // Which characters to show when `show_whitespaces` enabled "space": "•", - "tab": "→" + "tab": "⟶" // use "→", for a shorter arrow }, "unnecessary_code_fade": 0.3, // How much to fade out unused code. From 69b2ee7bf0b714e863f528d2c8d64c95359f87c0 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 10:50:29 -0400 Subject: [PATCH 150/202] Bump Zed to v0.211 (#40895) Release Notes: - N/A --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d57553190ec0e659dab8de9b60a57eb87fc6356..cbd977bd6ed4089d281697145105fbc48ac03f2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20918,7 +20918,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.210.0" +version = "0.211.0" dependencies = [ "acp_tools", "activity_indicator", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 409bee33ee4dee6562f6f8ea1388a2063f639ea3..c84fa8261fe2efdc4c8c831fcd239514c2d16526 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.210.0" +version = "0.211.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From d0398da09955e2642eb3c6ec0d1eb7bb509f4355 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 22 Oct 2025 16:59:30 +0200 Subject: [PATCH 151/202] editor: Fix singleton multibuffer titles not being replicated (#40896) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/items.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6a5552f8c7a689e310c548e11c3a516fb9aedbe3..28a416925672a937a163e85fcaa59066529481b1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -226,7 +226,7 @@ impl FollowableItem for Editor { Some(proto::view::Variant::Editor(proto::view::Editor { singleton: buffer.is_singleton(), - title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()), + title: buffer.explicit_title().map(ToOwned::to_owned), excerpts, scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor, &snapshot)), scroll_x: scroll_anchor.offset.x, diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e9e3b6f62c2bd5ec4a40ea8329aaf05110f91173..e6e2f7f1c68f976473b5e6ee8b60ca8652aa4b1d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -2556,6 +2556,10 @@ impl MultiBuffer { self.buffers.values().for_each(|state| f(&state.buffer)) } + pub fn explicit_title(&self) -> Option<&str> { + self.title.as_deref() + } + pub fn title<'a>(&'a self, cx: &'a App) -> Cow<'a, str> { if let Some(title) = self.title.as_ref() { return title.into(); From 3a12122d1b4f70464602d713be1e1d372c316a31 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Wed, 22 Oct 2025 17:27:37 +0200 Subject: [PATCH 152/202] Revert "keymaps: Update defaults for inline assist and signature help" (#40903) Reverts zed-industries/zed#39587 --- assets/keymaps/default-linux.json | 6 +++--- assets/keymaps/default-macos.json | 6 +++--- assets/keymaps/default-windows.json | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 51cf0b03a56a03aaaa9cc8ad6550d8debfda0df7..7774d4b3960de44455104c41b82d1c8243d3b78b 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -539,7 +539,7 @@ "ctrl-k ctrl-0": "editor::FoldAll", "ctrl-k ctrl-j": "editor::UnfoldAll", "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowSignatureHelp", + "ctrl-shift-space": "editor::ShowWordCompletions", "ctrl-.": "editor::ToggleCodeActions", "ctrl-k r": "editor::RevealInFileManager", "ctrl-k p": "editor::CopyPath", @@ -799,7 +799,7 @@ "ctrl-shift-e": "pane::RevealInProjectPanel", "ctrl-f8": "editor::GoToHunk", "ctrl-shift-f8": "editor::GoToPreviousHunk", - "ctrl-i": "assistant::InlineAssist", + "ctrl-enter": "assistant::InlineAssist", "ctrl-:": "editor::ToggleInlayHints" } }, @@ -1094,7 +1094,7 @@ "paste": "terminal::Paste", "shift-insert": "terminal::Paste", "ctrl-shift-v": "terminal::Paste", - "ctrl-i": "assistant::InlineAssist", + "ctrl-enter": "assistant::InlineAssist", "alt-b": ["terminal::SendText", "\u001bb"], "alt-f": ["terminal::SendText", "\u001bf"], "alt-.": ["terminal::SendText", "\u001b."], diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 97846a8edf63cae577eb17d49ee835b43295be35..33282d2df58f6a95e257d726cd6466f4e3560e7c 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -142,7 +142,7 @@ "cmd-\"": "editor::ExpandAllDiffHunks", "cmd-alt-g b": "git::Blame", "cmd-alt-g m": "git::OpenModifiedFiles", - "cmd-shift-space": "editor::ShowSignatureHelp", + "cmd-i": "editor::ShowSignatureHelp", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint", "ctrl-f12": "editor::GoToDeclaration", @@ -864,7 +864,7 @@ "cmd-shift-e": "pane::RevealInProjectPanel", "cmd-f8": "editor::GoToHunk", "cmd-shift-f8": "editor::GoToPreviousHunk", - "cmd-i": "assistant::InlineAssist", + "ctrl-enter": "assistant::InlineAssist", "ctrl-:": "editor::ToggleInlayHints" } }, @@ -1168,7 +1168,7 @@ "cmd-a": "editor::SelectAll", "cmd-k": "terminal::Clear", "cmd-n": "workspace::NewTerminal", - "cmd-i": "assistant::InlineAssist", + "ctrl-enter": "assistant::InlineAssist", "ctrl-_": null, // emacs undo // Some nice conveniences "cmd-backspace": ["terminal::SendText", "\u0015"], // ctrl-u: clear line diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 02bd2207c2805ef5f3eef1b06378f197595ad4a4..44ee25dcce0296aa318b1940619c8020da87fac0 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -548,7 +548,7 @@ "ctrl-k ctrl-0": "editor::FoldAll", "ctrl-k ctrl-j": "editor::UnfoldAll", "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowSignatureHelp", + "ctrl-shift-space": "editor::ShowWordCompletions", "ctrl-.": "editor::ToggleCodeActions", "ctrl-k r": "editor::RevealInFileManager", "ctrl-k p": "editor::CopyPath", @@ -812,7 +812,7 @@ "ctrl-shift-e": "pane::RevealInProjectPanel", "ctrl-f8": "editor::GoToHunk", "ctrl-shift-f8": "editor::GoToPreviousHunk", - "ctrl-i": "assistant::InlineAssist", + "ctrl-enter": "assistant::InlineAssist", "ctrl-shift-;": "editor::ToggleInlayHints" } }, @@ -1120,7 +1120,7 @@ "shift-insert": "terminal::Paste", "ctrl-v": "terminal::Paste", "ctrl-shift-v": "terminal::Paste", - "ctrl-i": "assistant::InlineAssist", + "ctrl-enter": "assistant::InlineAssist", "alt-b": ["terminal::SendText", "\u001bb"], "alt-f": ["terminal::SendText", "\u001bf"], "alt-.": ["terminal::SendText", "\u001b."], From a24601903aa1e75e89527c8f455557717a330b07 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:56:11 -0300 Subject: [PATCH 153/202] agent: Improve discoverability of the quote selection action (#40897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR renames the `agent::QuoteSelection` to `agent::AddSelectionToThread` _and_ adds it as a menu item in both the right-click context menu within regular buffers as well as the "Selection" app menu. We've received feedback in the past about how hard to discover this feature is, and after watching [the Syntax podcast crew](https://www.youtube.com/watch?v=bRK3PeVFfVE) recently struggle with doing so—and then naturally looking for it in the context menu and not finding it—it felt like time to push a change. I think the rename + the availability in these places could help bringing it to surface more. The same action can be done in Cursor through the `cmd-l` keybinding, but in Zed, that triggers `editor::SelectLine`, which I don't want to override by default. However, if you're using Cursor's keymap, then `cmd-l` does trigger this action, as expected. Screenshot 2025-10-22 at 12  01@2x Release Notes: - agent: Improves discoverability of the previously called "quote selection" action—which allows to add a text selection in a buffer as context within the agent panel—by renaming it to "add selection to thread" and making it available from the right-click editor context menu as well as the "Selection" app menu. --- assets/keymaps/default-linux.json | 4 ++-- assets/keymaps/default-macos.json | 4 ++-- assets/keymaps/default-windows.json | 4 ++-- assets/keymaps/linux/cursor.json | 4 ++-- assets/keymaps/macos/cursor.json | 4 ++-- crates/agent_ui/src/agent_ui.rs | 6 ------ crates/agent_ui/src/text_thread_editor.rs | 5 ++--- crates/editor/src/mouse_context_menu.rs | 2 ++ crates/zed/src/zed/app_menus.rs | 4 +++- crates/zed_actions/src/lib.rs | 5 ++++- docs/src/ai/text-threads.md | 4 ++-- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 7774d4b3960de44455104c41b82d1c8243d3b78b..dd599acfe6fb5df175ca794d48a3407f4aebb9e1 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -139,7 +139,7 @@ "find": "buffer_search::Deploy", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl->": "agent::QuoteSelection", + "ctrl->": "agent::AddSelectionToThread", "ctrl-<": "assistant::InsertIntoEditor", "ctrl-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -243,7 +243,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl->": "agent::QuoteSelection", + "ctrl->": "agent::AddSelectionToThread", "ctrl-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 33282d2df58f6a95e257d726cd6466f4e3560e7c..a3cc5e6c6bce709c0ddc7b4a8437847dbdce9ed9 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -163,7 +163,7 @@ "cmd-alt-f": "buffer_search::DeployReplace", "cmd-alt-l": ["buffer_search::Deploy", { "selection_search_enabled": true }], "cmd-e": ["buffer_search::Deploy", { "focus": false }], - "cmd->": "agent::QuoteSelection", + "cmd->": "agent::AddSelectionToThread", "cmd-<": "assistant::InsertIntoEditor", "cmd-alt-e": "editor::SelectEnclosingSymbol", "alt-enter": "editor::OpenSelectionsInMultibuffer" @@ -282,7 +282,7 @@ "cmd-shift-i": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "cmd->": "agent::QuoteSelection", + "cmd->": "agent::AddSelectionToThread", "cmd-alt-e": "agent::RemoveAllContext", "cmd-shift-e": "project_panel::ToggleFocus", "cmd-ctrl-b": "agent::ToggleBurnMode", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 44ee25dcce0296aa318b1940619c8020da87fac0..3507bfd2f0b6e10147fad728228fae55d99c9157 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -134,7 +134,7 @@ "ctrl-k z": "editor::ToggleSoftWrap", "ctrl-f": "buffer_search::Deploy", "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "agent::QuoteSelection", + "ctrl-shift-.": "agent::AddSelectionToThread", "ctrl-shift-,": "assistant::InsertIntoEditor", "shift-alt-e": "editor::SelectEnclosingSymbol", "ctrl-shift-backspace": "editor::GoToPreviousChange", @@ -244,7 +244,7 @@ "ctrl-shift-i": "agent::ToggleOptionsMenu", // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "agent::QuoteSelection", + "ctrl-shift-.": "agent::AddSelectionToThread", "shift-alt-e": "agent::RemoveAllContext", "ctrl-shift-e": "project_panel::ToggleFocus", "ctrl-shift-enter": "agent::ContinueThread", diff --git a/assets/keymaps/linux/cursor.json b/assets/keymaps/linux/cursor.json index 2e27158e1167f0840cadfb0d86dc06614f6076c6..4d2d13a90d96c31f72b1bb0ccc74608f81004eda 100644 --- a/assets/keymaps/linux/cursor.json +++ b/assets/keymaps/linux/cursor.json @@ -17,8 +17,8 @@ "bindings": { "ctrl-i": "agent::ToggleFocus", "ctrl-shift-i": "agent::ToggleFocus", - "ctrl-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode - "ctrl-l": "agent::QuoteSelection", // In cursor uses "Agent" mode + "ctrl-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode + "ctrl-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode "ctrl-k": "assistant::InlineAssist", "ctrl-shift-k": "assistant::InsertIntoEditor" } diff --git a/assets/keymaps/macos/cursor.json b/assets/keymaps/macos/cursor.json index 1d723bd75bb788aa1ea63335f9fa555cb50d2df0..97abc7dd819485850107eca6762fc1ed60ec0515 100644 --- a/assets/keymaps/macos/cursor.json +++ b/assets/keymaps/macos/cursor.json @@ -17,8 +17,8 @@ "bindings": { "cmd-i": "agent::ToggleFocus", "cmd-shift-i": "agent::ToggleFocus", - "cmd-shift-l": "agent::QuoteSelection", // In cursor uses "Ask" mode - "cmd-l": "agent::QuoteSelection", // In cursor uses "Agent" mode + "cmd-shift-l": "agent::AddSelectionToThread", // In cursor uses "Ask" mode + "cmd-l": "agent::AddSelectionToThread", // In cursor uses "Agent" mode "cmd-k": "assistant::InlineAssist", "cmd-shift-k": "assistant::InsertIntoEditor" } diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 7c31500c937a6513c932c66560cf8754cbafbf1c..cc0d212a86f5db3b0d5cf8ad4b0457689512f33c 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -130,12 +130,6 @@ actions!( ] ); -#[derive(Clone, Copy, Debug, PartialEq, Eq, Action)] -#[action(namespace = agent)] -#[action(deprecated_aliases = ["assistant::QuoteSelection"])] -/// Quotes the current selection in the agent panel's message editor. -pub struct QuoteSelection; - /// Creates a new conversation thread, optionally based on an existing thread. #[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 2d28d95450726554787f6a9cb211e852ceaccddf..408aecccfa7fa71aaf15e4c085ad31dce8a1d922 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,5 +1,4 @@ use crate::{ - QuoteSelection, language_model_selector::{LanguageModelSelector, language_model_selector}, ui::BurnModeTooltip, }; @@ -72,7 +71,7 @@ use workspace::{ pane, searchable::{SearchEvent, SearchableItem}, }; -use zed_actions::agent::ToggleModelSelector; +use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector}; use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker}; use assistant_context::{ @@ -1450,7 +1449,7 @@ impl TextThreadEditor { pub fn quote_selection( workspace: &mut Workspace, - _: &QuoteSelection, + _: &AddSelectionToThread, window: &mut Window, cx: &mut Context, ) { diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index cef691dec483c8a9ae978499689db69b14c5dffe..7c83113f7837565efc59889e74bf397b392c516b 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -11,6 +11,7 @@ use gpui::{Context, DismissEvent, Entity, Focusable as _, Pixels, Point, Subscri use std::ops::Range; use text::PointUtf16; use workspace::OpenInTerminal; +use zed_actions::agent::AddSelectionToThread; #[derive(Debug)] pub enum MenuPosition { @@ -233,6 +234,7 @@ pub fn deploy_context_menu( quick_launch: false, }), ) + .action("Add to Agent Thread", Box::new(AddSelectionToThread)) .separator() .action("Cut", Box::new(Cut)) .action("Copy", Box::new(Copy)) diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index e9bfb6d92a710177308dbd87b0fdc1129343a6a4..2baf4f708c29a7bec11e8aa26a1897a20a75e3c9 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -2,7 +2,7 @@ use collab_ui::collab_panel; use gpui::{App, Menu, MenuItem, OsAction}; use release_channel::ReleaseChannel; use terminal_view::terminal_panel; -use zed_actions::{ToggleFocus as ToggleDebugPanel, dev}; +use zed_actions::{ToggleFocus as ToggleDebugPanel, agent::AddSelectionToThread, dev}; pub fn app_menus(cx: &mut App) -> Vec { use zed_actions::Quit; @@ -214,6 +214,8 @@ pub fn app_menus(cx: &mut App) -> Vec { MenuItem::action("Move Line Up", editor::actions::MoveLineUp), MenuItem::action("Move Line Down", editor::actions::MoveLineDown), MenuItem::action("Duplicate Selection", editor::actions::DuplicateLineDown), + MenuItem::separator(), + MenuItem::action("Add to Agent Thread", AddSelectionToThread), ], }, Menu { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 521405edc29845f6d459b3924355a5d8ea4a1bf8..13a2837efb3240a400151b3bcd5342d8300f8730 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -306,7 +306,10 @@ pub mod agent { #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] ToggleModelSelector, /// Triggers re-authentication on Gemini - ReauthenticateAgent + ReauthenticateAgent, + /// Add the current selection as context for threads in the agent panel. + #[action(deprecated_aliases = ["assistant::QuoteSelection", "agent::QuoteSelection"])] + AddSelectionToThread, ] ); } diff --git a/docs/src/ai/text-threads.md b/docs/src/ai/text-threads.md index ed439252b4d1612ea1b20269c6286e2b94685ac2..4e7e7904cf53e1e7e141b29c777a6f53796177cf 100644 --- a/docs/src/ai/text-threads.md +++ b/docs/src/ai/text-threads.md @@ -16,7 +16,7 @@ To begin, type a message in a `You` block. As you type, the remaining tokens count for the selected model is updated. -Inserting text from an editor is as simple as highlighting the text and running `agent: quote selection` ({#kb agent::QuoteSelection}); Zed will wrap it in a fenced code block if it is code. +Inserting text from an editor is as simple as highlighting the text and running `agent: add selection to thread` ({#kb agent::AddSelectionToThread}); Zed will wrap it in a fenced code block if it is code. ![Quoting a selection](https://zed.dev/img/assistant/quoting-a-selection.png) @@ -148,7 +148,7 @@ Usage: `/terminal []` The `/selection` command inserts the selected text in the editor into the context. This is useful for referencing specific parts of your code. -This is equivalent to the `agent: quote selection` command ({#kb agent::QuoteSelection}). +This is equivalent to the `agent: add selection to thread` command ({#kb agent::AddSelectionToThread}). Usage: `/selection` From b207da5a712f7b1d7a1cfecb3fb451017d06f2cb Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 22 Oct 2025 18:33:16 +0200 Subject: [PATCH 154/202] gpui: Re-land uniform list scroll fixes (#40899) Re-lands https://github.com/zed-industries/zed/pull/40719, fixes the bugs that were discovered with it and improves some more stuff in that area Release Notes: - Fixed a rare issue where the extension page would stutter while scrolling. --------- Co-authored-by: Agus Zubiaga --- crates/gpui/src/elements/uniform_list.rs | 63 ++++++++---------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 949d4339e616cd9f49b3783f46da0f80424c474f..93082563c02f4168b1d73e2929a6bf9dbd153237 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -343,7 +343,7 @@ impl Element for UniformList { }; let content_size = Size { width: content_width, - height: longest_item_size.height * self.item_count + padding.top + padding.bottom, + height: longest_item_size.height * self.item_count, }; let shared_scroll_offset = self.interactivity.scroll_offset.clone().unwrap(); @@ -364,17 +364,7 @@ impl Element for UniformList { content_size, window, cx, - |style, mut scroll_offset, hitbox, window, cx| { - let border = style.border_widths.to_pixels(window.rem_size()); - let padding = style - .padding - .to_pixels(bounds.size.into(), window.rem_size()); - - let padded_bounds = Bounds::from_corners( - bounds.origin + point(border.left + padding.left, border.top), - bounds.bottom_right() - point(border.right + padding.right, border.bottom), - ); - + |_style, mut scroll_offset, hitbox, window, cx| { let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { let scroll_state = scroll_handle.0.borrow(); scroll_state.y_flipped @@ -383,13 +373,14 @@ impl Element for UniformList { }; if self.item_count > 0 { - let content_height = - item_height * self.item_count + padding.top + padding.bottom; + let content_height = item_height * self.item_count; + let is_scrolled_vertically = !scroll_offset.y.is_zero(); - let min_vertical_scroll_offset = padded_bounds.size.height - content_height; - if is_scrolled_vertically && scroll_offset.y < min_vertical_scroll_offset { - shared_scroll_offset.borrow_mut().y = min_vertical_scroll_offset; - scroll_offset.y = min_vertical_scroll_offset; + let max_scroll_offset = padded_bounds.size.height - content_height; + + if is_scrolled_vertically && scroll_offset.y < max_scroll_offset { + shared_scroll_offset.borrow_mut().y = max_scroll_offset; + scroll_offset.y = max_scroll_offset; } let content_width = content_size.width + padding.left + padding.right; @@ -407,18 +398,19 @@ impl Element for UniformList { } let list_height = padded_bounds.size.height; let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); - let item_top = item_height * ix + padding.top; + let item_top = item_height * ix; let item_bottom = item_top + item_height; let scroll_top = -updated_scroll_offset.y; let offset_pixels = item_height * deferred_scroll.offset; let mut scrolled_to_top = false; - if item_top < scroll_top + padding.top + offset_pixels { + if item_top < scroll_top + offset_pixels { scrolled_to_top = true; - updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels; - } else if item_bottom > scroll_top + list_height - padding.bottom { + // todo: using the padding here is wrong - this only works well for few scenarios + updated_scroll_offset.y = -item_top + padding.top + offset_pixels; + } else if item_bottom > scroll_top + list_height { scrolled_to_top = true; - updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; + updated_scroll_offset.y = -(item_bottom - list_height); } if deferred_scroll.scroll_strict @@ -480,14 +472,9 @@ impl Element for UniformList { window.with_content_mask(Some(content_mask), |window| { for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { let item_origin = padded_bounds.origin - + point( - if can_scroll_horizontally { - scroll_offset.x + padding.left - } else { - scroll_offset.x - }, - item_height * ix + scroll_offset.y + padding.top, - ); + + scroll_offset + + point(Pixels::ZERO, item_height * ix); + let available_width = if can_scroll_horizontally { padded_bounds.size.width + scroll_offset.x.abs() } else { @@ -502,18 +489,8 @@ impl Element for UniformList { frame_state.items.push(item); } - let bounds = Bounds::new( - padded_bounds.origin - + point( - if can_scroll_horizontally { - scroll_offset.x + padding.left - } else { - scroll_offset.x - }, - scroll_offset.y + padding.top, - ), - padded_bounds.size, - ); + let bounds = + Bounds::new(padded_bounds.origin + scroll_offset, padded_bounds.size); for decoration in &self.decorations { let mut decoration = decoration.as_ref().compute( visible_range.clone(), From 23e9e32d657e4eb0a4f0e650d92e580368446abc Mon Sep 17 00:00:00 2001 From: "Affonso, Guilherme" Date: Thu, 23 Oct 2025 01:37:00 +0900 Subject: [PATCH 155/202] emacs: Improve default keymap to better match the emacs behavior (#40631) Hello, I am having a great time setting up the editor, but with a few problems related to the Emacs keymap. In this PR I have compiled changes in the default `emacs.json` that I believe make the onboarding smoother for incoming emacs users. This includes points that may need further discussion and some breaking changes, although nothing that cannot be reverted with a quick `keymap.json` overwrite. (Please let me know if it is better to split up the PR) ### 1. Avoid fallbacks to the default keymap all platforms: - `ctrl-g` activating `go_to_line::Toggle` when there is nothing to cancel linux / windows: - `ctrl-x` activating `editor::Cut` on the 1 second timeout - `ctrl-p` activating `file_finder::Toggle` when the cursor is on the first character of the buffer - `ctrl-n` activating `workspace::NewFile` when the cursor is on the last character of the buffer ### 2. Make all move commands operate on full words In the current Zed implementation some commands run on full words and others on subwords. Although ultimately a matter of user preference, I think it is sensible to use full words as the default, since that is what is shipped with emacs. ### ~~3. Cancel selections after copy/cut commands~~ Moved to #40904 Canceling the selection is the default emacs behavior, but the way to achieve it might need some brushing. Currently I am using `workspace::SendKeystrokes` to copy -> cancel(`ctrl-g`), but this has the following problems: - can only be used in the main buffer (since `editor::Cancel` would typically close secondary buffers) - may cause problems downstream if the user overwrites the `ctrl-g` binding ### ~~4. Replace killring with normal cut/paste commands~~ Moved to #40905 Ideally Zed would support emacs-like killrings (#25270 and #22490). However, I understand that making an emacs emulator is not a project goal, and the Zed team should have a bunch of tasks with higher priority. By using a unified clipboard and standard cut/paste commands, we can provide an experience that is closer to the out-of-the-box emacs behavior (#33351) while also avoiding some pitfalls of the current killring implementation (#28715). ### 5. Promote some bindings to workspace commands - `alt-x` as `command_palette::Toggle` - `ctrl-x b` and `ctrl-x ctrl-b` as `tab_switcher::Toggle` --- Release Notes: - emacs: Fixed a problem where keys would fallback to their default keymap binding on certain conditions - emacs: Changed `alt-f` and `alt-b` to operate on full words, as in the emacs default - emacs: `alt-x`, `ctrl-x b`, and `ctrl-x ctrl-b` are now Workspace bindings --- assets/keymaps/linux/emacs.json | 46 ++++++++++++++++++++++++++++----- assets/keymaps/macos/emacs.json | 42 +++++++++++++++++++++++++----- 2 files changed, 74 insertions(+), 14 deletions(-) diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 5b8a8e5879cf21100895e4ea1ae7896d62b45d98..c5cf22c81220bf286187252394f8fde26bdd6509 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -8,13 +8,23 @@ "ctrl-g": "menu::Cancel" } }, + { + // Workaround to avoid falling back to default bindings. + // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: must be declared before the `Editor` override. + // NOTE: in macos the 'ctrl-x' 'ctrl-p' and 'ctrl-n' rebindings are not needed, since they default to 'cmd'. + "context": "Editor", + "bindings": { + "ctrl-g": null, // currently activates `go_to_line::Toggle` when there is nothing to cancel + "ctrl-x": null, // currently activates `editor::Cut` if no following key is pressed for 1 second + "ctrl-p": null, // currently activates `file_finder::Toggle` when the cursor is on the first character of the buffer + "ctrl-n": null // currently activates `workspace::NewFile` when the cursor is on the last character of the buffer + } + }, { "context": "Editor", "bindings": { - "alt-x": "command_palette::Toggle", "ctrl-g": "editor::Cancel", - "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer - "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers "alt-g g": "go_to_line::Toggle", // goto-line "alt-g alt-g": "go_to_line::Toggle", // goto-line "ctrl-space": "editor::SetMark", // set-mark @@ -33,8 +43,8 @@ "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation "alt-left": "editor::MoveToPreviousWordStart", // left-word "alt-right": "editor::MoveToNextWordEnd", // right-word - "alt-f": "editor::MoveToNextSubwordEnd", // forward-word - "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word + "alt-f": "editor::MoveToNextWordEnd", // forward-word + "alt-b": "editor::MoveToPreviousWordStart", // backward-word "alt-u": "editor::ConvertToUpperCase", // upcase-word "alt-l": "editor::ConvertToLowerCase", // downcase-word "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word @@ -98,7 +108,7 @@ "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], "alt-f": "editor::SelectToNextWordEnd", - "alt-b": "editor::SelectToPreviousSubwordStart", + "alt-b": "editor::SelectToPreviousWordStart", "alt-{": "editor::SelectToStartOfParagraph", "alt-}": "editor::SelectToEndOfParagraph", "ctrl-up": "editor::SelectToStartOfParagraph", @@ -126,15 +136,28 @@ "ctrl-n": "editor::SignatureHelpNext" } }, + // Example setting for using emacs-style tab + // (i.e. indent the current line / selection or perform symbol completion depending on context) + // { + // "context": "Editor && !showing_code_actions && !showing_completions", + // "bindings": { + // "tab": "editor::AutoIndent" // indent-for-tab-command + // } + // }, { "context": "Workspace", "bindings": { + "alt-x": "command_palette::Toggle", // execute-extended-command + "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer + "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers + // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command "ctrl-x o": "workspace::ActivateNextPane", // other-window "ctrl-x k": "pane::CloseActiveItem", // kill-buffer "ctrl-x 0": "pane::CloseActiveItem", // delete-window + // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows "ctrl-x 2": "pane::SplitDown", // split-window-below "ctrl-x 3": "pane::SplitRight", // split-window-right @@ -145,10 +168,19 @@ } }, { - // Workaround to enable using emacs in the Zed terminal. + // Workaround to enable using native emacs from the Zed terminal. // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: + // "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x), + // so override with null for compound sequences (e.g. ctrl-x ctrl-c). "context": "Terminal", "bindings": { + // If you want to perfect your emacs-in-zed setup, also consider the following. + // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work. + // "alt-x": ["terminal::SendKeystroke", "alt-x"], + // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"], + // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"], + // ... "ctrl-x ctrl-c": null, // save-buffers-kill-terminal "ctrl-x ctrl-f": null, // find-file "ctrl-x ctrl-s": null, // save-buffer diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index da815f14cecb2dd3fee403c5e1e54a383dd564a4..ea831c0c059ea082d002f3af01b8d97be9e86616 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -9,13 +9,19 @@ "ctrl-g": "menu::Cancel" } }, + { + // Workaround to avoid falling back to default bindings. + // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: must be declared before the `Editor` override. + "context": "Editor", + "bindings": { + "ctrl-g": null // currently activates `go_to_line::Toggle` when there is nothing to cancel + } + }, { "context": "Editor", "bindings": { - "alt-x": "command_palette::Toggle", "ctrl-g": "editor::Cancel", - "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer - "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers "alt-g g": "go_to_line::Toggle", // goto-line "alt-g alt-g": "go_to_line::Toggle", // goto-line "ctrl-space": "editor::SetMark", // set-mark @@ -34,8 +40,8 @@ "alt-m": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], // back-to-indentation "alt-left": "editor::MoveToPreviousWordStart", // left-word "alt-right": "editor::MoveToNextWordEnd", // right-word - "alt-f": "editor::MoveToNextSubwordEnd", // forward-word - "alt-b": "editor::MoveToPreviousSubwordStart", // backward-word + "alt-f": "editor::MoveToNextWordEnd", // forward-word + "alt-b": "editor::MoveToPreviousWordStart", // backward-word "alt-u": "editor::ConvertToUpperCase", // upcase-word "alt-l": "editor::ConvertToLowerCase", // downcase-word "alt-c": "editor::ConvertToUpperCamelCase", // capitalize-word @@ -99,7 +105,7 @@ "ctrl-e": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": false }], "alt-m": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": false, "stop_at_indent": true }], "alt-f": "editor::SelectToNextWordEnd", - "alt-b": "editor::SelectToPreviousSubwordStart", + "alt-b": "editor::SelectToPreviousWordStart", "alt-{": "editor::SelectToStartOfParagraph", "alt-}": "editor::SelectToEndOfParagraph", "ctrl-up": "editor::SelectToStartOfParagraph", @@ -127,15 +133,28 @@ "ctrl-n": "editor::SignatureHelpNext" } }, + // Example setting for using emacs-style tab + // (i.e. indent the current line / selection or perform symbol completion depending on context) + // { + // "context": "Editor && !showing_code_actions && !showing_completions", + // "bindings": { + // "tab": "editor::AutoIndent" // indent-for-tab-command + // } + // }, { "context": "Workspace", "bindings": { + "alt-x": "command_palette::Toggle", // execute-extended-command + "ctrl-x b": "tab_switcher::Toggle", // switch-to-buffer + "ctrl-x ctrl-b": "tab_switcher::Toggle", // list-buffers + // "ctrl-x ctrl-c": "workspace::CloseWindow" // in case you only want to exit the current Zed instance "ctrl-x ctrl-c": "zed::Quit", // save-buffers-kill-terminal "ctrl-x 5 0": "workspace::CloseWindow", // delete-frame "ctrl-x 5 2": "workspace::NewWindow", // make-frame-command "ctrl-x o": "workspace::ActivateNextPane", // other-window "ctrl-x k": "pane::CloseActiveItem", // kill-buffer "ctrl-x 0": "pane::CloseActiveItem", // delete-window + // "ctrl-x 1": "pane::JoinAll", // in case you prefer to delete the splits but keep the buffers open "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows "ctrl-x 2": "pane::SplitDown", // split-window-below "ctrl-x 3": "pane::SplitRight", // split-window-right @@ -146,10 +165,19 @@ } }, { - // Workaround to enable using emacs in the Zed terminal. + // Workaround to enable using native emacs from the Zed terminal. // Unbind so Zed ignores these keys and lets emacs handle them. + // NOTE: + // "terminal::SendKeystroke" only works for a single key stroke (e.g. ctrl-x), + // so override with null for compound sequences (e.g. ctrl-x ctrl-c). "context": "Terminal", "bindings": { + // If you want to perfect your emacs-in-zed setup, also consider the following. + // You may need to enable "option_as_meta" from the Zed settings for "alt-x" to work. + // "alt-x": ["terminal::SendKeystroke", "alt-x"], + // "ctrl-x": ["terminal::SendKeystroke", "ctrl-x"], + // "ctrl-n": ["terminal::SendKeystroke", "ctrl-n"], + // ... "ctrl-x ctrl-c": null, // save-buffers-kill-terminal "ctrl-x ctrl-f": null, // find-file "ctrl-x ctrl-s": null, // save-buffer From 96a0db24d97361bab5603c42b229b2d6a5d8f262 Mon Sep 17 00:00:00 2001 From: Donnie Adams Date: Wed, 22 Oct 2025 13:07:20 -0400 Subject: [PATCH 156/202] Add comment injections for gowork and gomod (#40842) Release Notes: - Add comment injections for go.mod and go.work Signed-off-by: Donnie Adams --- crates/languages/src/gomod/injections.scm | 2 ++ crates/languages/src/gowork/injections.scm | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 crates/languages/src/gomod/injections.scm create mode 100644 crates/languages/src/gowork/injections.scm diff --git a/crates/languages/src/gomod/injections.scm b/crates/languages/src/gomod/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..321c90add3710f35721daeb6b42abe38af094953 --- /dev/null +++ b/crates/languages/src/gomod/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/crates/languages/src/gowork/injections.scm b/crates/languages/src/gowork/injections.scm new file mode 100644 index 0000000000000000000000000000000000000000..321c90add3710f35721daeb6b42abe38af094953 --- /dev/null +++ b/crates/languages/src/gowork/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) From d5580050582a91bd2af9f7c4355c6a5b917bfa99 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Wed, 22 Oct 2025 19:10:37 +0200 Subject: [PATCH 157/202] markdown: Add support for `colspan` and `rowspan` for HTML tables (#39898) Closes https://github.com/zed-industries/zed/issues/39837 This PR adds support for `colspan` feature that is only supported for HTML tables. I also fixed an edge case where the right side border was not applied because it didn't match the total column count. **Before** 499166907-385cc787-fc89-4e6d-bf06-c72c3c0bd775 **After** Screenshot 2025-10-21 at 22 51 55 ```html
Region Revenue Growth
Q2 2024 Q3 2024
North America $2.8M $2.4B +85,614%
Europe $1.2M $1.9B +158,233%
Asia-Pacific $0.5M $1.4B +279,900%
``` **TODO**: - [x] Add tests for rending logic - [x] Test all the tables again cc @bennetbo Release Notes: - Markdown: Added support for `colspan` and `rowspan` for HTML tables --------- Co-authored-by: Zed AI Co-authored-by: Anthony Eid --- .../markdown_preview/src/markdown_elements.rs | 23 +- .../markdown_preview/src/markdown_parser.rs | 175 +++++++-- .../markdown_preview/src/markdown_renderer.rs | 334 ++++++++++++------ 3 files changed, 379 insertions(+), 153 deletions(-) diff --git a/crates/markdown_preview/src/markdown_elements.rs b/crates/markdown_preview/src/markdown_elements.rs index d0bde48889143b8ab9f66d9dc2839ebabf7d3541..b0a36a4cf29c386204f6fd1a347a839009e1c357 100644 --- a/crates/markdown_preview/src/markdown_elements.rs +++ b/crates/markdown_preview/src/markdown_elements.rs @@ -104,25 +104,34 @@ pub enum HeadingLevel { #[derive(Debug)] pub struct ParsedMarkdownTable { pub source_range: Range, - pub header: ParsedMarkdownTableRow, + pub header: Vec, pub body: Vec, pub column_alignments: Vec, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] #[cfg_attr(test, derive(PartialEq))] pub enum ParsedMarkdownTableAlignment { - /// Default text alignment. + #[default] None, Left, Center, Right, } +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ParsedMarkdownTableColumn { + pub col_span: usize, + pub row_span: usize, + pub is_header: bool, + pub children: MarkdownParagraph, +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] pub struct ParsedMarkdownTableRow { - pub children: Vec, + pub columns: Vec, } impl Default for ParsedMarkdownTableRow { @@ -134,12 +143,12 @@ impl Default for ParsedMarkdownTableRow { impl ParsedMarkdownTableRow { pub fn new() -> Self { Self { - children: Vec::new(), + columns: Vec::new(), } } - pub fn with_children(children: Vec) -> Self { - Self { children } + pub fn with_columns(columns: Vec) -> Self { + Self { columns } } } diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs index ad175922daaa20508852f36dd4f62ad90199b7ca..28388923a75f14c601dcafecb2008570e309561f 100644 --- a/crates/markdown_preview/src/markdown_parser.rs +++ b/crates/markdown_preview/src/markdown_parser.rs @@ -462,9 +462,9 @@ impl<'a> MarkdownParser<'a> { fn parse_table(&mut self, alignment: Vec) -> ParsedMarkdownTable { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); - let mut header = ParsedMarkdownTableRow::new(); + let mut header = vec![]; let mut body = vec![]; - let mut current_row = vec![]; + let mut row_columns = vec![]; let mut in_header = true; let column_alignments = alignment.iter().map(Self::convert_alignment).collect(); @@ -484,17 +484,21 @@ impl<'a> MarkdownParser<'a> { Event::Start(Tag::TableCell) => { self.cursor += 1; let cell_contents = self.parse_text(false, Some(source_range)); - current_row.push(cell_contents); + row_columns.push(ParsedMarkdownTableColumn { + col_span: 1, + row_span: 1, + is_header: in_header, + children: cell_contents, + }); } Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { self.cursor += 1; - let new_row = std::mem::take(&mut current_row); + let columns = std::mem::take(&mut row_columns); if in_header { - header.children = new_row; + header.push(ParsedMarkdownTableRow { columns: columns }); in_header = false; } else { - let row = ParsedMarkdownTableRow::with_children(new_row); - body.push(row); + body.push(ParsedMarkdownTableRow::with_columns(columns)); } } Event::End(TagEnd::Table) => { @@ -941,6 +945,70 @@ impl<'a> MarkdownParser<'a> { } } + fn parse_table_row( + &self, + source_range: Range, + node: &Rc, + ) -> Option { + let mut columns = Vec::new(); + + match &node.data { + markup5ever_rcdom::NodeData::Element { name, .. } => { + if local_name!("tr") != name.local { + return None; + } + + for node in node.children.borrow().iter() { + if let Some(column) = self.parse_table_column(source_range.clone(), node) { + columns.push(column); + } + } + } + _ => {} + } + + if columns.is_empty() { + None + } else { + Some(ParsedMarkdownTableRow { columns }) + } + } + + fn parse_table_column( + &self, + source_range: Range, + node: &Rc, + ) -> Option { + match &node.data { + markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { + if !matches!(name.local, local_name!("th") | local_name!("td")) { + return None; + } + + let mut children = MarkdownParagraph::new(); + self.consume_paragraph(source_range, node, &mut children); + + Some(ParsedMarkdownTableColumn { + col_span: std::cmp::max( + Self::attr_value(attrs, local_name!("colspan")) + .and_then(|span| span.parse().ok()) + .unwrap_or(1), + 1, + ), + row_span: std::cmp::max( + Self::attr_value(attrs, local_name!("rowspan")) + .and_then(|span| span.parse().ok()) + .unwrap_or(1), + 1, + ), + is_header: matches!(name.local, local_name!("th")), + children, + }) + } + _ => None, + } + } + fn consume_children( &self, source_range: Range, @@ -1056,7 +1124,7 @@ impl<'a> MarkdownParser<'a> { node: &Rc, source_range: Range, ) -> Option { - let mut header_columns = Vec::new(); + let mut header_rows = Vec::new(); let mut body_rows = Vec::new(); // node should be a thead or tbody element @@ -1066,21 +1134,16 @@ impl<'a> MarkdownParser<'a> { if local_name!("thead") == name.local { // node should be a tr element for node in node.children.borrow().iter() { - let mut paragraph = MarkdownParagraph::new(); - self.consume_paragraph(source_range.clone(), node, &mut paragraph); - - for paragraph in paragraph.into_iter() { - header_columns.push(vec![paragraph]); + if let Some(row) = self.parse_table_row(source_range.clone(), node) { + header_rows.push(row); } } } else if local_name!("tbody") == name.local { // node should be a tr element for node in node.children.borrow().iter() { - let mut row = MarkdownParagraph::new(); - self.consume_paragraph(source_range.clone(), node, &mut row); - body_rows.push(ParsedMarkdownTableRow::with_children( - row.into_iter().map(|column| vec![column]).collect(), - )); + if let Some(row) = self.parse_table_row(source_range.clone(), node) { + body_rows.push(row); + } } } } @@ -1088,12 +1151,12 @@ impl<'a> MarkdownParser<'a> { } } - if !header_columns.is_empty() || !body_rows.is_empty() { + if !header_rows.is_empty() || !body_rows.is_empty() { Some(ParsedMarkdownTable { source_range, body: body_rows, column_alignments: Vec::default(), - header: ParsedMarkdownTableRow::with_children(header_columns), + header: header_rows, }) } else { None @@ -1589,10 +1652,19 @@ mod tests { ParsedMarkdown { children: vec![ParsedMarkdownElement::Table(table( 0..366, - row(vec![text("Id", 0..366), text("Name ", 0..366)]), + vec![row(vec![ + column(1, 1, true, text("Id", 0..366)), + column(1, 1, true, text("Name ", 0..366)) + ])], vec![ - row(vec![text("1", 0..366), text("Chris", 0..366)]), - row(vec![text("2", 0..366), text("Dennis", 0..366)]), + row(vec![ + column(1, 1, false, text("1", 0..366)), + column(1, 1, false, text("Chris", 0..366)) + ]), + row(vec![ + column(1, 1, false, text("2", 0..366)), + column(1, 1, false, text("Dennis", 0..366)) + ]), ], ))], }, @@ -1622,10 +1694,16 @@ mod tests { ParsedMarkdown { children: vec![ParsedMarkdownElement::Table(table( 0..240, - row(vec![]), + vec![], vec![ - row(vec![text("1", 0..240), text("Chris", 0..240)]), - row(vec![text("2", 0..240), text("Dennis", 0..240)]), + row(vec![ + column(1, 1, false, text("1", 0..240)), + column(1, 1, false, text("Chris", 0..240)) + ]), + row(vec![ + column(1, 1, false, text("2", 0..240)), + column(1, 1, false, text("Dennis", 0..240)) + ]), ], ))], }, @@ -1651,7 +1729,10 @@ mod tests { ParsedMarkdown { children: vec![ParsedMarkdownElement::Table(table( 0..150, - row(vec![text("Id", 0..150), text("Name", 0..150)]), + vec![row(vec![ + column(1, 1, true, text("Id", 0..150)), + column(1, 1, true, text("Name", 0..150)) + ])], vec![], ))], }, @@ -1833,7 +1914,10 @@ Some other content let expected_table = table( 0..48, - row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]), + vec![row(vec![ + column(1, 1, true, text("Header 1", 1..11)), + column(1, 1, true, text("Header 2", 12..22)), + ])], vec![], ); @@ -1853,10 +1937,19 @@ Some other content let expected_table = table( 0..95, - row(vec![text("Header 1", 1..11), text("Header 2", 12..22)]), + vec![row(vec![ + column(1, 1, true, text("Header 1", 1..11)), + column(1, 1, true, text("Header 2", 12..22)), + ])], vec![ - row(vec![text("Cell 1", 49..59), text("Cell 2", 60..70)]), - row(vec![text("Cell 3", 73..83), text("Cell 4", 84..94)]), + row(vec![ + column(1, 1, false, text("Cell 1", 49..59)), + column(1, 1, false, text("Cell 2", 60..70)), + ]), + row(vec![ + column(1, 1, false, text("Cell 3", 73..83)), + column(1, 1, false, text("Cell 4", 84..94)), + ]), ], ); @@ -2313,7 +2406,7 @@ fn main() { fn table( source_range: Range, - header: ParsedMarkdownTableRow, + header: Vec, body: Vec, ) -> ParsedMarkdownTable { ParsedMarkdownTable { @@ -2324,8 +2417,22 @@ fn main() { } } - fn row(children: Vec) -> ParsedMarkdownTableRow { - ParsedMarkdownTableRow { children } + fn row(columns: Vec) -> ParsedMarkdownTableRow { + ParsedMarkdownTableRow { columns } + } + + fn column( + col_span: usize, + row_span: usize, + is_header: bool, + children: MarkdownParagraph, + ) -> ParsedMarkdownTableColumn { + ParsedMarkdownTableColumn { + col_span, + row_span, + is_header, + children, + } } impl PartialEq for ParsedMarkdownTable { diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs index d3768ca99449e820f6c7b457ce93eb886511340f..0abb12015af317702ff3afd853eab74a40817941 100644 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ b/crates/markdown_preview/src/markdown_renderer.rs @@ -8,8 +8,8 @@ use fs::normalize_path; use gpui::{ AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div, Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, - Keystroke, Length, Modifiers, ParentElement, Render, Resource, SharedString, Styled, - StyledText, TextStyle, WeakEntity, Window, div, img, rems, + Keystroke, Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, + TextStyle, WeakEntity, Window, div, img, rems, }; use settings::Settings; use std::{ @@ -19,8 +19,10 @@ use std::{ }; use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; use ui::{ - Clickable, FluentBuilder, LinkPreview, StatefulInteractiveElement, StyledExt, StyledImage, - ToggleState, Tooltip, VisibleOnHover, prelude::*, tooltip_container, + ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize, + InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems, + StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover, + h_flex, tooltip_container, v_flex, }; use workspace::{OpenOptions, OpenVisible, Workspace}; @@ -467,132 +469,100 @@ impl gpui::RenderOnce for MarkdownCheckbox { } } -fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize { - paragraphs - .iter() - .map(|paragraph| match paragraph { - MarkdownParagraphChunk::Text(text) => text.contents.len(), - // TODO: Scale column width based on image size - MarkdownParagraphChunk::Image(_) => 1, - }) - .sum() +fn calculate_table_columns_count(rows: &Vec) -> usize { + let mut actual_column_count = 0; + for row in rows { + actual_column_count = actual_column_count.max( + row.columns + .iter() + .map(|column| column.col_span) + .sum::(), + ); + } + actual_column_count } fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { - let mut max_lengths: Vec = vec![0; parsed.header.children.len()]; + let actual_header_column_count = calculate_table_columns_count(&parsed.header); + let actual_body_column_count = calculate_table_columns_count(&parsed.body); + let max_column_count = std::cmp::max(actual_header_column_count, actual_body_column_count); - for (index, cell) in parsed.header.children.iter().enumerate() { - let length = paragraph_len(cell); - max_lengths[index] = length; - } + let total_rows = parsed.header.len() + parsed.body.len(); - for row in &parsed.body { - for (index, cell) in row.children.iter().enumerate() { - let length = paragraph_len(cell); + // Track which grid cells are occupied by spanning cells + let mut grid_occupied = vec![vec![false; max_column_count]; total_rows]; - if index >= max_lengths.len() { - max_lengths.resize(index + 1, length); - } - - if length > max_lengths[index] { - max_lengths[index] = length; - } - } - } + let mut cells = Vec::with_capacity(total_rows * max_column_count); - let total_max_length: usize = max_lengths.iter().sum(); - let max_column_widths: Vec = max_lengths - .iter() - .map(|&length| length as f32 / total_max_length as f32) - .collect(); + for (row_idx, row) in parsed.header.iter().chain(parsed.body.iter()).enumerate() { + let mut col_idx = 0; - let header = render_markdown_table_row( - &parsed.header, - &parsed.column_alignments, - &max_column_widths, - true, - 0, - cx, - ); - - let body: Vec = parsed - .body - .iter() - .enumerate() - .map(|(index, row)| { - render_markdown_table_row( - row, - &parsed.column_alignments, - &max_column_widths, - false, - index, - cx, - ) - }) - .collect(); + for (cell_idx, cell) in row.columns.iter().enumerate() { + // Skip columns occupied by row-spanning cells from previous rows + while col_idx < max_column_count && grid_occupied[row_idx][col_idx] { + col_idx += 1; + } - div().child(header).children(body).into_any() -} + if col_idx >= max_column_count { + break; + } -fn render_markdown_table_row( - parsed: &ParsedMarkdownTableRow, - alignments: &Vec, - max_column_widths: &Vec, - is_header: bool, - row_index: usize, - cx: &mut RenderContext, -) -> AnyElement { - let mut items = Vec::with_capacity(parsed.children.len()); - let count = parsed.children.len(); + let alignment = parsed + .column_alignments + .get(cell_idx) + .copied() + .unwrap_or_else(|| { + if cell.is_header { + ParsedMarkdownTableAlignment::Center + } else { + ParsedMarkdownTableAlignment::None + } + }); - for (index, cell) in parsed.children.iter().enumerate() { - let alignment = alignments - .get(index) - .copied() - .unwrap_or(ParsedMarkdownTableAlignment::None); + let container = match alignment { + ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(), + ParsedMarkdownTableAlignment::Center => v_flex().items_center(), + ParsedMarkdownTableAlignment::Right => v_flex().items_end(), + }; - let contents = render_markdown_text(cell, cx); + let cell_element = container + .col_span(cell.col_span.min(max_column_count - col_idx) as u16) + .row_span(cell.row_span.min(total_rows - row_idx) as u16) + .children(render_markdown_text(&cell.children, cx)) + .px_2() + .py_1() + .border_1() + .size_full() + .border_color(cx.border_color) + .when(cell.is_header, |this| { + this.bg(cx.title_bar_background_color) + }) + .when(cell.row_span > 1, |this| this.justify_center()) + .when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color)); - let container = match alignment { - ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(), - ParsedMarkdownTableAlignment::Center => v_flex().items_center(), - ParsedMarkdownTableAlignment::Right => v_flex().items_end(), - }; + cells.push(cell_element); - let max_width = max_column_widths.get(index).unwrap_or(&0.0); - let mut cell = container - .w(Length::Definite(relative(*max_width))) - .h_full() - .children(contents) - .px_2() - .py_1() - .border_color(cx.border_color) - .border_l_1(); - - if count == index + 1 { - cell = cell.border_r_1(); - } + // Mark grid positions as occupied for row-spanning cells + for r in 0..cell.row_span { + for c in 0..cell.col_span { + if row_idx + r < total_rows && col_idx + c < max_column_count { + grid_occupied[row_idx + r][col_idx + c] = true; + } + } + } - if is_header { - cell = cell.bg(cx.title_bar_background_color).opacity(0.6) + col_idx += cell.col_span; } - - items.push(cell); - } - - let mut row = h_flex().border_color(cx.border_color); - - if is_header { - row = row.border_y_1(); - } else { - row = row.border_b_1(); - } - - if row_index % 2 == 1 { - row = row.bg(cx.panel_background_color) } - row.children(items).into_any_element() + cx.with_common_p(div()) + .grid() + .size_full() + .grid_cols(max_column_count as u16) + .border_1() + .border_color(cx.border_color) + .children(cells) + .into_any() } fn render_markdown_block_quote( @@ -903,3 +873,143 @@ impl Render for InteractiveMarkdownElementTooltip { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::markdown_elements::ParsedMarkdownTableColumn; + use crate::markdown_elements::ParsedMarkdownText; + + fn text(text: &str) -> MarkdownParagraphChunk { + MarkdownParagraphChunk::Text(ParsedMarkdownText { + source_range: 0..text.len(), + contents: SharedString::new(text), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }) + } + + fn column( + col_span: usize, + row_span: usize, + children: Vec, + ) -> ParsedMarkdownTableColumn { + ParsedMarkdownTableColumn { + col_span, + row_span, + is_header: false, + children, + } + } + + fn column_with_row_span( + col_span: usize, + row_span: usize, + children: Vec, + ) -> ParsedMarkdownTableColumn { + ParsedMarkdownTableColumn { + col_span, + row_span, + is_header: false, + children, + } + } + + #[test] + fn test_calculate_table_columns_count() { + assert_eq!(0, calculate_table_columns_count(&vec![])); + + assert_eq!( + 1, + calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ + column(1, 1, vec![text("column1")]) + ])]) + ); + + assert_eq!( + 2, + calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ + column(1, 1, vec![text("column1")]), + column(1, 1, vec![text("column2")]), + ])]) + ); + + assert_eq!( + 2, + calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ + column(2, 1, vec![text("column1")]) + ])]) + ); + + assert_eq!( + 3, + calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ + column(1, 1, vec![text("column1")]), + column(2, 1, vec![text("column2")]), + ])]) + ); + + assert_eq!( + 2, + calculate_table_columns_count(&vec![ + ParsedMarkdownTableRow::with_columns(vec![ + column(1, 1, vec![text("column1")]), + column(1, 1, vec![text("column2")]), + ]), + ParsedMarkdownTableRow::with_columns(vec![column(1, 1, vec![text("column1")]),]) + ]) + ); + + assert_eq!( + 3, + calculate_table_columns_count(&vec![ + ParsedMarkdownTableRow::with_columns(vec![ + column(1, 1, vec![text("column1")]), + column(1, 1, vec![text("column2")]), + ]), + ParsedMarkdownTableRow::with_columns(vec![column(3, 3, vec![text("column1")]),]) + ]) + ); + } + + #[test] + fn test_row_span_support() { + assert_eq!( + 3, + calculate_table_columns_count(&vec![ + ParsedMarkdownTableRow::with_columns(vec![ + column_with_row_span(1, 2, vec![text("spans 2 rows")]), + column(1, 1, vec![text("column2")]), + column(1, 1, vec![text("column3")]), + ]), + ParsedMarkdownTableRow::with_columns(vec![ + // First column is covered by row span from above + column(1, 1, vec![text("column2 row2")]), + column(1, 1, vec![text("column3 row2")]), + ]) + ]) + ); + + assert_eq!( + 4, + calculate_table_columns_count(&vec![ + ParsedMarkdownTableRow::with_columns(vec![ + column_with_row_span(1, 3, vec![text("spans 3 rows")]), + column_with_row_span(2, 1, vec![text("spans 2 cols")]), + column(1, 1, vec![text("column4")]), + ]), + ParsedMarkdownTableRow::with_columns(vec![ + // First column covered by row span + column(1, 1, vec![text("column2")]), + column(1, 1, vec![text("column3")]), + column(1, 1, vec![text("column4")]), + ]), + ParsedMarkdownTableRow::with_columns(vec![ + // First column still covered by row span + column(3, 1, vec![text("spans 3 cols")]), + ]) + ]) + ); + } +} From 4a93719b6b5666fd04cbe63fd90aba28c3547f0d Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 22 Oct 2025 13:13:28 -0400 Subject: [PATCH 158/202] Upgrade `async-tar` to v0.5.1 (#40911) This PR switches us back to the upstream version of `async-tar` and upgrades to v0.5.1. This version has the patch we need: https://github.com/dignifiedquire/async-tar/commit/0c181956395fa28a9ae45786a57c9cf58c413e84. Release Notes: - N/A --- Cargo.lock | 43 ++++++++++++++++++++++--------------------- Cargo.toml | 3 +-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cbd977bd6ed4089d281697145105fbc48ac03f2e..4628b20e29cee879c9a68d5af52a89c3d684302b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1182,6 +1182,20 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "async-tar" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1937db2d56578aa3919b9bdb0e5100693fd7d1c0f145c53eb81fbb03e217550" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall 0.2.16", + "xattr", +] + [[package]] name = "async-task" version = "4.7.1" @@ -1279,6 +1293,7 @@ name = "audio" version = "0.1.0" dependencies = [ "anyhow", + "async-tar", "collections", "crossbeam", "denoise", @@ -1292,7 +1307,6 @@ dependencies = [ "smol", "thiserror 2.0.17", "util", - "zed-async-tar", ] [[package]] @@ -4459,6 +4473,7 @@ dependencies = [ "anyhow", "async-compression", "async-pipe", + "async-tar", "async-trait", "client", "collections", @@ -4485,7 +4500,6 @@ dependencies = [ "tree-sitter", "tree-sitter-go", "util", - "zed-async-tar", "zlog", ] @@ -5751,6 +5765,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", + "async-tar", "async-trait", "collections", "dap", @@ -5773,7 +5788,6 @@ dependencies = [ "util", "wasm-encoder 0.221.3", "wasmparser 0.221.3", - "zed-async-tar", ] [[package]] @@ -5805,6 +5819,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-compression", + "async-tar", "async-trait", "client", "collections", @@ -5845,7 +5860,6 @@ dependencies = [ "wasmparser 0.221.3", "wasmtime", "wasmtime-wasi", - "zed-async-tar", "zlog", ] @@ -6306,6 +6320,7 @@ version = "0.1.0" dependencies = [ "anyhow", "ashpd 0.11.0", + "async-tar", "async-trait", "cocoa 0.26.0", "collections", @@ -6330,7 +6345,6 @@ dependencies = [ "time", "util", "windows 0.61.3", - "zed-async-tar", ] [[package]] @@ -7669,6 +7683,7 @@ dependencies = [ "anyhow", "async-compression", "async-fs", + "async-tar", "bytes 1.10.1", "derive_more 0.99.20", "futures 0.3.31", @@ -7682,7 +7697,6 @@ dependencies = [ "tempfile", "url", "util", - "zed-async-tar", "zed-reqwest", ] @@ -8878,6 +8892,7 @@ dependencies = [ "anyhow", "async-compression", "async-fs", + "async-tar", "async-trait", "chrono", "collections", @@ -8934,7 +8949,6 @@ dependencies = [ "url", "util", "workspace", - "zed-async-tar", ] [[package]] @@ -10178,6 +10192,7 @@ dependencies = [ "anyhow", "async-compression", "async-std", + "async-tar", "async-trait", "futures 0.3.31", "http_client", @@ -10190,7 +10205,6 @@ dependencies = [ "util", "watch", "which 6.0.3", - "zed-async-tar", ] [[package]] @@ -21070,19 +21084,6 @@ dependencies = [ "zlog_settings", ] -[[package]] -name = "zed-async-tar" -version = "0.5.0-zed" -source = "git+https://github.com/zed-industries/async-tar?rev=a307f6bf3e4219c3a457bea0cab198b6d7c36e25#a307f6bf3e4219c3a457bea0cab198b6d7c36e25" -dependencies = [ - "async-std", - "filetime", - "libc", - "pin-project", - "redox_syscall 0.2.16", - "xattr", -] - [[package]] name = "zed-font-kit" version = "0.14.1-zed" diff --git a/Cargo.toml b/Cargo.toml index bd0083d122018c6769ca161c31e0a9b3ef4ec898..792f38e4ce0aa2ad947f60b2962f7711eff846f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -452,8 +452,7 @@ async-fs = "2.1" async-lock = "2.1" async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" -# WARNING: If you change this, you must also publish a new version of zed-async-tar to crates.io -async-tar = { git = "https://github.com/zed-industries/async-tar", rev = "a307f6bf3e4219c3a457bea0cab198b6d7c36e25", package = "zed-async-tar", version = "0.5.0-zed" } +async-tar = "0.5.1" async-task = "4.7" async-trait = "0.1" async-tungstenite = "0.31.0" From c60343af719a04323b4b6bdbf27d1d549247309d Mon Sep 17 00:00:00 2001 From: Bennet Fenner Date: Wed, 22 Oct 2025 19:55:26 +0200 Subject: [PATCH 159/202] eval: Port to agent2 (#40704) Release Notes: - N/A --- Cargo.lock | 55 ++ Cargo.toml | 2 +- crates/acp_thread/src/terminal.rs | 74 ++- crates/agent/Cargo.toml | 2 + crates/agent/src/edit_agent/evals.rs | 29 +- crates/agent/src/tests/mod.rs | 2 +- crates/agent/src/thread.rs | 41 +- crates/agent_servers/src/acp.rs | 74 +-- crates/eval/Cargo.toml | 5 +- crates/eval/runner_settings.json | 5 +- crates/eval/src/eval.rs | 39 +- crates/eval/src/example.rs | 302 +++++------ .../src/examples/add_arg_to_trait_method.rs | 6 +- .../eval/src/examples/code_block_citations.rs | 19 +- .../eval/src/examples/comment_translation.rs | 32 +- .../src/examples/file_change_notification.rs | 6 +- crates/eval/src/examples/file_search.rs | 11 +- .../src/examples/grep_params_escapement.rs | 7 +- crates/eval/src/examples/mod.rs | 3 +- crates/eval/src/examples/overwrite_file.rs | 18 +- crates/eval/src/examples/planets.rs | 15 +- crates/eval/src/instance.rs | 492 ++++++++++++------ 22 files changed, 783 insertions(+), 456 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4628b20e29cee879c9a68d5af52a89c3d684302b..e426bc4ce64d540ea77fcd03decb875ebb76a572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5692,6 +5692,61 @@ dependencies = [ "num-traits", ] +[[package]] +name = "eval" +version = "0.1.0" +dependencies = [ + "acp_thread", + "agent", + "agent-client-protocol", + "agent_settings", + "agent_ui", + "anyhow", + "async-trait", + "buffer_diff", + "chrono", + "clap", + "client", + "collections", + "debug_adapter_extension", + "dirs 4.0.0", + "dotenvy", + "env_logger 0.11.8", + "extension", + "fs", + "futures 0.3.31", + "gpui", + "gpui_tokio", + "handlebars 4.5.0", + "language", + "language_extension", + "language_model", + "language_models", + "languages", + "markdown", + "node_runtime", + "pathdiff", + "paths", + "pretty_assertions", + "project", + "prompt_store", + "rand 0.9.2", + "regex", + "release_channel", + "reqwest_client", + "serde", + "serde_json", + "settings", + "shellexpand 2.1.2", + "telemetry", + "terminal_view", + "toml 0.8.23", + "unindent", + "util", + "uuid", + "watch", +] + [[package]] name = "event-listener" version = "2.5.3" diff --git a/Cargo.toml b/Cargo.toml index 792f38e4ce0aa2ad947f60b2962f7711eff846f4..c0c0ffc1508aaa51465db7a30cccfcfa04fd8467 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ members = [ "crates/edit_prediction_context", "crates/zeta2_tools", "crates/editor", - # "crates/eval", + "crates/eval", "crates/explorer_command_injector", "crates/extension", "crates/extension_api", diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs index 888c7698c3d2270769f3afbe712ecba7d08b055f..9ca6d4021b316231930ab7803957dab3a0139f1e 100644 --- a/crates/acp_thread/src/terminal.rs +++ b/crates/acp_thread/src/terminal.rs @@ -1,10 +1,15 @@ use agent_client_protocol as acp; - +use anyhow::Result; use futures::{FutureExt as _, future::Shared}; -use gpui::{App, AppContext, Context, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, Task}; use language::LanguageRegistry; use markdown::Markdown; +use project::Project; +use settings::{Settings as _, SettingsLocation}; use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant}; +use task::Shell; +use terminal::terminal_settings::TerminalSettings; +use util::get_default_system_shell_preferring_bash; pub struct Terminal { id: acp::TerminalId, @@ -170,3 +175,68 @@ impl Terminal { ) } } + +pub async fn create_terminal_entity( + command: String, + args: &[String], + env_vars: Vec<(String, String)>, + cwd: Option, + project: &Entity, + cx: &mut AsyncApp, +) -> Result> { + let mut env = if let Some(dir) = &cwd { + project + .update(cx, |project, cx| { + let worktree = project.find_worktree(dir.as_path(), cx); + let shell = TerminalSettings::get( + worktree.as_ref().map(|(worktree, path)| SettingsLocation { + worktree_id: worktree.read(cx).id(), + path: &path, + }), + cx, + ) + .shell + .clone(); + project.directory_environment(&shell, dir.clone().into(), cx) + })? + .await + .unwrap_or_default() + } else { + Default::default() + }; + + // Disables paging for `git` and hopefully other commands + env.insert("PAGER".into(), "".into()); + env.extend(env_vars); + + // Use remote shell or default system shell, as appropriate + let shell = project + .update(cx, |project, cx| { + project + .remote_client() + .and_then(|r| r.read(cx).default_system_shell()) + .map(Shell::Program) + })? + .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash())); + let is_windows = project + .read_with(cx, |project, cx| project.path_style(cx).is_windows()) + .unwrap_or(cfg!(windows)); + let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows) + .redirect_stdin_to_dev_null() + .build(Some(command.clone()), &args); + + project + .update(cx, |project, cx| { + project.create_terminal_task( + task::SpawnInTerminal { + command: Some(task_command), + args: task_args, + cwd, + env, + ..Default::default() + }, + cx, + ) + })? + .await +} diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 86027d01fe3e93d2f6234cce9e935ebace318481..9e5b6ad66096b784bfb496b71ef1ee5cb30005cb 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -10,6 +10,8 @@ path = "src/agent.rs" [features] test-support = ["db/test-support"] +eval = [] +edit-agent-eval = [] e2e = [] [lints] diff --git a/crates/agent/src/edit_agent/evals.rs b/crates/agent/src/edit_agent/evals.rs index b3043f0a81256568338f5d4be22bfe02de277076..a39ed21c7cde1304e4e955e20f6011672ee70c3e 100644 --- a/crates/agent/src/edit_agent/evals.rs +++ b/crates/agent/src/edit_agent/evals.rs @@ -31,7 +31,7 @@ use std::{ use util::path; #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_extract_handle_command_output() { // Test how well agent generates multiple edit hunks. // @@ -108,7 +108,7 @@ fn eval_extract_handle_command_output() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_delete_run_git_blame() { // Model | Pass rate // ----------------------------|---------- @@ -171,7 +171,7 @@ fn eval_delete_run_git_blame() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_translate_doc_comments() { // Model | Pass rate // ============================================ @@ -234,7 +234,7 @@ fn eval_translate_doc_comments() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { // Model | Pass rate // ============================================ @@ -360,7 +360,7 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_disable_cursor_blinking() { // Model | Pass rate // ============================================ @@ -446,7 +446,7 @@ fn eval_disable_cursor_blinking() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_from_pixels_constructor() { // Results for 2025-06-13 // @@ -656,7 +656,7 @@ fn eval_from_pixels_constructor() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_zode() { // Model | Pass rate // ============================================ @@ -763,7 +763,7 @@ fn eval_zode() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_add_overwrite_test() { // Model | Pass rate // ============================================ @@ -995,7 +995,7 @@ fn eval_add_overwrite_test() { } #[test] -#[cfg_attr(not(feature = "eval"), ignore)] +#[cfg_attr(not(feature = "edit-agent-eval"), ignore)] fn eval_create_empty_file() { // Check that Edit Agent can create a file without writing its // thoughts into it. This issue is not specific to empty files, but @@ -1490,9 +1490,20 @@ impl EditAgentTest { &std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-4-sonnet-latest".into()), ) .unwrap(); + + let authenticate_provider_tasks = cx.update(|cx| { + LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry + .providers() + .iter() + .map(|p| p.authenticate(cx)) + .collect::>() + }) + }); let (agent_model, judge_model) = cx .update(|cx| { cx.spawn(async move |cx| { + futures::future::join_all(authenticate_provider_tasks).await; let agent_model = Self::load_model(&agent_model, cx).await; let judge_model = Self::load_model(&judge_model, cx).await; (agent_model.unwrap(), judge_model.unwrap()) diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 6b7d30b37f825bf664ee270bee9f965ee194291c..66b006893e50b9c59701eff850adb7747f96e3b5 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -1995,7 +1995,7 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { locations: vec![], raw_input: Some(json!({})), raw_output: None, - meta: None, + meta: Some(json!({ "tool_name": "thinking" })), } ); let update = expect_tool_call_update_fields(&mut events).await; diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index c89ad1df241c3b9c6e07b9a5433dd964244ba2cb..d873e4f26cb22d34c501b1d4d3ffd3af94465af4 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -745,7 +745,13 @@ impl Thread { let title = tool.initial_title(tool_use.input.clone(), cx); let kind = tool.kind(); - stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); + stream.send_tool_call( + &tool_use.id, + &tool_use.name, + title, + kind, + tool_use.input.clone(), + ); let output = tool_result .as_ref() @@ -1044,14 +1050,18 @@ impl Thread { Ok(()) } - pub fn latest_token_usage(&self) -> Option { + pub fn latest_request_token_usage(&self) -> Option { let last_user_message = self.last_user_message()?; let tokens = self.request_token_usage.get(&last_user_message.id)?; - let model = self.model.clone()?; + Some(*tokens) + } + pub fn latest_token_usage(&self) -> Option { + let usage = self.latest_request_token_usage()?; + let model = self.model.clone()?; Some(acp_thread::TokenUsage { max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), - used_tokens: tokens.total_tokens(), + used_tokens: usage.total_tokens(), }) } @@ -1094,6 +1104,14 @@ impl Thread { self.run_turn(cx) } + #[cfg(feature = "eval")] + pub fn proceed( + &mut self, + cx: &mut Context, + ) -> Result>> { + self.run_turn(cx) + } + fn run_turn( &mut self, cx: &mut Context, @@ -1461,7 +1479,13 @@ impl Thread { }); if push_new_tool_use { - event_stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone()); + event_stream.send_tool_call( + &tool_use.id, + &tool_use.name, + title, + kind, + tool_use.input.clone(), + ); last_message .content .push(AgentMessageContent::ToolUse(tool_use.clone())); @@ -2256,6 +2280,7 @@ impl ThreadEventStream { fn send_tool_call( &self, id: &LanguageModelToolUseId, + tool_name: &str, title: SharedString, kind: acp::ToolKind, input: serde_json::Value, @@ -2263,6 +2288,7 @@ impl ThreadEventStream { self.0 .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( id, + tool_name, title.to_string(), kind, input, @@ -2272,12 +2298,15 @@ impl ThreadEventStream { fn initial_tool_call( id: &LanguageModelToolUseId, + tool_name: &str, title: String, kind: acp::ToolKind, input: serde_json::Value, ) -> acp::ToolCall { acp::ToolCall { - meta: None, + meta: Some(serde_json::json!({ + "tool_name": tool_name + })), id: acp::ToolCallId(id.to_string().into()), title, kind, diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index ad205137a44f3fd7e33e4998c023d552e4007b5c..6f92b958b2d94e48539e34b6a58b4789ea376fb5 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -9,9 +9,7 @@ use futures::io::BufReader; use project::Project; use project::agent_server_store::AgentServerCommand; use serde::Deserialize; -use settings::{Settings as _, SettingsLocation}; -use task::Shell; -use util::{ResultExt as _, get_default_system_shell_preferring_bash}; +use util::ResultExt as _; use std::path::PathBuf; use std::{any::Any, cell::RefCell}; @@ -23,7 +21,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent}; use terminal::TerminalBuilder; -use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings}; +use terminal::terminal_settings::{AlternateScroll, CursorShape}; #[derive(Debug, Error)] #[error("Unsupported version")] @@ -816,62 +814,18 @@ impl acp::Client for ClientDelegate { let thread = self.session_thread(&args.session_id)?; let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?; - let mut env = if let Some(dir) = &args.cwd { - project - .update(&mut self.cx.clone(), |project, cx| { - let worktree = project.find_worktree(dir.as_path(), cx); - let shell = TerminalSettings::get( - worktree.as_ref().map(|(worktree, path)| SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: &path, - }), - cx, - ) - .shell - .clone(); - project.directory_environment(&shell, dir.clone().into(), cx) - })? - .await - .unwrap_or_default() - } else { - Default::default() - }; - // Disables paging for `git` and hopefully other commands - env.insert("PAGER".into(), "".into()); - for var in args.env { - env.insert(var.name, var.value); - } - - // Use remote shell or default system shell, as appropriate - let shell = project - .update(&mut self.cx.clone(), |project, cx| { - project - .remote_client() - .and_then(|r| r.read(cx).default_system_shell()) - .map(Shell::Program) - })? - .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash())); - let is_windows = project - .read_with(&self.cx, |project, cx| project.path_style(cx).is_windows()) - .unwrap_or(cfg!(windows)); - let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows) - .redirect_stdin_to_dev_null() - .build(Some(args.command.clone()), &args.args); - - let terminal_entity = project - .update(&mut self.cx.clone(), |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(task_command), - args: task_args, - cwd: args.cwd.clone(), - env, - ..Default::default() - }, - cx, - ) - })? - .await?; + let terminal_entity = acp_thread::create_terminal_entity( + args.command.clone(), + &args.args, + args.env + .into_iter() + .map(|env| (env.name, env.value)) + .collect(), + args.cwd.clone(), + &project, + &mut self.cx.clone(), + ) + .await?; // Register with renderer let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| { diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index 6e1cc7a6f554ac4a8d6c84ecf58f4fcbf8ac1d96..30908be1e2fde15c0c32894b266d971b7f0ca54f 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -19,7 +19,7 @@ path = "src/explorer.rs" [dependencies] acp_thread.workspace = true -agent.workspace = true +agent = { workspace = true, features = ["eval"] } agent-client-protocol.workspace = true agent_settings.workspace = true agent_ui.workspace = true @@ -29,7 +29,6 @@ buffer_diff.workspace = true chrono.workspace = true clap.workspace = true client.workspace = true -cloud_llm_client.workspace = true collections.workspace = true debug_adapter_extension.workspace = true dirs.workspace = true @@ -54,13 +53,13 @@ pretty_assertions.workspace = true project.workspace = true prompt_store.workspace = true regex.workspace = true +rand.workspace = true release_channel.workspace = true reqwest_client.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true shellexpand.workspace = true -smol.workspace = true telemetry.workspace = true terminal_view.workspace = true toml.workspace = true diff --git a/crates/eval/runner_settings.json b/crates/eval/runner_settings.json index 53d853023c75e78f19c78f797b5751ff79bf1e44..ea2ccb051164c4a6c40aed9d6607db0a8911c5d6 100644 --- a/crates/eval/runner_settings.json +++ b/crates/eval/runner_settings.json @@ -1,6 +1,5 @@ { - "assistant": { - "always_allow_tool_actions": true, - "version": "2" + "agent": { + "always_allow_tool_actions": true } } diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 3afcc32a930ab32746352e81577d55a25c807cb4..c5b34a63eec33a45e6d1c75e73fa473f845c5e36 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -61,9 +61,22 @@ struct Args { /// Maximum number of examples to run concurrently. #[arg(long, default_value = "4")] concurrency: usize, + /// Output current environment variables as JSON to stdout + #[arg(long, hide = true)] + printenv: bool, } fn main() { + let args = Args::parse(); + + // This prevents errors showing up in the logs, because + // project::environment::load_shell_environment() calls + // std::env::current_exe().unwrap() --printenv + if args.printenv { + util::shell_env::print_env(); + return; + } + dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok(); env_logger::init(); @@ -99,7 +112,6 @@ fn main() { let zed_commit_sha = commit_sha_for_path(&root_dir); let zed_branch_name = git_branch_for_path(&root_dir); - let args = Args::parse(); let languages: HashSet = args.languages.into_iter().collect(); let http_client = Arc::new(ReqwestClient::new()); @@ -126,19 +138,20 @@ fn main() { let mut cumulative_tool_metrics = ToolMetrics::default(); - let agent_model = load_model(&args.model, cx).unwrap(); - let judge_model = load_model(&args.judge_model, cx).unwrap(); - - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.set_default_model(Some(agent_model.clone()), cx); + let tasks = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry.providers().iter().map(|p| p.authenticate(cx)).collect::>() }); - let auth1 = agent_model.provider.authenticate(cx); - let auth2 = judge_model.provider.authenticate(cx); - cx.spawn(async move |cx| { - auth1.await?; - auth2.await?; + future::join_all(tasks).await; + let judge_model = cx.update(|cx| { + let agent_model = load_model(&args.model, cx).unwrap(); + let judge_model = load_model(&args.judge_model, cx).unwrap(); + LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry.set_default_model(Some(agent_model.clone()), cx); + }); + judge_model + })?; let mut examples = Vec::new(); @@ -268,7 +281,6 @@ fn main() { future::join_all((0..args.concurrency).map(|_| { let app_state = app_state.clone(); - let model = agent_model.model.clone(); let judge_model = judge_model.model.clone(); let zed_commit_sha = zed_commit_sha.clone(); let zed_branch_name = zed_branch_name.clone(); @@ -283,7 +295,7 @@ fn main() { let result = async { example.setup().await?; let run_output = cx - .update(|cx| example.run(model.clone(), app_state.clone(), cx))? + .update(|cx| example.run(app_state.clone(), cx))? .await?; let judge_output = judge_example( example.clone(), @@ -524,7 +536,6 @@ async fn judge_example( diff_evaluation = judge_output.diff.clone(), thread_evaluation = judge_output.thread, tool_metrics = run_output.tool_metrics, - response_count = run_output.response_count, token_usage = run_output.token_usage, model = model.telemetry_id(), model_provider = model.provider_id().to_string(), diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 22a8f9484c9f2c1d4ad01a107841b57e8b96f67b..84c47766e96948bccfc01f3b4472b5100c4b7b64 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -3,6 +3,7 @@ use std::{ fmt::{self, Debug}, sync::{Arc, Mutex}, time::Duration, + u32, }; use crate::{ @@ -16,11 +17,10 @@ use agent_settings::AgentProfileId; use anyhow::{Result, anyhow}; use async_trait::async_trait; use buffer_diff::DiffHunkStatus; -use cloud_llm_client::CompletionIntent; use collections::HashMap; -use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased}; +use futures::{FutureExt as _, StreamExt, select_biased}; use gpui::{App, AppContext, AsyncApp, Entity}; -use language_model::{LanguageModel, Role, StopReason}; +use language_model::Role; use util::rel_path::RelPath; pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2); @@ -93,7 +93,6 @@ pub struct ExampleContext { log_prefix: String, agent_thread: Entity, app: AsyncApp, - model: Arc, pub assertions: AssertionsReport, pub tool_metrics: Arc>, } @@ -103,7 +102,6 @@ impl ExampleContext { meta: ExampleMetadata, log_prefix: String, agent_thread: Entity, - model: Arc, app: AsyncApp, ) -> Self { let assertions = AssertionsReport::new(meta.max_assertions); @@ -113,26 +111,11 @@ impl ExampleContext { log_prefix, agent_thread, assertions, - model, app, tool_metrics: Arc::new(Mutex::new(ToolMetrics::default())), } } - pub fn push_user_message(&mut self, text: impl ToString) { - self.app - .update_entity(&self.agent_thread, |thread, cx| { - thread.insert_user_message( - text.to_string(), - ContextLoadResult::default(), - None, - Vec::new(), - cx, - ); - }) - .unwrap(); - } - pub fn assert(&mut self, expected: bool, message: impl ToString) -> Result<()> { let message = message.to_string(); self.log_assertion( @@ -204,156 +187,174 @@ impl ExampleContext { result } - pub async fn run_to_end(&mut self) -> Result { - self.run_turns(u32::MAX).await + pub async fn prompt(&mut self, prompt: impl Into) -> Result { + self.prompt_with_max_turns(prompt, u32::MAX).await } - pub async fn run_turn(&mut self) -> Result { - self.run_turns(1).await + pub async fn prompt_with_max_turns( + &mut self, + prompt: impl Into, + max_turns: u32, + ) -> Result { + let content = vec![UserMessageContent::Text(prompt.into())]; + self.run_turns(Some(content), max_turns).await } - pub async fn run_turns(&mut self, iterations: u32) -> Result { - let (mut tx, mut rx) = mpsc::channel(1); + pub async fn proceed_with_max_turns(&mut self, max_turns: u32) -> Result { + self.run_turns(None, max_turns).await + } + async fn run_turns( + &mut self, + prompt: Option>, + max_turns: u32, + ) -> Result { let tool_metrics = self.tool_metrics.clone(); let log_prefix = self.log_prefix.clone(); - let _subscription = self.app.subscribe( - &self.agent_thread, - move |thread, event: &ThreadEvent, cx| match event { - ThreadEvent::ShowError(thread_error) => { - tx.try_send(Err(anyhow!(thread_error.clone()))).ok(); - } - ThreadEvent::Stopped(reason) => match reason { - Ok(StopReason::EndTurn) => { - tx.close_channel(); + + let mut remaining_turns = max_turns; + + let mut event_stream = self.agent_thread.update(&mut self.app, |thread, cx| { + if let Some(prompt) = prompt { + let id = UserMessageId::new(); + thread.send(id, prompt, cx) + } else { + thread.proceed(cx) + } + })??; + + let task = self.app.background_spawn(async move { + let mut messages = Vec::new(); + let mut tool_uses_by_id = HashMap::default(); + while let Some(event) = event_stream.next().await { + match event? { + ThreadEvent::UserMessage(user_message) => { + messages.push(Message { + role: Role::User, + text: user_message.to_markdown(), + tool_use: Vec::new(), + }); } - Ok(StopReason::ToolUse) => { - if thread.read(cx).remaining_turns() == 0 { - tx.close_channel(); + ThreadEvent::AgentThinking(text) | ThreadEvent::AgentText(text) => { + if matches!( + messages.last(), + Some(Message { + role: Role::Assistant, + .. + }) + ) { + messages.last_mut().unwrap().text.push_str(&text); + } else { + messages.push(Message { + role: Role::Assistant, + text, + tool_use: Vec::new(), + }); } } - Ok(StopReason::MaxTokens) => { - tx.try_send(Err(anyhow!("Exceeded maximum tokens"))).ok(); - } - Ok(StopReason::Refusal) => { - tx.try_send(Err(anyhow!("Model refused to generate content"))) - .ok(); - } - Err(err) => { - tx.try_send(Err(anyhow!(err.clone()))).ok(); + ThreadEvent::ToolCall(tool_call) => { + let meta = tool_call.meta.expect("Missing meta field in tool_call"); + let tool_name = meta + .get("tool_name") + .expect("Missing tool_name field in meta") + .as_str() + .expect("Unknown tool_name content in meta"); + + tool_uses_by_id.insert( + tool_call.id, + ToolUse { + name: tool_name.to_string(), + value: tool_call.raw_input.unwrap_or_default(), + }, + ); + if matches!( + tool_call.status, + acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed + ) { + panic!("Tool call completed without update"); + } } - }, - ThreadEvent::NewRequest - | ThreadEvent::StreamedAssistantText(_, _) - | ThreadEvent::StreamedAssistantThinking(_, _) - | ThreadEvent::UsePendingTools { .. } - | ThreadEvent::CompletionCanceled => {} - ThreadEvent::ToolUseLimitReached => {} - ThreadEvent::ToolFinished { - tool_use_id, - pending_tool_use, - .. - } => { - thread.update(cx, |thread, _cx| { - if let Some(tool_use) = pending_tool_use { - let mut tool_metrics = tool_metrics.lock().unwrap(); - if let Some(tool_result) = thread.tool_result(tool_use_id) { - let message = if tool_result.is_error { - format!("✖︎ {}", tool_use.name) - } else { + ThreadEvent::ToolCallUpdate(tool_call_update) => { + if let acp_thread::ToolCallUpdate::UpdateFields(update) = tool_call_update { + if let Some(raw_input) = update.fields.raw_input { + if let Some(tool_use) = tool_uses_by_id.get_mut(&update.id) { + tool_use.value = raw_input; + } + } + + if matches!( + update.fields.status, + Some(acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed) + ) { + let succeeded = + update.fields.status == Some(acp::ToolCallStatus::Completed); + + let tool_use = tool_uses_by_id + .remove(&update.id) + .expect("Unrecognized tool call completed"); + + let log_message = if succeeded { format!("✔︎ {}", tool_use.name) + } else { + format!("✖︎ {}", tool_use.name) }; - println!("{log_prefix}{message}"); + println!("{log_prefix}{log_message}"); + tool_metrics - .insert(tool_result.tool_name.clone(), !tool_result.is_error); - } else { - let message = - format!("TOOL FINISHED WITHOUT RESULT: {}", tool_use.name); - println!("{log_prefix}{message}"); - tool_metrics.insert(tool_use.name.clone(), true); + .lock() + .unwrap() + .insert(tool_use.name.clone().into(), succeeded); + + if let Some(message) = messages.last_mut() { + message.tool_use.push(tool_use); + } else { + messages.push(Message { + role: Role::Assistant, + text: "".to_string(), + tool_use: vec![tool_use], + }); + } + + remaining_turns -= 1; + if remaining_turns == 0 { + return Ok(messages); + } } } - }); - } - ThreadEvent::InvalidToolInput { .. } => { - println!("{log_prefix} invalid tool input"); - } - ThreadEvent::MissingToolUse { - tool_use_id: _, - ui_text, - } => { - println!("{log_prefix} {ui_text}"); - } - ThreadEvent::ToolConfirmationNeeded => { - panic!( + } + ThreadEvent::ToolCallAuthorization(_) => panic!( "{}Bug: Tool confirmation should not be required in eval", log_prefix - ); - } - ThreadEvent::StreamedCompletion - | ThreadEvent::MessageAdded(_) - | ThreadEvent::MessageEdited(_) - | ThreadEvent::MessageDeleted(_) - | ThreadEvent::SummaryChanged - | ThreadEvent::SummaryGenerated - | ThreadEvent::ProfileChanged - | ThreadEvent::ReceivedTextChunk - | ThreadEvent::StreamedToolUse { .. } - | ThreadEvent::CheckpointChanged - | ThreadEvent::CancelEditing => { - tx.try_send(Ok(())).ok(); - if std::env::var("ZED_EVAL_DEBUG").is_ok() { - println!("{}Event: {:#?}", log_prefix, event); - } - } - }, - ); - - let model = self.model.clone(); - - let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| { - thread.set_remaining_turns(iterations); - thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx); - thread.messages().len() - })?; - - loop { - select_biased! { - result = rx.next() => { - if let Some(result) = result { - result?; - } else { - break; + ), + ThreadEvent::Retry(status) => { + println!("{log_prefix} Got retry: {status:?}"); } - } - _ = self.app.background_executor().timer(THREAD_EVENT_TIMEOUT).fuse() => { - anyhow::bail!("Agentic loop stalled - waited {THREAD_EVENT_TIMEOUT:?} without any events"); + ThreadEvent::Stop(stop_reason) => match stop_reason { + acp::StopReason::EndTurn => {} + acp::StopReason::MaxTokens => { + return Err(anyhow!("Exceeded maximum tokens")); + } + acp::StopReason::MaxTurnRequests => { + return Err(anyhow!("Exceeded maximum turn requests")); + } + acp::StopReason::Refusal => { + return Err(anyhow!("Refusal")); + } + acp::StopReason::Cancelled => return Err(anyhow!("Cancelled")), + }, } } - } + Ok(messages) + }); - let messages = self.app.read_entity(&self.agent_thread, |thread, cx| { - let mut messages = Vec::new(); - for message in thread.messages().skip(message_count_before) { - messages.push(Message { - _role: message.role, - text: message.to_message_content(), - tool_use: thread - .tool_uses_for_message(message.id, cx) - .into_iter() - .map(|tool_use| ToolUse { - name: tool_use.name.to_string(), - value: tool_use.input, - }) - .collect(), - }); + select_biased! { + result = task.fuse() => { + Ok(Response::new(result?)) } - messages - })?; - - let response = Response::new(messages); - - Ok(response) + _ = self.app.background_executor().timer(THREAD_EVENT_TIMEOUT).fuse() => { + anyhow::bail!("Agentic loop stalled - waited {THREAD_EVENT_TIMEOUT:?} without any events"); + } + } } pub fn edits(&self) -> HashMap, FileEdits> { @@ -488,7 +489,7 @@ impl Response { Self { messages } } - pub fn expect_tool( + pub fn expect_tool_call( &self, tool_name: &'static str, cx: &mut ExampleContext, @@ -505,8 +506,7 @@ impl Response { }) } - #[allow(dead_code)] - pub fn tool_uses(&self) -> impl Iterator { + pub fn tool_calls(&self) -> impl Iterator { self.messages.iter().flat_map(|msg| &msg.tool_use) } @@ -517,7 +517,7 @@ impl Response { #[derive(Debug)] pub struct Message { - _role: Role, + role: Role, text: String, tool_use: Vec, } diff --git a/crates/eval/src/examples/add_arg_to_trait_method.rs b/crates/eval/src/examples/add_arg_to_trait_method.rs index 41fa7c3dc6361c25868e2bbe73b71010b5d07d80..1692932b3304e07ebce261afb75877400e0493f4 100644 --- a/crates/eval/src/examples/add_arg_to_trait_method.rs +++ b/crates/eval/src/examples/add_arg_to_trait_method.rs @@ -27,14 +27,12 @@ impl Example for AddArgToTraitMethod { async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { const FILENAME: &str = "assistant_tool.rs"; - cx.push_user_message(format!( + let _ = cx.prompt(format!( r#" Add a `window: Option` argument to the `Tool::run` trait method in {FILENAME}, and update all the implementations of the trait and call sites accordingly. "# - )); - - let _ = cx.run_to_end().await?; + )).await?; // Adds ignored argument to all but `batch_tool` diff --git a/crates/eval/src/examples/code_block_citations.rs b/crates/eval/src/examples/code_block_citations.rs index 8150d68ac3e54772e35fe52f086fb942d8923ffb..c8ba75e99f019b0b0609743b10573bae712f82cd 100644 --- a/crates/eval/src/examples/code_block_citations.rs +++ b/crates/eval/src/examples/code_block_citations.rs @@ -29,16 +29,19 @@ impl Example for CodeBlockCitations { async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { const FILENAME: &str = "assistant_tool.rs"; - cx.push_user_message(format!( - r#" - Show me the method bodies of all the methods of the `Tool` trait in {FILENAME}. - - Please show each method in a separate code snippet. - "# - )); // Verify that the messages all have the correct formatting. - let texts: Vec = cx.run_to_end().await?.texts().collect(); + let texts: Vec = cx + .prompt(format!( + r#" + Show me the method bodies of all the methods of the `Tool` trait in {FILENAME}. + + Please show each method in a separate code snippet. + "# + )) + .await? + .texts() + .collect(); let closing_fence = format!("\n{FENCE}"); for text in texts.iter() { diff --git a/crates/eval/src/examples/comment_translation.rs b/crates/eval/src/examples/comment_translation.rs index 893166f3f13207e3444cb03bb17b2dea650170e7..421999893a5a39b3d6f61c22d405bf90528758e7 100644 --- a/crates/eval/src/examples/comment_translation.rs +++ b/crates/eval/src/examples/comment_translation.rs @@ -22,30 +22,26 @@ impl Example for CommentTranslation { } async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { - cx.push_user_message(r#" - Edit the following files and translate all their comments to italian, in this exact order: + let response = cx.prompt( + r#" + Edit the following files and translate all their comments to italian, in this exact order: - - font-kit/src/family.rs - - font-kit/src/canvas.rs - - font-kit/src/error.rs - "#); - cx.run_to_end().await?; + - font-kit/src/family.rs + - font-kit/src/canvas.rs + - font-kit/src/error.rs + "# + ).await?; let mut create_or_overwrite_count = 0; - cx.agent_thread().read_with(cx, |thread, cx| { - for message in thread.messages() { - for tool_use in thread.tool_uses_for_message(message.id, cx) { - if tool_use.name == "edit_file" { - let input: EditFileToolInput = serde_json::from_value(tool_use.input)?; - if !matches!(input.mode, EditFileMode::Edit) { - create_or_overwrite_count += 1; - } - } + for tool_call in response.tool_calls() { + if tool_call.name == "edit_file" { + let input = tool_call.parse_input::()?; + if !matches!(input.mode, EditFileMode::Edit) { + create_or_overwrite_count += 1; } } + } - anyhow::Ok(()) - })??; cx.assert_eq(create_or_overwrite_count, 0, "no_creation_or_overwrite")?; Ok(()) diff --git a/crates/eval/src/examples/file_change_notification.rs b/crates/eval/src/examples/file_change_notification.rs index 7879ad6f2ebb782bd4a5620f0fdf562c9aad1360..41ce10cd2240f2e81812a51b2ec581422c102c41 100644 --- a/crates/eval/src/examples/file_change_notification.rs +++ b/crates/eval/src/examples/file_change_notification.rs @@ -48,8 +48,8 @@ impl Example for FileChangeNotificationExample { })?; // Start conversation (specific message is not important) - cx.push_user_message("Find all files in this repo"); - cx.run_turn().await?; + cx.prompt_with_max_turns("Find all files in this repo", 1) + .await?; // Edit the README buffer - the model should get a notification on next turn buffer.update(cx, |buffer, cx| { @@ -58,7 +58,7 @@ impl Example for FileChangeNotificationExample { // Run for some more turns. // The model shouldn't thank us for letting it know about the file change. - cx.run_turns(3).await?; + cx.proceed_with_max_turns(3).await?; Ok(()) } diff --git a/crates/eval/src/examples/file_search.rs b/crates/eval/src/examples/file_search.rs index c893aef14299a6086e8c50072d69b0cbed7e9fde..7de7a07d19184b473fd2cb5ba29b270431b71a4c 100644 --- a/crates/eval/src/examples/file_search.rs +++ b/crates/eval/src/examples/file_search.rs @@ -25,18 +25,19 @@ impl Example for FileSearchExample { async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { const FILENAME: &str = "find_replace_file_tool.rs"; - cx.push_user_message(format!( - r#" + + let prompt = format!( + r#" Look at the `{FILENAME}`. I want to implement a card for it. The card should implement the `Render` trait. The card should show a diff. It should be a beautifully presented diff. The card "box" should look like what we show for markdown codeblocks (look at `MarkdownElement`). I want to see a red background for lines that were deleted and a green background for lines that were added. We should have a div per diff line. "# - )); + ); - let response = cx.run_turn().await?; - let tool_use = response.expect_tool("find_path", cx)?; + let response = cx.prompt_with_max_turns(prompt, 1).await?; + let tool_use = response.expect_tool_call("find_path", cx)?; let input = tool_use.parse_input::()?; let glob = input.glob; diff --git a/crates/eval/src/examples/grep_params_escapement.rs b/crates/eval/src/examples/grep_params_escapement.rs index face6451572725ed402f23aac7bdc2c70a670b67..57086a1b9bd217e04072754539ddea20aa38c7a8 100644 --- a/crates/eval/src/examples/grep_params_escapement.rs +++ b/crates/eval/src/examples/grep_params_escapement.rs @@ -1,3 +1,4 @@ +use agent::GrepToolInput; use agent_settings::AgentProfileId; use anyhow::Result; use async_trait::async_trait; @@ -35,9 +36,9 @@ impl Example for GrepParamsEscapementExample { } async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { - // cx.push_user_message("How does the precedence/specificity work with Keymap contexts? I am seeing that `MessageEditor > Editor` is lower precendence than `Editor` which is surprising to me, but might be how it works"); - cx.push_user_message("Search for files containing the characters `>` or `<`"); - let response = cx.run_turns(2).await?; + let response = cx + .prompt_with_max_turns("Search for files containing the characters `>` or `<`", 2) + .await?; let grep_input = response .find_tool_call("grep") .and_then(|tool_use| tool_use.parse_input::().ok()); diff --git a/crates/eval/src/examples/mod.rs b/crates/eval/src/examples/mod.rs index afe258aa76b1abb5406ce212af4f223c56cb2020..aec1bce07957fb81c17666b3e64b00a1fa47240f 100644 --- a/crates/eval/src/examples/mod.rs +++ b/crates/eval/src/examples/mod.rs @@ -144,9 +144,8 @@ impl Example for DeclarativeExample { } async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { - cx.push_user_message(&self.prompt); let max_turns = self.metadata.max_turns.unwrap_or(1000); - let _ = cx.run_turns(max_turns).await; + let _ = cx.prompt_with_max_turns(&self.prompt, max_turns).await; Ok(()) } diff --git a/crates/eval/src/examples/overwrite_file.rs b/crates/eval/src/examples/overwrite_file.rs index d4b73aaec4d7d9a18be411ba7d453db9ffcb18a1..a4df1e97a3f4d9c66262f8679d93324e53df9d53 100644 --- a/crates/eval/src/examples/overwrite_file.rs +++ b/crates/eval/src/examples/overwrite_file.rs @@ -1,3 +1,4 @@ +use agent::{EditFileMode, EditFileToolInput}; use agent_settings::AgentProfileId; use anyhow::Result; use async_trait::async_trait; @@ -35,17 +36,14 @@ impl Example for FileOverwriteExample { } async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { - let response = cx.run_turns(1).await?; - let file_overwritten = if let Some(tool_use) = response.find_tool_call("edit_file") { - let input = tool_use.parse_input::()?; - match input.mode { - EditFileMode::Edit => false, - EditFileMode::Create | EditFileMode::Overwrite => { - input.path.ends_with("src/language_model_selector.rs") - } + let response = cx.proceed_with_max_turns(1).await?; + let tool_use = response.expect_tool_call("edit_file", cx)?; + let input = tool_use.parse_input::()?; + let file_overwritten = match input.mode { + EditFileMode::Edit => false, + EditFileMode::Create | EditFileMode::Overwrite => { + input.path.ends_with("src/language_model_selector.rs") } - } else { - false }; cx.assert(!file_overwritten, "File should be edited, not overwritten") diff --git a/crates/eval/src/examples/planets.rs b/crates/eval/src/examples/planets.rs index caa15c728400a82b4223fb9ea8522b0815b36b5a..6b6ca0e3fe75633c49f11f24a24835dc58886a01 100644 --- a/crates/eval/src/examples/planets.rs +++ b/crates/eval/src/examples/planets.rs @@ -23,20 +23,19 @@ impl Example for Planets { } async fn conversation(&self, cx: &mut ExampleContext) -> Result<()> { - cx.push_user_message( - r#" + let response = cx + .prompt( + r#" Make a plain JavaScript web page which renders an animated 3D solar system. Let me drag to rotate the camera around. Do not use npm. - "# - .to_string(), - ); - - let response = cx.run_to_end().await?; + "#, + ) + .await?; let mut open_tool_uses = 0; let mut terminal_tool_uses = 0; - for tool_use in response.tool_uses() { + for tool_use in response.tool_calls() { if tool_use.name == OpenTool::name() { open_tool_uses += 1; } else if tool_use.name == TerminalTool::name() { diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index e95264c3c3b726244abe4edb61dee474d3bff51a..5317f100456748616dfec63819bc0373aaceb4c1 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -1,36 +1,38 @@ -use agent::Message; +use agent::ContextServerRegistry; +use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow, bail}; use client::proto::LspWorkProgress; use futures::channel::mpsc; +use futures::future::Shared; use futures::{FutureExt as _, StreamExt as _, future}; use gpui::{App, AppContext as _, AsyncApp, Entity, Task}; use handlebars::Handlebars; use language::{Buffer, DiagnosticSeverity, OffsetRangeExt as _}; use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelToolResultContent, MessageContent, Role, TokenUsage, + LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, + LanguageModelRequestMessage, LanguageModelToolResultContent, MessageContent, Role, TokenUsage, }; -use project::lsp_store::OpenLspBufferHandle; -use project::{DiagnosticSummary, Project, ProjectPath}; +use project::{DiagnosticSummary, Project, ProjectPath, lsp_store::OpenLspBufferHandle}; +use prompt_store::{ProjectContext, WorktreeContext}; +use rand::{distr, prelude::*}; use serde::{Deserialize, Serialize}; -use std::cell::RefCell; -use std::fmt::Write as _; -use std::fs; -use std::fs::File; -use std::io::Write as _; -use std::path::Path; -use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; +use std::{ + fmt::Write as _, + fs::{self, File}, + io::Write as _, + path::{Path, PathBuf}, + rc::Rc, + sync::{Arc, Mutex}, + time::Duration, +}; use unindent::Unindent as _; -use util::ResultExt as _; -use util::command::new_smol_command; -use util::markdown::MarkdownCodeBlock; +use util::{ResultExt as _, command::new_smol_command, markdown::MarkdownCodeBlock}; -use crate::assertions::{AssertionsReport, RanAssertion, RanAssertionResult}; -use crate::example::{Example, ExampleContext, FailedAssertion, JudgeAssertion}; -use crate::{AgentAppState, ToolMetrics}; +use crate::{ + AgentAppState, ToolMetrics, + assertions::{AssertionsReport, RanAssertion, RanAssertionResult}, + example::{Example, ExampleContext, FailedAssertion, JudgeAssertion}, +}; pub const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed.git"; @@ -56,10 +58,9 @@ pub struct RunOutput { pub diagnostic_summary_after: DiagnosticSummary, pub diagnostics_before: Option, pub diagnostics_after: Option, - pub response_count: usize, pub token_usage: TokenUsage, pub tool_metrics: ToolMetrics, - pub all_messages: String, + pub thread_markdown: String, pub programmatic_assertions: AssertionsReport, } @@ -193,12 +194,7 @@ impl ExampleInstance { .join(self.thread.meta().repo_name()) } - pub fn run( - &self, - model: Arc, - app_state: Arc, - cx: &mut App, - ) -> Task> { + pub fn run(&self, app_state: Arc, cx: &mut App) -> Task> { let project = Project::local( app_state.client.clone(), app_state.node_runtime.clone(), @@ -213,15 +209,6 @@ impl ExampleInstance { project.create_worktree(self.worktree_path(), true, cx) }); - let tools = cx.new(|_| ToolWorkingSet::default()); - let prompt_store = None; - let thread_store = ThreadStore::load( - project.clone(), - tools, - prompt_store, - app_state.prompt_builder.clone(), - cx, - ); let meta = self.thread.meta(); let this = self.clone(); @@ -300,74 +287,62 @@ impl ExampleInstance { // history using undo/redo. std::fs::write(&last_diff_file_path, "")?; - let thread_store = thread_store.await?; - + let thread = cx.update(|cx| { + //todo: Do we want to load rules files here? + let worktrees = project.read(cx).visible_worktrees(cx).map(|worktree| { + let root_name = worktree.read(cx).root_name_str().into(); + let abs_path = worktree.read(cx).abs_path(); - let thread = - thread_store.update(cx, |thread_store, cx| { - let thread = if let Some(json) = &meta.existing_thread_json { - let serialized = SerializedThread::from_json(json.as_bytes()).expect("Can't read serialized thread"); - thread_store.create_thread_from_serialized(serialized, cx) - } else { - thread_store.create_thread(cx) - }; - thread.update(cx, |thread, cx| { - thread.set_profile(meta.profile_id.clone(), cx); - }); - thread - })?; - - - thread.update(cx, |thread, _cx| { - let mut request_count = 0; - let previous_diff = Rc::new(RefCell::new("".to_string())); - let example_output_dir = this.run_directory.clone(); - let last_diff_file_path = last_diff_file_path.clone(); - let messages_json_file_path = example_output_dir.join("last.messages.json"); - let this = this.clone(); - thread.set_request_callback(move |request, response_events| { - request_count += 1; - let messages_file_path = example_output_dir.join(format!("{request_count}.messages.md")); - let diff_file_path = example_output_dir.join(format!("{request_count}.diff")); - let last_messages_file_path = example_output_dir.join("last.messages.md"); - let request_markdown = RequestMarkdown::new(request); - let response_events_markdown = response_events_to_markdown(response_events); - let dialog = ThreadDialog::new(request, response_events); - let dialog_json = serde_json::to_string_pretty(&dialog.to_combined_request()).unwrap_or_default(); - - let messages = format!("{}\n\n{}", request_markdown.messages, response_events_markdown); - fs::write(&messages_file_path, messages.clone()).expect("failed to write messages file"); - fs::write(&last_messages_file_path, messages).expect("failed to write last messages file"); - fs::write(&messages_json_file_path, dialog_json).expect("failed to write last.messages.json"); - - let diff_result = smol::block_on(this.repository_diff()); - match diff_result { - Ok(diff) => { - if diff != previous_diff.borrow().clone() { - fs::write(&diff_file_path, &diff).expect("failed to write diff file"); - fs::write(&last_diff_file_path, &diff).expect("failed to write last diff file"); - *previous_diff.borrow_mut() = diff; - } - } - Err(err) => { - let error_message = format!("{err:?}"); - fs::write(&diff_file_path, &error_message).expect("failed to write diff error to file"); - fs::write(&last_diff_file_path, &error_message).expect("failed to write last diff file"); - } + WorktreeContext { + root_name, + abs_path, + rules_file: None, } + }).collect::>(); + let project_context = cx.new(|_cx| ProjectContext::new(worktrees, vec![])); + let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); + + let thread = if let Some(json) = &meta.existing_thread_json { + let session_id = acp::SessionId( + rand::rng() + .sample_iter(&distr::Alphanumeric) + .take(7) + .map(char::from) + .collect::() + .into(), + ); + + let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread"); + cx.new(|cx| agent::Thread::from_db(session_id, db_thread, project.clone(), project_context, context_server_registry, agent::Templates::new(), cx)) + } else { + cx.new(|cx| agent::Thread::new(project.clone(), project_context, context_server_registry, agent::Templates::new(), None, cx)) + }; - if request_count == 1 { - let tools_file_path = example_output_dir.join("tools.md"); - fs::write(tools_file_path, request_markdown.tools).expect("failed to write tools file"); - } + thread.update(cx, |thread, cx| { + thread.add_default_tools(Rc::new(EvalThreadEnvironment { + project: project.clone(), + }), cx); + thread.set_profile(meta.profile_id.clone()); + thread.set_model( + LanguageModelInterceptor::new( + LanguageModelRegistry::read_global(cx).default_model().expect("Missing model").model.clone(), + this.run_directory.clone(), + last_diff_file_path.clone(), + this.run_directory.join("last.messages.json"), + this.worktree_path(), + this.repo_url(), + ), + cx, + ); }); - })?; + + thread + }).unwrap(); let mut example_cx = ExampleContext::new( meta.clone(), this.log_prefix.clone(), thread.clone(), - model.clone(), cx.clone(), ); let result = this.thread.conversation(&mut example_cx).await; @@ -380,7 +355,7 @@ impl ExampleInstance { println!("{}Stopped", this.log_prefix); println!("{}Getting repository diff", this.log_prefix); - let repository_diff = this.repository_diff().await?; + let repository_diff = Self::repository_diff(this.worktree_path(), &this.repo_url()).await?; std::fs::write(last_diff_file_path, &repository_diff)?; @@ -415,34 +390,28 @@ impl ExampleInstance { } thread.update(cx, |thread, _cx| { - let response_count = thread - .messages() - .filter(|message| message.role == language_model::Role::Assistant) - .count(); RunOutput { repository_diff, diagnostic_summary_before, diagnostic_summary_after, diagnostics_before, diagnostics_after, - response_count, - token_usage: thread.cumulative_token_usage(), + token_usage: thread.latest_request_token_usage().unwrap(), tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(), - all_messages: messages_to_markdown(thread.messages()), + thread_markdown: thread.to_markdown(), programmatic_assertions: example_cx.assertions, } }) }) } - async fn repository_diff(&self) -> Result { - let worktree_path = self.worktree_path(); - run_git(&worktree_path, &["add", "."]).await?; + async fn repository_diff(repository_path: PathBuf, repository_url: &str) -> Result { + run_git(&repository_path, &["add", "."]).await?; let mut diff_args = vec!["diff", "--staged"]; - if self.thread.meta().url == ZED_REPO_URL { + if repository_url == ZED_REPO_URL { diff_args.push(":(exclude).rules"); } - run_git(&worktree_path, &diff_args).await + run_git(&repository_path, &diff_args).await } pub async fn judge( @@ -542,7 +511,7 @@ impl ExampleInstance { hbs.register_template_string(judge_thread_prompt_name, judge_thread_prompt) .unwrap(); - let complete_messages = &run_output.all_messages; + let complete_messages = &run_output.thread_markdown; let to_prompt = |assertion: String| { hbs.render( judge_thread_prompt_name, @@ -634,6 +603,273 @@ impl ExampleInstance { } } +struct EvalThreadEnvironment { + project: Entity, +} + +struct EvalTerminalHandle { + terminal: Entity, +} + +impl agent::TerminalHandle for EvalTerminalHandle { + fn id(&self, cx: &AsyncApp) -> Result { + self.terminal.read_with(cx, |term, _cx| term.id().clone()) + } + + fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { + self.terminal + .read_with(cx, |term, _cx| term.wait_for_exit()) + } + + fn current_output(&self, cx: &AsyncApp) -> Result { + self.terminal + .read_with(cx, |term, cx| term.current_output(cx)) + } +} + +impl agent::ThreadEnvironment for EvalThreadEnvironment { + fn create_terminal( + &self, + command: String, + cwd: Option, + output_byte_limit: Option, + cx: &mut AsyncApp, + ) -> Task>> { + let project = self.project.clone(); + cx.spawn(async move |cx| { + let language_registry = + project.read_with(cx, |project, _cx| project.languages().clone())?; + let id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into()); + let terminal = + acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx) + .await?; + let terminal = cx.new(|cx| { + acp_thread::Terminal::new( + id, + "", + cwd, + output_byte_limit.map(|limit| limit as usize), + terminal, + language_registry, + cx, + ) + })?; + Ok(Rc::new(EvalTerminalHandle { terminal }) as Rc) + }) + } +} + +struct LanguageModelInterceptor { + model: Arc, + request_count: Arc>, + previous_diff: Arc>, + example_output_dir: PathBuf, + last_diff_file_path: PathBuf, + messages_json_file_path: PathBuf, + repository_path: PathBuf, + repository_url: String, +} + +impl LanguageModelInterceptor { + fn new( + model: Arc, + example_output_dir: PathBuf, + last_diff_file_path: PathBuf, + messages_json_file_path: PathBuf, + repository_path: PathBuf, + repository_url: String, + ) -> Arc { + Arc::new(Self { + model, + request_count: Arc::new(Mutex::new(0)), + previous_diff: Arc::new(Mutex::new("".to_string())), + example_output_dir, + last_diff_file_path, + messages_json_file_path, + repository_path, + repository_url, + }) + } +} + +impl language_model::LanguageModel for LanguageModelInterceptor { + fn id(&self) -> language_model::LanguageModelId { + self.model.id() + } + + fn name(&self) -> language_model::LanguageModelName { + self.model.name() + } + + fn provider_id(&self) -> language_model::LanguageModelProviderId { + self.model.provider_id() + } + + fn provider_name(&self) -> language_model::LanguageModelProviderName { + self.model.provider_name() + } + + fn telemetry_id(&self) -> String { + self.model.telemetry_id() + } + + fn supports_images(&self) -> bool { + self.model.supports_images() + } + + fn supports_tools(&self) -> bool { + self.model.supports_tools() + } + + fn supports_tool_choice(&self, choice: language_model::LanguageModelToolChoice) -> bool { + self.model.supports_tool_choice(choice) + } + + fn max_token_count(&self) -> u64 { + self.model.max_token_count() + } + + fn count_tokens( + &self, + request: LanguageModelRequest, + cx: &App, + ) -> future::BoxFuture<'static, Result> { + self.model.count_tokens(request, cx) + } + + fn stream_completion( + &self, + request: LanguageModelRequest, + cx: &AsyncApp, + ) -> future::BoxFuture< + 'static, + Result< + futures::stream::BoxStream< + 'static, + Result, + >, + language_model::LanguageModelCompletionError, + >, + > { + let stream = self.model.stream_completion(request.clone(), cx); + let request_count = self.request_count.clone(); + let previous_diff = self.previous_diff.clone(); + let example_output_dir = self.example_output_dir.clone(); + let last_diff_file_path = self.last_diff_file_path.clone(); + let messages_json_file_path = self.messages_json_file_path.clone(); + let repository_path = self.repository_path.clone(); + let repository_url = self.repository_url.clone(); + + Box::pin(async move { + let stream = stream.await?; + + let response_events = Arc::new(Mutex::new(Vec::new())); + let request_clone = request.clone(); + + let wrapped_stream = stream.then(move |event| { + let response_events = response_events.clone(); + let request = request_clone.clone(); + let request_count = request_count.clone(); + let previous_diff = previous_diff.clone(); + let example_output_dir = example_output_dir.clone(); + let last_diff_file_path = last_diff_file_path.clone(); + let messages_json_file_path = messages_json_file_path.clone(); + let repository_path = repository_path.clone(); + let repository_url = repository_url.clone(); + + async move { + let event_result = match &event { + Ok(ev) => Ok(ev.clone()), + Err(err) => Err(err.to_string()), + }; + response_events.lock().unwrap().push(event_result); + + let should_execute = matches!( + &event, + Ok(LanguageModelCompletionEvent::Stop { .. }) | Err(_) + ); + + if should_execute { + let current_request_count = { + let mut count = request_count.lock().unwrap(); + *count += 1; + *count + }; + + let messages_file_path = + example_output_dir.join(format!("{current_request_count}.messages.md")); + let diff_file_path = + example_output_dir.join(format!("{current_request_count}.diff")); + let last_messages_file_path = example_output_dir.join("last.messages.md"); + + let collected_events = response_events.lock().unwrap().clone(); + let request_markdown = RequestMarkdown::new(&request); + let response_events_markdown = + response_events_to_markdown(&collected_events); + let dialog = ThreadDialog::new(&request, &collected_events); + let dialog_json = + serde_json::to_string_pretty(&dialog.to_combined_request()) + .unwrap_or_default(); + + let messages = format!( + "{}\n\n{}", + request_markdown.messages, response_events_markdown + ); + fs::write(&messages_file_path, messages.clone()) + .expect("failed to write messages file"); + fs::write(&last_messages_file_path, messages) + .expect("failed to write last messages file"); + fs::write(&messages_json_file_path, dialog_json) + .expect("failed to write last.messages.json"); + + // Get repository diff + let diff_result = + ExampleInstance::repository_diff(repository_path, &repository_url) + .await; + + match diff_result { + Ok(diff) => { + let prev_diff = previous_diff.lock().unwrap().clone(); + if diff != prev_diff { + fs::write(&diff_file_path, &diff) + .expect("failed to write diff file"); + fs::write(&last_diff_file_path, &diff) + .expect("failed to write last diff file"); + *previous_diff.lock().unwrap() = diff; + } + } + Err(err) => { + let error_message = format!("{err:?}"); + fs::write(&diff_file_path, &error_message) + .expect("failed to write diff error to file"); + fs::write(&last_diff_file_path, &error_message) + .expect("failed to write last diff file"); + } + } + + if current_request_count == 1 { + let tools_file_path = example_output_dir.join("tools.md"); + fs::write(tools_file_path, request_markdown.tools) + .expect("failed to write tools file"); + } + } + + event + } + }); + + Ok(Box::pin(wrapped_stream) + as futures::stream::BoxStream< + 'static, + Result< + LanguageModelCompletionEvent, + language_model::LanguageModelCompletionError, + >, + >) + }) + } +} + pub fn wait_for_lang_server( project: &Entity, buffer: &Entity, @@ -825,40 +1061,6 @@ pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result { Ok(String::from_utf8(output.stdout)?.trim().to_string()) } -fn messages_to_markdown<'a>(message_iter: impl IntoIterator) -> String { - let mut messages = String::new(); - let mut assistant_message_number: u32 = 1; - - for message in message_iter { - push_role(&message.role, &mut messages, &mut assistant_message_number); - - for segment in &message.segments { - match segment { - MessageSegment::Text(text) => { - messages.push_str(text); - messages.push_str("\n\n"); - } - MessageSegment::Thinking { text, signature } => { - messages.push_str("**Thinking**:\n\n"); - if let Some(sig) = signature { - messages.push_str(&format!("Signature: {}\n\n", sig)); - } - messages.push_str(text); - messages.push_str("\n"); - } - MessageSegment::RedactedThinking(items) => { - messages.push_str(&format!( - "**Redacted Thinking**: {} item(s)\n\n", - items.len() - )); - } - } - } - } - - messages -} - fn push_role(role: &Role, buf: &mut String, assistant_message_number: &mut u32) { match role { Role::System => buf.push_str("# ⚙️ SYSTEM\n\n"), From cb7881ec0bc4075c3e3e581e7f86e0cee89d0a76 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 22 Oct 2025 11:17:01 -0700 Subject: [PATCH 160/202] Fix migration from #40409 for users who haven't been migrated yet (#40916) Closes #40874 Release Notes: - Fixed an issue where migrating settings after v0.208.5+ would spuriously enable prettier. This fix only affects those who have not updated or migrated yet. For those who have already updated to version v0.208.5 or later, placing `"formatter": []` in your settings in the affected languages will fix the issue. --------- Co-authored-by: Mikayla --- crates/migrator/src/migrations/m_2025_10_16/settings.rs | 2 +- crates/migrator/src/migrator.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/migrator/src/migrations/m_2025_10_16/settings.rs b/crates/migrator/src/migrations/m_2025_10_16/settings.rs index c9a8b7600cbf8842453318ed8dc9b47c3a73beaf..684a331af90ecd097376e629a3eb65a24d0609ae 100644 --- a/crates/migrator/src/migrations/m_2025_10_16/settings.rs +++ b/crates/migrator/src/migrations/m_2025_10_16/settings.rs @@ -58,7 +58,7 @@ fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Res .map(|code_action| (code_action, Value::Bool(true))), ); - obj.remove("formatter"); + obj.insert("formatter".to_string(), Value::Array(vec![])); obj.insert( "code_actions_on_format".into(), Value::Object(code_actions_map), diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index e5f0c584c407284aa175a3ac33f3c9a9e01c1365..17cedaab666cd3ee53478456fbce8198ae65d8d2 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -2016,9 +2016,9 @@ mod tests { &r#"{ "code_actions_on_format": { "foo": true - } - } - "# + }, + "formatter": [] + }"# .unindent(), ), ); @@ -2053,6 +2053,7 @@ mod tests { .unindent(), Some( &r#"{ + "formatter": [], "code_actions_on_format": { "foo": true, "bar": true, @@ -2080,6 +2081,7 @@ mod tests { .unindent(), Some( &r#"{ + "formatter": [], "code_actions_on_format": { "foo": true, "qux": true, From 6622902964c14e1632bfb3583e6ea45e56941144 Mon Sep 17 00:00:00 2001 From: Bennet Fenner Date: Wed, 22 Oct 2025 20:17:33 +0200 Subject: [PATCH 161/202] agent: Only show compatible tools in profile selector (#40917) In practice this just hides the web search tool when not using the Zed provider Release Notes: - Fixed an issue where the web search tool would show up in the profile selector even when not using a model via Zed Pro --- crates/agent/src/thread.rs | 10 +++++----- crates/agent/src/tools.rs | 10 ++++++++-- crates/agent/src/tools/web_search_tool.rs | 2 +- .../manage_profiles_modal.rs | 19 +++++++++++++++---- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index d873e4f26cb22d34c501b1d4d3ffd3af94465af4..d3414d84c8f5594a567e5b38b45ddf0739965365 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1857,7 +1857,7 @@ impl Thread { .tools .iter() .filter_map(|(tool_name, tool)| { - if tool.supported_provider(&model.provider_id()) + if tool.supports_provider(&model.provider_id()) && profile.is_tool_enabled(tool_name) { Some((truncate(tool_name), tool.clone())) @@ -2133,7 +2133,7 @@ where /// Some tools rely on a provider for the underlying billing or other reasons. /// Allow the tool to check if they are compatible, or should be filtered out. - fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { + fn supports_provider(_provider: &LanguageModelProviderId) -> bool { true } @@ -2174,7 +2174,7 @@ pub trait AnyAgentTool { fn kind(&self) -> acp::ToolKind; fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString; fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; - fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool { + fn supports_provider(&self, _provider: &LanguageModelProviderId) -> bool { true } fn run( @@ -2219,8 +2219,8 @@ where Ok(json) } - fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { - self.0.supported_provider(provider) + fn supports_provider(&self, provider: &LanguageModelProviderId) -> bool { + T::supports_provider(provider) } fn run( diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs index 831efcad8f154de9aac19d9fd587fafb345d1aad..1d3c0d557716ec3a52f910971547df4ee764cab0 100644 --- a/crates/agent/src/tools.rs +++ b/crates/agent/src/tools.rs @@ -40,13 +40,19 @@ pub use web_search_tool::*; macro_rules! tools { ($($tool:ty),* $(,)?) => { /// A list of all built-in tool names - pub fn built_in_tool_names() -> impl Iterator { + pub fn supported_built_in_tool_names(provider: Option) -> impl Iterator { [ $( - <$tool>::name().to_string(), + (if let Some(provider) = provider.as_ref() { + <$tool>::supports_provider(provider) + } else { + true + }) + .then(|| <$tool>::name().to_string()), )* ] .into_iter() + .flatten() } /// A list of all built-in tools diff --git a/crates/agent/src/tools/web_search_tool.rs b/crates/agent/src/tools/web_search_tool.rs index b65c89167d6f5ed026bb4ebb5e1990fa4e1c17ce..03e9db6601579e082e4d83de50f1999209d9f197 100644 --- a/crates/agent/src/tools/web_search_tool.rs +++ b/crates/agent/src/tools/web_search_tool.rs @@ -57,7 +57,7 @@ impl AgentTool for WebSearchTool { } /// We currently only support Zed Cloud as a provider. - fn supported_provider(&self, provider: &LanguageModelProviderId) -> bool { + fn supports_provider(provider: &LanguageModelProviderId) -> bool { provider == &ZED_CLOUD_PROVIDER_ID } diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index fc4bde2c784894b94b7ce35e6e262e52865ffcd1..ad23d68d02c16c1379479684091f77a41c758a7a 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -7,6 +7,7 @@ use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profil use editor::Editor; use fs::Fs; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*}; +use language_model::LanguageModel; use settings::Settings as _; use ui::{ KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*, @@ -96,6 +97,7 @@ pub struct NewProfileMode { pub struct ManageProfilesModal { fs: Arc, context_server_registry: Entity, + active_model: Option>, focus_handle: FocusHandle, mode: Mode, } @@ -109,9 +111,14 @@ impl ManageProfilesModal { workspace.register_action(|workspace, action: &ManageProfiles, window, cx| { if let Some(panel) = workspace.panel::(cx) { let fs = workspace.app_state().fs.clone(); + let active_model = panel + .read(cx) + .active_native_agent_thread(cx) + .and_then(|thread| thread.read(cx).model().cloned()); + let context_server_registry = panel.read(cx).context_server_registry().clone(); workspace.toggle_modal(window, cx, |window, cx| { - let mut this = Self::new(fs, context_server_registry, window, cx); + let mut this = Self::new(fs, active_model, context_server_registry, window, cx); if let Some(profile_id) = action.customize_tools.clone() { this.configure_builtin_tools(profile_id, window, cx); @@ -125,6 +132,7 @@ impl ManageProfilesModal { pub fn new( fs: Arc, + active_model: Option>, context_server_registry: Entity, window: &mut Window, cx: &mut Context, @@ -133,6 +141,7 @@ impl ManageProfilesModal { Self { fs, + active_model, context_server_registry, focus_handle, mode: Mode::choose_profile(window, cx), @@ -228,9 +237,11 @@ impl ManageProfilesModal { let tool_picker = cx.new(|cx| { let delegate = ToolPickerDelegate::builtin_tools( //todo: This causes the web search tool to show up even it only works when using zed hosted models - agent::built_in_tool_names() - .map(|s| s.into()) - .collect::>(), + agent::supported_built_in_tool_names( + self.active_model.as_ref().map(|model| model.provider_id()), + ) + .map(|s| s.into()) + .collect::>(), self.fs.clone(), profile_id.clone(), profile, From 5738bde3ce0d1e312d0f0f57dba3d89e593cf243 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 22 Oct 2025 20:54:29 +0200 Subject: [PATCH 162/202] gpui: Make `Empty` request layout with `Display::None` by default (#40900) Release Notes: - N/A --- crates/gpui/src/element.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index a3fc6269f33d8726b55f8e8be4aadb52109a7606..5fa2f9ead8274452ac04795bf68dffc571f5dc31 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -741,7 +741,17 @@ impl Element for Empty { window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), None, cx), ()) + ( + window.request_layout( + Style { + display: crate::Display::None, + ..Default::default() + }, + None, + cx, + ), + (), + ) } fn prepaint( From ed5b9a47053d18180d66b44cbc6753f9db0507a8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 22 Oct 2025 22:34:15 +0300 Subject: [PATCH 163/202] Rework inlay hints system (#40183) Closes https://github.com/zed-industries/zed/issues/40047 Closes https://github.com/zed-industries/zed/issues/24798 Closes https://github.com/zed-industries/zed/issues/24788 Before, each editor, even if it's the same buffer split in 2, was querying for inlay hints separately, and storing the whole inlay hint twice, in `Editor`'s `display_map` and its `inlay_hint_cache` fields. Now, instead of `inlay_hint_cache`, each editor maintains a minimal set of metadata (which area was queried by what task) instead, and all LSP inlay hint data had been moved into `LspStore`, both local and remote flavors store the data. This allows Zed, as long as a buffer is open, to reuse the inlay hint data similar to how document colors and code lens are now stored and reused. Unlike other reused LSP data, inlay hints data is the first one that's possible to query by document ranges and previous version had issue with caching and invalidating such ranges already queried for. The new version re-approaches this by chunking the file into row ranges, which are queried based on the editors' visible area. Among the corresponding refactoring, one notable difference in inlays display are multi buffers: buffers in them are not [registered](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen) in the language server until a caret/selection is placed inside their excerpts inside the multi buffer. New inlays code does not query language servers for unregistered buffers, as servers usually respond with empty responses or errors in such cases. Release Notes: - Reworked inlay hints to be less error-prone --------- Co-authored-by: Lukas Wirth Co-authored-by: dino Co-authored-by: Lukas Wirth --- Cargo.lock | 1 + crates/agent_ui/src/acp/message_editor.rs | 11 +- crates/collab/src/rpc.rs | 1 - crates/collab/src/tests/editor_tests.rs | 167 +- crates/diagnostics/src/diagnostics_tests.rs | 4 +- crates/editor/src/display_map.rs | 27 +- crates/editor/src/display_map/fold_map.rs | 3 +- crates/editor/src/display_map/inlay_map.rs | 111 +- crates/editor/src/editor.rs | 503 +-- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/hover_links.rs | 194 +- crates/editor/src/hover_popover.rs | 17 +- crates/editor/src/inlays.rs | 193 ++ .../inlay_hints.rs} | 3028 +++++++++-------- crates/editor/src/lsp_colors.rs | 11 +- crates/editor/src/movement.rs | 2 +- crates/editor/src/proposed_changes_editor.rs | 47 +- .../src/test/editor_lsp_test_context.rs | 50 +- crates/language/src/language.rs | 59 + crates/project/src/lsp_command.rs | 2 +- crates/project/src/lsp_store.rs | 1060 ++++-- .../project/src/lsp_store/inlay_hint_cache.rs | 221 ++ crates/project/src/project.rs | 58 +- crates/project/src/project_tests.rs | 10 +- crates/proto/proto/lsp.proto | 4 + crates/proto/src/proto.rs | 2 + crates/rpc/src/proto_client.rs | 5 + crates/search/Cargo.toml | 2 + crates/search/src/project_search.rs | 98 +- crates/util/src/paths.rs | 2 +- crates/vim/src/motion.rs | 2 +- 31 files changed, 3286 insertions(+), 2613 deletions(-) create mode 100644 crates/editor/src/inlays.rs rename crates/editor/src/{inlay_hint_cache.rs => inlays/inlay_hints.rs} (59%) create mode 100644 crates/project/src/lsp_store/inlay_hint_cache.rs diff --git a/Cargo.lock b/Cargo.lock index e426bc4ce64d540ea77fcd03decb875ebb76a572..0cc10bde430f4e527053e21d69c89c66f4d25241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14955,6 +14955,7 @@ dependencies = [ "futures 0.3.31", "gpui", "language", + "lsp", "menu", "project", "schemars 1.0.4", diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 57157e59c6b48541ff82bdc417bc119ed01bb997..53a26d9fabdd59e93efbc615ce5be5d1c2d492fb 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -11,10 +11,10 @@ use assistant_slash_commands::codeblock_fence_for_path; use collections::{HashMap, HashSet}; use editor::{ Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, InlayId, + EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay, MultiBuffer, ToOffset, actions::Paste, - display_map::{Crease, CreaseId, FoldId, Inlay}, + display_map::{Crease, CreaseId, FoldId}, }; use futures::{ FutureExt as _, @@ -29,7 +29,8 @@ use language::{Buffer, Language, language_settings::InlayHintKind}; use language_model::LanguageModelImage; use postage::stream::Stream as _; use project::{ - CompletionIntent, InlayHint, InlayHintLabel, Project, ProjectItem, ProjectPath, Worktree, + CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath, + Worktree, }; use prompt_store::{PromptId, PromptStore}; use rope::Point; @@ -75,7 +76,7 @@ pub enum MessageEditorEvent { impl EventEmitter for MessageEditor {} -const COMMAND_HINT_INLAY_ID: u32 = 0; +const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); impl MessageEditor { pub fn new( @@ -151,7 +152,7 @@ impl MessageEditor { let has_new_hint = !new_hints.is_empty(); editor.splice_inlays( if has_hint { - &[InlayId::Hint(COMMAND_HINT_INLAY_ID)] + &[COMMAND_HINT_INLAY_ID] } else { &[] }, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index fa2ca6a890af93979eed759265286d99a5a98bb2..67cde1794865ad4f305be84cdb1572ea5d620978 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -343,7 +343,6 @@ impl Server { .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) - .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) .add_request_handler(forward_read_only_project_request::) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 6a41f84697e17d85a0a9777a9285dad691d73176..f675cd3522b0f0e273db7528d62f31e37ceda794 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1849,10 +1849,40 @@ async fn test_mutual_editor_inlay_hint_cache_update( ..lsp::ServerCapabilities::default() }; client_a.language_registry().add(rust_lang()); + + // Set up the language server to return an additional inlay hint on each request. + let edits_made = Arc::new(AtomicUsize::new(0)); + let closure_edits_made = Arc::clone(&edits_made); let mut fake_language_servers = client_a.language_registry().register_fake_lsp( "Rust", FakeLspAdapter { capabilities: capabilities.clone(), + initializer: Some(Box::new(move |fake_language_server| { + let closure_edits_made = closure_edits_made.clone(); + fake_language_server.set_request_handler::( + move |params, _| { + let edits_made_2 = Arc::clone(&closure_edits_made); + async move { + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), + ); + let edits_made = + AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, edits_made as u32), + label: lsp::InlayHintLabel::String(edits_made.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + })), ..FakeLspAdapter::default() }, ); @@ -1894,61 +1924,20 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); - executor.start_waiting(); // The host opens a rust file. - let _buffer_a = project_a - .update(cx_a, |project, cx| { - project.open_local_buffer(path!("/a/main.rs"), cx) - }) - .await - .unwrap(); - let editor_a = workspace_a - .update_in(cx_a, |workspace, window, cx| { - workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - + let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| { + workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx) + }); let fake_language_server = fake_language_servers.next().await.unwrap(); - - // Set up the language server to return an additional inlay hint on each request. - let edits_made = Arc::new(AtomicUsize::new(0)); - let closure_edits_made = Arc::clone(&edits_made); - fake_language_server - .set_request_handler::(move |params, _| { - let edits_made_2 = Arc::clone(&closure_edits_made); - async move { - assert_eq!( - params.text_document.uri, - lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(), - ); - let edits_made = AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, edits_made as u32), - label: lsp::InlayHintLabel::String(edits_made.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - }) - .next() - .await - .unwrap(); - + let editor_a = file_a.await.unwrap().downcast::().unwrap(); executor.run_until_parked(); let initial_edit = edits_made.load(atomic::Ordering::Acquire); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![initial_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Host should get its first hints when opens an editor" ); }); @@ -1963,10 +1952,10 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap(); executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![initial_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Client should get its first hints when opens an editor" ); }); @@ -1981,16 +1970,16 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx_b.focus(&editor_b); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![after_client_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![after_client_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); @@ -2004,16 +1993,16 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx_a.focus(&editor_a); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![after_host_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![after_host_edit.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), ); }); @@ -2025,26 +2014,22 @@ async fn test_mutual_editor_inlay_hint_cache_update( .expect("inlay refresh request failed"); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert_eq!( vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Host should react to /refresh LSP request" ); }); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec![after_special_edit_for_refresh.to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Guest should get a /refresh LSP request propagated by host" ); }); } -// This test started hanging on seed 2 after the theme settings -// PR. The hypothesis is that it's been buggy for a while, but got lucky -// on seeds. -#[ignore] #[gpui::test(iterations = 10)] async fn test_inlay_hint_refresh_is_forwarded( cx_a: &mut TestAppContext, @@ -2206,18 +2191,18 @@ async fn test_inlay_hint_refresh_is_forwarded( executor.finish_waiting(); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert!( - extract_hint_labels(editor).is_empty(), + extract_hint_labels(editor, cx).is_empty(), "Host should get no hints due to them turned off" ); }); executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec!["initial hint".to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Client should get its first hints when opens an editor" ); }); @@ -2229,18 +2214,18 @@ async fn test_inlay_hint_refresh_is_forwarded( .into_response() .expect("inlay refresh request failed"); executor.run_until_parked(); - editor_a.update(cx_a, |editor, _| { + editor_a.update(cx_a, |editor, cx| { assert!( - extract_hint_labels(editor).is_empty(), + extract_hint_labels(editor, cx).is_empty(), "Host should get no hints due to them turned off, even after the /refresh" ); }); executor.run_until_parked(); - editor_b.update(cx_b, |editor, _| { + editor_b.update(cx_b, |editor, cx| { assert_eq!( vec!["other hint".to_string()], - extract_hint_labels(editor), + extract_hint_labels(editor, cx), "Guest should get a /refresh LSP request propagated by host despite host hints are off" ); }); @@ -4217,15 +4202,35 @@ fn tab_undo_assert( cx_b.assert_editor_state(expected_initial); } -fn extract_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for hint in editor.inlay_hint_cache().hints() { - match hint.label { - project::InlayHintLabel::String(s) => labels.push(s), - _ => unreachable!(), - } +fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + + let mut all_cached_labels = Vec::new(); + let mut all_fetched_hints = Vec::new(); + for buffer in editor.buffer().read(cx).all_buffers() { + lsp_store.update(cx, |lsp_store, cx| { + let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints(); + all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| { + let mut label = hint.text().to_string(); + if hint.padding_left { + label.insert(0, ' '); + } + if hint.padding_right { + label.push_str(" "); + } + label + })); + all_fetched_hints.extend(hints.all_fetched_hints()); + }); } - labels + + assert!( + all_fetched_hints.is_empty(), + "Did not expect background hints fetch tasks, but got {} of them", + all_fetched_hints.len() + ); + + all_cached_labels } #[track_caller] diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 74a235697834b120dd1b0dbb55aae03fe950be64..d97a5ab65aab4bb238182040821ecf9fdf828bc3 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1,9 +1,9 @@ use super::*; use collections::{HashMap, HashSet}; use editor::{ - DisplayPoint, EditorSettings, + DisplayPoint, EditorSettings, Inlay, actions::{GoToDiagnostic, GoToPreviousDiagnostic, Hover, MoveToBeginning}, - display_map::{DisplayRow, Inlay}, + display_map::DisplayRow, test::{ editor_content_with_blocks, editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aad62e0debd366a968a34e5d7b937b75f6272c0d..a6b3d904be94fdcab1b347f68c6c0b03ae091a04 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -27,7 +27,7 @@ mod tab_map; mod wrap_map; use crate::{ - EditorStyle, InlayId, RowExt, hover_links::InlayHighlight, movement::TextLayoutDetails, + EditorStyle, RowExt, hover_links::InlayHighlight, inlays::Inlay, movement::TextLayoutDetails, }; pub use block_map::{ Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement, @@ -42,7 +42,6 @@ pub use fold_map::{ ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint, }; use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle}; -pub use inlay_map::Inlay; use inlay_map::InlaySnapshot; pub use inlay_map::{InlayOffset, InlayPoint}; pub use invisibles::{is_invisible, replacement}; @@ -50,9 +49,10 @@ use language::{ OffsetUtf16, Point, Subscription as BufferSubscription, language_settings::language_settings, }; use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferPoint, MultiBufferRow, - MultiBufferSnapshot, RowInfo, ToOffset, ToPoint, + Anchor, AnchorRangeExt, MultiBuffer, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, + RowInfo, ToOffset, ToPoint, }; +use project::InlayId; use project::project_settings::DiagnosticSeverity; use serde::Deserialize; @@ -594,25 +594,6 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } - pub fn remove_inlays_for_excerpts( - &mut self, - excerpts_removed: &[ExcerptId], - cx: &mut Context, - ) { - let to_remove = self - .inlay_map - .current_inlays() - .filter_map(|inlay| { - if excerpts_removed.contains(&inlay.position.excerpt_id) { - Some(inlay.id) - } else { - None - } - }) - .collect::>(); - self.splice_inlays(&to_remove, Vec::new(), cx); - } - fn tab_size(buffer: &Entity, cx: &App) -> NonZeroU32 { let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)); let language = buffer diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index d93f5acbc65a9a39a95df51469a3bcc02989426c..a31599ef9b276246226c12640fa8ffbec57eb9e3 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,4 +1,4 @@ -use crate::{InlayId, display_map::inlay_map::InlayChunk}; +use crate::display_map::inlay_map::InlayChunk; use super::{ Highlights, @@ -9,6 +9,7 @@ use language::{Edit, HighlightId, Point, TextSummary}; use multi_buffer::{ Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, }; +use project::InlayId; use std::{ any::TypeId, cmp::{self, Ordering}, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 050cd06a9781db5812cf129968471561e2abd095..486676f1120bc2e9d85effd4c328a2b7a547e06b 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,17 +1,18 @@ -use crate::{ChunkRenderer, HighlightStyles, InlayId}; +use crate::{ + ChunkRenderer, HighlightStyles, + inlays::{Inlay, InlayContent}, +}; use collections::BTreeSet; -use gpui::{Hsla, Rgba}; use language::{Chunk, Edit, Point, TextSummary}; -use multi_buffer::{ - Anchor, MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset, -}; +use multi_buffer::{MultiBufferRow, MultiBufferRows, MultiBufferSnapshot, RowInfo, ToOffset}; +use project::InlayId; use std::{ cmp, ops::{Add, AddAssign, Range, Sub, SubAssign}, - sync::{Arc, OnceLock}, + sync::Arc, }; use sum_tree::{Bias, Cursor, Dimensions, SumTree}; -use text::{ChunkBitmaps, Patch, Rope}; +use text::{ChunkBitmaps, Patch}; use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div}; use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId}; @@ -37,85 +38,6 @@ enum Transform { Inlay(Inlay), } -#[derive(Debug, Clone)] -pub struct Inlay { - pub id: InlayId, - pub position: Anchor, - pub content: InlayContent, -} - -#[derive(Debug, Clone)] -pub enum InlayContent { - Text(text::Rope), - Color(Hsla), -} - -impl Inlay { - pub fn hint(id: u32, position: Anchor, hint: &project::InlayHint) -> Self { - let mut text = hint.text(); - if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { - text.push(" "); - } - if hint.padding_left && text.chars_at(0).next() != Some(' ') { - text.push_front(" "); - } - Self { - id: InlayId::Hint(id), - position, - content: InlayContent::Text(text), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn mock_hint(id: u32, position: Anchor, text: impl Into) -> Self { - Self { - id: InlayId::Hint(id), - position, - content: InlayContent::Text(text.into()), - } - } - - pub fn color(id: u32, position: Anchor, color: Rgba) -> Self { - Self { - id: InlayId::Color(id), - position, - content: InlayContent::Color(color.into()), - } - } - - pub fn edit_prediction>(id: u32, position: Anchor, text: T) -> Self { - Self { - id: InlayId::EditPrediction(id), - position, - content: InlayContent::Text(text.into()), - } - } - - pub fn debugger>(id: u32, position: Anchor, text: T) -> Self { - Self { - id: InlayId::DebuggerValue(id), - position, - content: InlayContent::Text(text.into()), - } - } - - pub fn text(&self) -> &Rope { - static COLOR_TEXT: OnceLock = OnceLock::new(); - match &self.content { - InlayContent::Text(text) => text, - InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn get_color(&self) -> Option { - match self.content { - InlayContent::Color(color) => Some(color), - _ => None, - } - } -} - impl sum_tree::Item for Transform { type Summary = TransformSummary; @@ -750,7 +672,7 @@ impl InlayMap { #[cfg(test)] pub(crate) fn randomly_mutate( &mut self, - next_inlay_id: &mut u32, + next_inlay_id: &mut usize, rng: &mut rand::rngs::StdRng, ) -> (InlaySnapshot, Vec) { use rand::prelude::*; @@ -1245,17 +1167,18 @@ const fn is_utf8_char_boundary(byte: u8) -> bool { mod tests { use super::*; use crate::{ - InlayId, MultiBuffer, + MultiBuffer, display_map::{HighlightKey, InlayHighlights, TextHighlights}, hover_links::InlayHighlight, }; use gpui::{App, HighlightStyle}; + use multi_buffer::Anchor; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{any::TypeId, cmp::Reverse, env, sync::Arc}; use sum_tree::TreeMap; - use text::Patch; + use text::{Patch, Rope}; use util::RandomCharIter; use util::post_inc; @@ -1263,7 +1186,7 @@ mod tests { fn test_inlay_properties_label_padding() { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), @@ -1283,7 +1206,7 @@ mod tests { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), @@ -1303,7 +1226,7 @@ mod tests { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), @@ -1323,7 +1246,7 @@ mod tests { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), @@ -1346,7 +1269,7 @@ mod tests { fn test_inlay_hint_padding_with_multibyte_chars() { assert_eq!( Inlay::hint( - 0, + InlayId::Hint(0), Anchor::min(), &InlayHint { label: InlayHintLabel::String("🎨".to_string()), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1a8c8600bf5bcbde37d0d4fcfb8a2133bba3766d..fb62438cebb9e7baf9f8a45a439465f34b921bce 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7,7 +7,6 @@ //! * [`element`] — the place where all rendering happens //! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. //! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). -//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. //! //! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). //! @@ -24,7 +23,7 @@ mod highlight_matching_bracket; mod hover_links; pub mod hover_popover; mod indent_guides; -mod inlay_hint_cache; +mod inlays; pub mod items; mod jsx_tag_auto_close; mod linked_editing_ranges; @@ -61,6 +60,7 @@ pub use element::{ }; pub use git::blame::BlameRenderer; pub use hover_popover::hover_markdown_style; +pub use inlays::Inlay; pub use items::MAX_TAB_TITLE_LEN; pub use lsp::CompletionContext; pub use lsp_ext::lsp_tasks; @@ -112,10 +112,10 @@ use gpui::{ div, point, prelude::*, pulsating_between, px, relative, size, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; -use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; +use hover_links::{HoverLink, HoveredLinkState, find_file}; use hover_popover::{HoverState, hide_hover}; use indent_guides::ActiveIndentGuidesState; -use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; +use inlays::{InlaySplice, inlay_hints::InlayHintRefreshReason}; use itertools::{Either, Itertools}; use language::{ AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow, @@ -124,8 +124,8 @@ use language::{ IndentSize, Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ - self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, - all_language_settings, language_settings, + self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, + language_settings, }, point_from_lsp, point_to_lsp, text_diff_with_options, }; @@ -146,9 +146,9 @@ use parking_lot::Mutex; use persistence::DB; use project::{ BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent, - CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, - Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectPath, - ProjectTransaction, TaskSourceKind, + CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId, + InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, + ProjectPath, ProjectTransaction, TaskSourceKind, debugger::{ breakpoint_store::{ Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState, @@ -157,7 +157,10 @@ use project::{ session::{Session, SessionEvent}, }, git_store::{GitStoreEvent, RepositoryEvent}, - lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, + lsp_store::{ + CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget, + OpenLspBufferHandle, + }, project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter, ProjectSettings}, }; use rand::seq::SliceRandom; @@ -178,7 +181,7 @@ use std::{ iter::{self, Peekable}, mem, num::NonZeroU32, - ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive}, + ops::{Deref, DerefMut, Not, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -208,6 +211,10 @@ use crate::{ code_context_menus::CompletionsMenuSource, editor_settings::MultiCursorModifier, hover_links::{find_url, find_url_from_range}, + inlays::{ + InlineValueCache, + inlay_hints::{LspInlayHintData, inlay_hint_settings}, + }, scroll::{ScrollOffset, ScrollPixelOffset}, signature_help::{SignatureHelpHiddenBy, SignatureHelpState}, }; @@ -261,42 +268,6 @@ impl ReportEditorEvent { } } -struct InlineValueCache { - enabled: bool, - inlays: Vec, - refresh_task: Task>, -} - -impl InlineValueCache { - fn new(enabled: bool) -> Self { - Self { - enabled, - inlays: Vec::new(), - refresh_task: Task::ready(None), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InlayId { - EditPrediction(u32), - DebuggerValue(u32), - // LSP - Hint(u32), - Color(u32), -} - -impl InlayId { - fn id(&self) -> u32 { - match self { - Self::EditPrediction(id) => *id, - Self::DebuggerValue(id) => *id, - Self::Hint(id) => *id, - Self::Color(id) => *id, - } - } -} - pub enum ActiveDebugLine {} pub enum DebugStackFrameLine {} enum DocumentHighlightRead {} @@ -1124,9 +1095,8 @@ pub struct Editor { edit_prediction_preview: EditPredictionPreview, edit_prediction_indent_conflict: bool, edit_prediction_requires_modifier_in_indent_conflict: bool, - inlay_hint_cache: InlayHintCache, - next_inlay_id: u32, - next_color_inlay_id: u32, + next_inlay_id: usize, + next_color_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, gutter_dimensions: GutterDimensions, @@ -1193,10 +1163,19 @@ pub struct Editor { colors: Option, post_scroll_update: Task<()>, refresh_colors_task: Task<()>, + inlay_hints: Option, folding_newlines: Task<()>, pub lookup_key: Option>, } +fn debounce_value(debounce_ms: u64) -> Option { + if debounce_ms > 0 { + Some(Duration::from_millis(debounce_ms)) + } else { + None + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] enum NextScrollCursorCenterTopBottom { #[default] @@ -1621,31 +1600,6 @@ pub enum GotoDefinitionKind { Implementation, } -#[derive(Debug, Clone)] -enum InlayHintRefreshReason { - ModifiersChanged(bool), - Toggle(bool), - SettingsChange(InlayHintSettings), - NewLinesShown, - BufferEdited(HashSet>), - RefreshRequested, - ExcerptsRemoved(Vec), -} - -impl InlayHintRefreshReason { - fn description(&self) -> &'static str { - match self { - Self::ModifiersChanged(_) => "modifiers changed", - Self::Toggle(_) => "toggle", - Self::SettingsChange(_) => "settings change", - Self::NewLinesShown => "new lines shown", - Self::BufferEdited(_) => "buffer edited", - Self::RefreshRequested => "refresh requested", - Self::ExcerptsRemoved(_) => "excerpts removed", - } - } -} - pub enum FormatTarget { Buffers(HashSet>), Ranges(Vec>), @@ -1881,8 +1835,11 @@ impl Editor { project::Event::RefreshCodeLens => { // we always query lens with actions, without storing them, always refreshing them } - project::Event::RefreshInlayHints => { - editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); + project::Event::RefreshInlayHints(server_id) => { + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested(*server_id), + cx, + ); } project::Event::LanguageServerRemoved(..) => { if editor.tasks_update_task.is_none() { @@ -1919,17 +1876,12 @@ impl Editor { project::Event::LanguageServerBufferRegistered { buffer_id, .. } => { let buffer_id = *buffer_id; if editor.buffer().read(cx).buffer(buffer_id).is_some() { - let registered = editor.register_buffer(buffer_id, cx); - if registered { - editor.update_lsp_data(Some(buffer_id), window, cx); - editor.refresh_inlay_hints( - InlayHintRefreshReason::RefreshRequested, - cx, - ); - refresh_linked_ranges(editor, window, cx); - editor.refresh_code_actions(window, cx); - editor.refresh_document_highlights(cx); - } + editor.register_buffer(buffer_id, cx); + editor.update_lsp_data(Some(buffer_id), window, cx); + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + refresh_linked_ranges(editor, window, cx); + editor.refresh_code_actions(window, cx); + editor.refresh_document_highlights(cx); } } @@ -2200,7 +2152,6 @@ impl Editor { diagnostics_enabled: full_mode, word_completions_enabled: full_mode, inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), - inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -2266,6 +2217,7 @@ impl Editor { pull_diagnostics_task: Task::ready(()), colors: None, refresh_colors_task: Task::ready(()), + inlay_hints: None, next_color_inlay_id: 0, post_scroll_update: Task::ready(()), linked_edit_ranges: Default::default(), @@ -2403,13 +2355,15 @@ impl Editor { editor.go_to_active_debug_line(window, cx); - if let Some(buffer) = multi_buffer.read(cx).as_singleton() { - editor.register_buffer(buffer.read(cx).remote_id(), cx); - } - editor.minimap = editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); editor.colors = Some(LspColorData::new(cx)); + editor.inlay_hints = Some(LspInlayHintData::new(inlay_hint_settings)); + + if let Some(buffer) = multi_buffer.read(cx).as_singleton() { + editor.register_buffer(buffer.read(cx).remote_id(), cx); + } + editor.update_lsp_data(None, window, cx); editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx); } @@ -5198,179 +5152,8 @@ impl Editor { } } - pub fn toggle_inline_values( - &mut self, - _: &ToggleInlineValues, - _: &mut Window, - cx: &mut Context, - ) { - self.inline_value_cache.enabled = !self.inline_value_cache.enabled; - - self.refresh_inline_values(cx); - } - - pub fn toggle_inlay_hints( - &mut self, - _: &ToggleInlayHints, - _: &mut Window, - cx: &mut Context, - ) { - self.refresh_inlay_hints( - InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), - cx, - ); - } - - pub fn inlay_hints_enabled(&self) -> bool { - self.inlay_hint_cache.enabled - } - - pub fn inline_values_enabled(&self) -> bool { - self.inline_value_cache.enabled - } - - #[cfg(any(test, feature = "test-support"))] - pub fn inline_value_inlays(&self, cx: &App) -> Vec { - self.display_map - .read(cx) - .current_inlays() - .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_))) - .cloned() - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn all_inlays(&self, cx: &App) -> Vec { - self.display_map - .read(cx) - .current_inlays() - .cloned() - .collect() - } - - fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { - if self.semantics_provider.is_none() || !self.mode.is_full() { - return; - } - - let reason_description = reason.description(); - let ignore_debounce = matches!( - reason, - InlayHintRefreshReason::SettingsChange(_) - | InlayHintRefreshReason::Toggle(_) - | InlayHintRefreshReason::ExcerptsRemoved(_) - | InlayHintRefreshReason::ModifiersChanged(_) - ); - let (invalidate_cache, required_languages) = match reason { - InlayHintRefreshReason::ModifiersChanged(enabled) => { - match self.inlay_hint_cache.modifiers_override(enabled) { - Some(enabled) => { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.clear_inlay_hints(cx); - return; - } - } - None => return, - } - } - InlayHintRefreshReason::Toggle(enabled) => { - if self.inlay_hint_cache.toggle(enabled) { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.clear_inlay_hints(cx); - return; - } - } else { - return; - } - } - InlayHintRefreshReason::SettingsChange(new_settings) => { - match self.inlay_hint_cache.update_settings( - &self.buffer, - new_settings, - self.visible_inlay_hints(cx).cloned().collect::>(), - cx, - ) { - ControlFlow::Break(Some(InlaySplice { - to_remove, - to_insert, - })) => { - self.splice_inlays(&to_remove, to_insert, cx); - return; - } - ControlFlow::Break(None) => return, - ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), - } - } - InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed) - { - self.splice_inlays(&to_remove, to_insert, cx); - } - self.display_map.update(cx, |display_map, cx| { - display_map.remove_inlays_for_excerpts(&excerpts_removed, cx) - }); - return; - } - InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), - InlayHintRefreshReason::BufferEdited(buffer_languages) => { - (InvalidationStrategy::BufferEdited, Some(buffer_languages)) - } - InlayHintRefreshReason::RefreshRequested => { - (InvalidationStrategy::RefreshRequested, None) - } - }; - - let mut visible_excerpts = self.visible_excerpts(required_languages.as_ref(), cx); - visible_excerpts.retain(|_, (buffer, _, _)| { - self.registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - }); - - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.spawn_hint_refresh( - reason_description, - visible_excerpts, - invalidate_cache, - ignore_debounce, - cx, - ) { - self.splice_inlays(&to_remove, to_insert, cx); - } - } - - pub fn clear_inlay_hints(&self, cx: &mut Context) { - self.splice_inlays( - &self - .visible_inlay_hints(cx) - .map(|inlay| inlay.id) - .collect::>(), - Vec::new(), - cx, - ); - } - - fn visible_inlay_hints<'a>( - &'a self, - cx: &'a Context, - ) -> impl Iterator { - self.display_map - .read(cx) - .current_inlays() - .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) - } - pub fn visible_excerpts( &self, - restrict_to_languages: Option<&HashSet>>, cx: &mut Context, ) -> HashMap, clock::Global, Range)> { let Some(project) = self.project() else { @@ -5389,9 +5172,8 @@ impl Editor { + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), Bias::Left, ); - let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; multi_buffer_snapshot - .range_to_buffer_ranges(multi_buffer_visible_range) + .range_to_buffer_ranges(multi_buffer_visible_start..multi_buffer_visible_end) .into_iter() .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { @@ -5401,23 +5183,17 @@ impl Editor { .read(cx) .entry_for_id(buffer_file.project_entry_id()?)?; if worktree_entry.is_ignored { - return None; - } - - let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages - && !restrict_to_languages.contains(language) - { - return None; + None + } else { + Some(( + excerpt_id, + ( + multi_buffer.buffer(buffer.remote_id()).unwrap(), + buffer.version().clone(), + excerpt_visible_range, + ), + )) } - Some(( - excerpt_id, - ( - multi_buffer.buffer(buffer.remote_id()).unwrap(), - buffer.version().clone(), - excerpt_visible_range, - ), - )) }) .collect() } @@ -5433,18 +5209,6 @@ impl Editor { } } - pub fn splice_inlays( - &self, - to_remove: &[InlayId], - to_insert: Vec, - cx: &mut Context, - ) { - self.display_map.update(cx, |display_map, cx| { - display_map.splice_inlays(to_remove, to_insert, cx) - }); - cx.notify(); - } - fn trigger_on_type_formatting( &self, input: String, @@ -17618,9 +17382,9 @@ impl Editor { HashSet::default(), cx, ); - cx.emit(project::Event::RefreshInlayHints); }); }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } } @@ -20808,18 +20572,6 @@ impl Editor { cx.notify(); } - pub(crate) fn highlight_inlays( - &mut self, - highlights: Vec, - style: HighlightStyle, - cx: &mut Context, - ) { - self.display_map.update(cx, |map, _| { - map.highlight_inlays(TypeId::of::(), highlights, style) - }); - cx.notify(); - } - pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a App, @@ -20970,38 +20722,19 @@ impl Editor { self.update_visible_edit_prediction(window, cx); } - if let Some(edited_buffer) = edited_buffer { - if edited_buffer.read(cx).file().is_none() { + if let Some(buffer) = edited_buffer { + if buffer.read(cx).file().is_none() { cx.emit(EditorEvent::TitleChanged); } - let buffer_id = edited_buffer.read(cx).remote_id(); - if let Some(project) = self.project.clone() { + if self.project.is_some() { + let buffer_id = buffer.read(cx).remote_id(); self.register_buffer(buffer_id, cx); self.update_lsp_data(Some(buffer_id), window, cx); - #[allow(clippy::mutable_key_type)] - let languages_affected = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .all_buffers() - .into_iter() - .filter_map(|buffer| { - buffer.update(cx, |buffer, cx| { - let language = buffer.language()?; - let should_discard = project.update(cx, |project, cx| { - project.is_local() - && !project.has_language_servers_for(buffer, cx) - }); - should_discard.not().then_some(language.clone()) - }) - }) - .collect::>() - }); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); - } + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(buffer_id), + cx, + ); } } @@ -21048,6 +20781,9 @@ impl Editor { ids, removed_buffer_ids, } => { + if let Some(inlay_hints) = &mut self.inlay_hints { + inlay_hints.remove_inlay_chunk_data(removed_buffer_ids); + } self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); for buffer_id in removed_buffer_ids { self.registered_buffers.remove(buffer_id); @@ -21222,7 +20958,7 @@ impl Editor { if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| { colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors) }) { - if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() { + if !inlay_splice.is_empty() { self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx); } self.refresh_colors_for_visible_range(None, window, cx); @@ -21684,10 +21420,6 @@ impl Editor { mouse_context_menu::deploy_context_menu(self, None, position, window, cx); } - pub fn inlay_hint_cache(&self) -> &InlayHintCache { - &self.inlay_hint_cache - } - pub fn replay_insert_event( &mut self, text: &str, @@ -21726,21 +21458,6 @@ impl Editor { self.handle_input(text, window, cx); } - pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { - let Some(provider) = self.semantics_provider.as_ref() else { - return false; - }; - - let mut supports = false; - self.buffer().update(cx, |this, cx| { - this.for_each_buffer(|buffer| { - supports |= provider.supports_inlay_hints(buffer, cx); - }); - }); - - supports - } - pub fn is_focused(&self, window: &Window) -> bool { self.focus_handle.is_focused(window) } @@ -22156,12 +21873,12 @@ impl Editor { if self.ignore_lsp_data() { return; } - for (_, (visible_buffer, _, _)) in self.visible_excerpts(None, cx) { + for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) { self.register_buffer(visible_buffer.read(cx).remote_id(), cx); } } - fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) -> bool { + fn register_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { if !self.registered_buffers.contains_key(&buffer_id) && let Some(project) = self.project.as_ref() { @@ -22172,13 +21889,10 @@ impl Editor { project.register_buffer_with_language_servers(&buffer, cx), ); }); - return true; } else { self.registered_buffers.remove(&buffer_id); } } - - false } fn ignore_lsp_data(&self) -> bool { @@ -22886,20 +22600,23 @@ pub trait SemanticsProvider { cx: &mut App, ) -> Option>>>; - fn inlay_hints( + fn applicable_inlay_chunks( &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>>; + buffer_id: BufferId, + ranges: &[Range], + cx: &App, + ) -> Vec>; + + fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App); - fn resolve_inlay_hint( + fn inlay_hints( &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, + invalidate: InvalidationStrategy, + buffer: Entity, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut App, - ) -> Option>>; + ) -> Option, Task>>>; fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; @@ -23392,26 +23109,34 @@ impl SemanticsProvider for Entity { }) } - fn inlay_hints( + fn applicable_inlay_chunks( &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| { - project.inlay_hints(buffer_handle, range, cx) - })) + buffer_id: BufferId, + ranges: &[Range], + cx: &App, + ) -> Vec> { + self.read(cx) + .lsp_store() + .read(cx) + .applicable_inlay_chunks(buffer_id, ranges) + } + + fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App) { + self.read(cx).lsp_store().update(cx, |lsp_store, _| { + lsp_store.invalidate_inlay_hints(for_buffers) + }); } - fn resolve_inlay_hint( + fn inlay_hints( &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, + invalidate: InvalidationStrategy, + buffer: Entity, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| { - project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) + ) -> Option, Task>>> { + Some(self.read(cx).lsp_store().update(cx, |lsp_store, cx| { + lsp_store.inlay_hints(invalidate, buffer, ranges, known_chunks, cx) })) } @@ -23460,16 +23185,6 @@ impl SemanticsProvider for Entity { } } -fn inlay_hint_settings( - location: Anchor, - snapshot: &MultiBufferSnapshot, - cx: &mut Context, -) -> InlayHintSettings { - let file = snapshot.file_at(location); - let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints -} - fn consume_contiguous_rows( contiguous_row_selections: &mut Vec>, selection: &Selection, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7a085d4f4fe0701b1dc9117144c819aeccd9005e..ea2ae32ba9a9d589b937e3cbc7939cd8b5bc1b2a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -31,6 +31,7 @@ use language::{ tree_sitter_python, }; use language_settings::Formatter; +use languages::rust_lang; use lsp::CompletionParams; use multi_buffer::{IndentGuide, PathKey}; use parking_lot::Mutex; @@ -50,7 +51,7 @@ use std::{ iter, sync::atomic::{self, AtomicUsize}, }; -use test::{build_editor_with_project, editor_lsp_test_context::rust_lang}; +use test::build_editor_with_project; use text::ToPoint as _; use unindent::Unindent; use util::{ @@ -12640,6 +12641,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { ); } }); + cx.run_until_parked(); // Handle formatting requests to the language server. cx.lsp diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 4a1a6a934678adb9512ee6883684d2ecb1b2d90a..f36c82b20277fc748620928e6d7fc49a2b20cd3e 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -1,19 +1,14 @@ use crate::{ Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition, - GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, InlayId, - Navigated, PointForPosition, SelectPhase, - editor_settings::GoToDefinitionFallback, - hover_popover::{self, InlayHover}, + GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind, + Navigated, PointForPosition, SelectPhase, editor_settings::GoToDefinitionFallback, scroll::ScrollAmount, }; use gpui::{App, AsyncWindowContext, Context, Entity, Modifiers, Task, Window, px}; use language::{Bias, ToOffset}; use linkify::{LinkFinder, LinkKind}; use lsp::LanguageServerId; -use project::{ - HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, Project, - ResolveState, ResolvedPath, -}; +use project::{InlayId, LocationLink, Project, ResolvedPath}; use settings::Settings; use std::ops::Range; use theme::ActiveTheme as _; @@ -138,10 +133,9 @@ impl Editor { show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx); } None => { - update_inlay_link_and_hover_points( + self.update_inlay_link_and_hover_points( snapshot, point_for_position, - self, hovered_link_modifier, modifiers.shift, window, @@ -283,182 +277,6 @@ impl Editor { } } -pub fn update_inlay_link_and_hover_points( - snapshot: &EditorSnapshot, - point_for_position: PointForPosition, - editor: &mut Editor, - secondary_held: bool, - shift_held: bool, - window: &mut Window, - cx: &mut Context, -) { - let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { - Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) - } else { - None - }; - let mut go_to_definition_updated = false; - let mut hover_updated = false; - if let Some(hovered_offset) = hovered_offset { - let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); - let previous_valid_anchor = - buffer_snapshot.anchor_before(point_for_position.previous_valid.to_point(snapshot)); - let next_valid_anchor = - buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot)); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .skip_while(|hint| { - hint.position - .cmp(&previous_valid_anchor, &buffer_snapshot) - .is_lt() - }) - .take_while(|hint| { - hint.position - .cmp(&next_valid_anchor, &buffer_snapshot) - .is_le() - }) - .max_by_key(|hint| hint.id) - { - let inlay_hint_cache = editor.inlay_hint_cache(); - let excerpt_id = previous_valid_anchor.excerpt_id; - if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { - match cached_hint.resolve_state { - ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = snapshot - .buffer_snapshot() - .buffer_id_for_anchor(previous_valid_anchor) - { - inlay_hint_cache.spawn_hint_resolve( - buffer_id, - excerpt_id, - hovered_hint.id, - window, - cx, - ); - } - } - ResolveState::Resolved => { - let mut extra_shift_left = 0; - let mut extra_shift_right = 0; - if cached_hint.padding_left { - extra_shift_left += 1; - extra_shift_right += 1; - } - if cached_hint.padding_right { - extra_shift_right += 1; - } - match cached_hint.label { - project::InlayHintLabel::String(_) => { - if let Some(tooltip) = cached_hint.tooltip { - hover_popover::hover_at_inlay( - editor, - InlayHover { - tooltip: match tooltip { - InlayHintTooltip::String(text) => HoverBlock { - text, - kind: HoverBlockKind::PlainText, - }, - InlayHintTooltip::MarkupContent(content) => { - HoverBlock { - text: content.value, - kind: content.kind, - } - } - }, - range: InlayHighlight { - inlay: hovered_hint.id, - inlay_position: hovered_hint.position, - range: extra_shift_left - ..hovered_hint.text().len() + extra_shift_right, - }, - }, - window, - cx, - ); - hover_updated = true; - } - } - project::InlayHintLabel::LabelParts(label_parts) => { - let hint_start = - snapshot.anchor_to_inlay_offset(hovered_hint.position); - if let Some((hovered_hint_part, part_range)) = - hover_popover::find_hovered_hint_part( - label_parts, - hint_start, - hovered_offset, - ) - { - let highlight_start = - (part_range.start - hint_start).0 + extra_shift_left; - let highlight_end = - (part_range.end - hint_start).0 + extra_shift_right; - let highlight = InlayHighlight { - inlay: hovered_hint.id, - inlay_position: hovered_hint.position, - range: highlight_start..highlight_end, - }; - if let Some(tooltip) = hovered_hint_part.tooltip { - hover_popover::hover_at_inlay( - editor, - InlayHover { - tooltip: match tooltip { - InlayHintLabelPartTooltip::String(text) => { - HoverBlock { - text, - kind: HoverBlockKind::PlainText, - } - } - InlayHintLabelPartTooltip::MarkupContent( - content, - ) => HoverBlock { - text: content.value, - kind: content.kind, - }, - }, - range: highlight.clone(), - }, - window, - cx, - ); - hover_updated = true; - } - if let Some((language_server_id, location)) = - hovered_hint_part.location - && secondary_held - && !editor.has_pending_nonempty_selection() - { - go_to_definition_updated = true; - show_link_definition( - shift_held, - editor, - TriggerPoint::InlayHint( - highlight, - location, - language_server_id, - ), - snapshot, - window, - cx, - ); - } - } - } - }; - } - ResolveState::Resolving => {} - } - } - } - } - - if !go_to_definition_updated { - editor.hide_hovered_link(cx) - } - if !hover_updated { - hover_popover::hover_at(editor, None, window, cx); - } -} - pub fn show_link_definition( shift_held: bool, editor: &mut Editor, @@ -912,7 +730,7 @@ mod tests { DisplayPoint, display_map::ToDisplayPoint, editor_tests::init_test, - inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels}, test::editor_lsp_test_context::EditorLspTestContext, }; use futures::StreamExt; @@ -1343,7 +1161,7 @@ mod tests { cx.background_executor.run_until_parked(); cx.update_editor(|editor, _window, cx| { let expected_layers = vec![hint_label.to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, cached_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 9db04363b27959d8f8b81539da4ba65c75fbeb02..19213638f417d20cd54868305ea9e39d57363fca 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -986,17 +986,17 @@ impl DiagnosticPopover { mod tests { use super::*; use crate::{ - InlayId, PointForPosition, + PointForPosition, actions::ConfirmCompletion, editor_tests::{handle_completion_request, init_test}, - hover_links::update_inlay_link_and_hover_points, - inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + inlays::inlay_hints::tests::{cached_hint_labels, visible_hint_labels}, test::editor_lsp_test_context::EditorLspTestContext, }; use collections::BTreeSet; use gpui::App; use indoc::indoc; use markdown::parser::MarkdownEvent; + use project::InlayId; use settings::InlayHintSettingsContent; use smol::stream::StreamExt; use std::sync::atomic; @@ -1648,7 +1648,7 @@ mod tests { cx.background_executor.run_until_parked(); cx.update_editor(|editor, _, cx| { let expected_layers = vec![entire_hint_label.to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, cached_hint_labels(editor, cx)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); @@ -1687,10 +1687,9 @@ mod tests { } }); cx.update_editor(|editor, window, cx| { - update_inlay_link_and_hover_points( + editor.update_inlay_link_and_hover_points( &editor.snapshot(window, cx), new_type_hint_part_hover_position, - editor, true, false, window, @@ -1758,10 +1757,9 @@ mod tests { cx.background_executor.run_until_parked(); cx.update_editor(|editor, window, cx| { - update_inlay_link_and_hover_points( + editor.update_inlay_link_and_hover_points( &editor.snapshot(window, cx), new_type_hint_part_hover_position, - editor, true, false, window, @@ -1813,10 +1811,9 @@ mod tests { } }); cx.update_editor(|editor, window, cx| { - update_inlay_link_and_hover_points( + editor.update_inlay_link_and_hover_points( &editor.snapshot(window, cx), struct_hint_part_hover_position, - editor, true, false, window, diff --git a/crates/editor/src/inlays.rs b/crates/editor/src/inlays.rs new file mode 100644 index 0000000000000000000000000000000000000000..f07bf0b315161f0ce9cdf3ef7e2f6db6d60abfb5 --- /dev/null +++ b/crates/editor/src/inlays.rs @@ -0,0 +1,193 @@ +//! The logic, responsible for managing [`Inlay`]s in the editor. +//! +//! Inlays are "not real" text that gets mixed into the "real" buffer's text. +//! They are attached to a certain [`Anchor`], and display certain contents (usually, strings) +//! between real text around that anchor. +//! +//! Inlay examples in Zed: +//! * inlay hints, received from LSP +//! * inline values, shown in the debugger +//! * inline predictions, showing the Zeta/Copilot/etc. predictions +//! * document color values, if configured to be displayed as inlays +//! * ... anything else, potentially. +//! +//! Editor uses [`crate::DisplayMap`] and [`crate::display_map::InlayMap`] to manage what's rendered inside the editor, using +//! [`InlaySplice`] to update this state. + +/// Logic, related to managing LSP inlay hint inlays. +pub mod inlay_hints; + +use std::{any::TypeId, sync::OnceLock}; + +use gpui::{Context, HighlightStyle, Hsla, Rgba, Task}; +use multi_buffer::Anchor; +use project::{InlayHint, InlayId}; +use text::Rope; + +use crate::{Editor, hover_links::InlayHighlight}; + +/// A splice to send into the `inlay_map` for updating the visible inlays on the screen. +/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes. +/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead. +/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen. +#[derive(Debug, Default)] +pub struct InlaySplice { + pub to_remove: Vec, + pub to_insert: Vec, +} + +impl InlaySplice { + pub fn is_empty(&self) -> bool { + self.to_remove.is_empty() && self.to_insert.is_empty() + } +} + +#[derive(Debug, Clone)] +pub struct Inlay { + pub id: InlayId, + pub position: Anchor, + pub content: InlayContent, +} + +#[derive(Debug, Clone)] +pub enum InlayContent { + Text(text::Rope), + Color(Hsla), +} + +impl Inlay { + pub fn hint(id: InlayId, position: Anchor, hint: &InlayHint) -> Self { + let mut text = hint.text(); + if hint.padding_right && text.reversed_chars_at(text.len()).next() != Some(' ') { + text.push(" "); + } + if hint.padding_left && text.chars_at(0).next() != Some(' ') { + text.push_front(" "); + } + Self { + id, + position, + content: InlayContent::Text(text), + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn mock_hint(id: usize, position: Anchor, text: impl Into) -> Self { + Self { + id: InlayId::Hint(id), + position, + content: InlayContent::Text(text.into()), + } + } + + pub fn color(id: usize, position: Anchor, color: Rgba) -> Self { + Self { + id: InlayId::Color(id), + position, + content: InlayContent::Color(color.into()), + } + } + + pub fn edit_prediction>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::EditPrediction(id), + position, + content: InlayContent::Text(text.into()), + } + } + + pub fn debugger>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::DebuggerValue(id), + position, + content: InlayContent::Text(text.into()), + } + } + + pub fn text(&self) -> &Rope { + static COLOR_TEXT: OnceLock = OnceLock::new(); + match &self.content { + InlayContent::Text(text) => text, + InlayContent::Color(_) => COLOR_TEXT.get_or_init(|| Rope::from("◼")), + } + } + + #[cfg(any(test, feature = "test-support"))] + pub fn get_color(&self) -> Option { + match self.content { + InlayContent::Color(color) => Some(color), + _ => None, + } + } +} + +pub struct InlineValueCache { + pub enabled: bool, + pub inlays: Vec, + pub refresh_task: Task>, +} + +impl InlineValueCache { + pub fn new(enabled: bool) -> Self { + Self { + enabled, + inlays: Vec::new(), + refresh_task: Task::ready(None), + } + } +} + +impl Editor { + /// Modify which hints are displayed in the editor. + pub fn splice_inlays( + &mut self, + to_remove: &[InlayId], + to_insert: Vec, + cx: &mut Context, + ) { + if let Some(inlay_hints) = &mut self.inlay_hints { + for id_to_remove in to_remove { + inlay_hints.added_hints.remove(id_to_remove); + } + } + self.display_map.update(cx, |display_map, cx| { + display_map.splice_inlays(to_remove, to_insert, cx) + }); + cx.notify(); + } + + pub(crate) fn highlight_inlays( + &mut self, + highlights: Vec, + style: HighlightStyle, + cx: &mut Context, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), highlights, style) + }); + cx.notify(); + } + + pub fn inline_values_enabled(&self) -> bool { + self.inline_value_cache.enabled + } + + #[cfg(any(test, feature = "test-support"))] + pub fn inline_value_inlays(&self, cx: &gpui::App) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(|inlay| matches!(inlay.id, InlayId::DebuggerValue(_))) + .cloned() + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn all_inlays(&self, cx: &gpui::App) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .cloned() + .collect() + } +} diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlays/inlay_hints.rs similarity index 59% rename from crates/editor/src/inlay_hint_cache.rs rename to crates/editor/src/inlays/inlay_hints.rs index 34d737452e905449c259fb41fe03a96e34159b05..07faf8446749085ed24795451c241c0a5747335f 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlays/inlay_hints.rs @@ -1,295 +1,116 @@ -/// Stores and updates all data received from LSP
textDocument/inlayHint requests. -/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere. -/// On every update, cache may query for more inlay hints and update inlays on the screen. -/// -/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map. -/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work. -/// -/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes. use std::{ - cmp, + collections::hash_map, ops::{ControlFlow, Range}, sync::Arc, time::Duration, }; -use crate::{ - Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, display_map::Inlay, -}; -use anyhow::Context as _; use clock::Global; -use futures::future; -use gpui::{AppContext as _, AsyncApp, Context, Entity, Task, Window}; +use collections::{HashMap, HashSet}; +use futures::future::join_all; +use gpui::{App, Entity, Task}; use language::{ - Buffer, BufferSnapshot, - language_settings::{InlayHintKind, InlayHintSettings}, + BufferRow, + language_settings::{InlayHintKind, InlayHintSettings, language_settings}, }; -use parking_lot::RwLock; -use project::{InlayHint, ResolveState}; +use lsp::LanguageServerId; +use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot}; +use parking_lot::Mutex; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabel, InlayHintLabelPartTooltip, InlayHintTooltip, + InvalidationStrategy, ResolveState, + lsp_store::{CacheInlayHints, ResolvedHint}, +}; +use text::{Bias, BufferId}; +use ui::{Context, Window}; +use util::debug_panic; -use collections::{HashMap, HashSet, hash_map}; -use smol::lock::Semaphore; -use sum_tree::Bias; -use text::{BufferId, ToOffset, ToPoint}; -use util::{ResultExt, post_inc}; +use super::{Inlay, InlayId}; +use crate::{ + Editor, EditorSnapshot, PointForPosition, ToggleInlayHints, ToggleInlineValues, debounce_value, + hover_links::{InlayHighlight, TriggerPoint, show_link_definition}, + hover_popover::{self, InlayHover}, + inlays::InlaySplice, +}; -pub struct InlayHintCache { - hints: HashMap>>, - allowed_hint_kinds: HashSet>, - version: usize, - pub(super) enabled: bool, +pub fn inlay_hint_settings( + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut Context, +) -> InlayHintSettings { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location).map(|l| l.name()); + language_settings(language, file, cx).inlay_hints +} + +#[derive(Debug)] +pub struct LspInlayHintData { + enabled: bool, modifiers_override: bool, enabled_in_settings: bool, - update_tasks: HashMap, - refresh_task: Task<()>, + allowed_hint_kinds: HashSet>, invalidate_debounce: Option, append_debounce: Option, - lsp_request_limiter: Arc, -} - -#[derive(Debug)] -struct TasksForRanges { - tasks: Vec>, - sorted_ranges: Vec>, -} - -#[derive(Debug)] -struct CachedExcerptHints { - version: usize, - buffer_version: Global, - buffer_id: BufferId, - ordered_hints: Vec, - hints_by_id: HashMap, -} - -/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. -#[derive(Debug, Clone, Copy)] -pub(super) enum InvalidationStrategy { - /// Hints reset is requested by the LSP server. - /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. - /// - /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. - RefreshRequested, - /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. - /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. - BufferEdited, - /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. - /// No invalidation should be done at all, all new hints are added to the cache. - /// - /// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other). - /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. - None, -} - -/// A splice to send into the `inlay_map` for updating the visible inlays on the screen. -/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes. -/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead. -/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen. -#[derive(Debug, Default)] -pub(super) struct InlaySplice { - pub to_remove: Vec, - pub to_insert: Vec, -} - -#[derive(Debug)] -struct ExcerptHintsUpdate { - excerpt_id: ExcerptId, - remove_from_visible: HashSet, - remove_from_cache: HashSet, - add_to_cache: Vec, -} - -#[derive(Debug, Clone, Copy)] -struct ExcerptQuery { - buffer_id: BufferId, - excerpt_id: ExcerptId, - cache_version: usize, - invalidate: InvalidationStrategy, - reason: &'static str, -} - -impl InvalidationStrategy { - fn should_invalidate(&self) -> bool { - matches!( - self, - InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited - ) - } + hint_refresh_tasks: HashMap>, Vec>>>, + hint_chunk_fetched: HashMap>)>, + pub added_hints: HashMap>, } -impl TasksForRanges { - fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { +impl LspInlayHintData { + pub fn new(settings: InlayHintSettings) -> Self { Self { - tasks: vec![task], - sorted_ranges: query_ranges.into_sorted_query_ranges(), + modifiers_override: false, + enabled: settings.enabled, + enabled_in_settings: settings.enabled, + hint_refresh_tasks: HashMap::default(), + added_hints: HashMap::default(), + hint_chunk_fetched: HashMap::default(), + invalidate_debounce: debounce_value(settings.edit_debounce_ms), + append_debounce: debounce_value(settings.scroll_debounce_ms), + allowed_hint_kinds: settings.enabled_inlay_hint_kinds(), } } - fn update_cached_tasks( - &mut self, - buffer_snapshot: &BufferSnapshot, - query_ranges: QueryRanges, - invalidate: InvalidationStrategy, - spawn_task: impl FnOnce(QueryRanges) -> Task<()>, - ) { - let query_ranges = if invalidate.should_invalidate() { - self.tasks.clear(); - self.sorted_ranges = query_ranges.clone().into_sorted_query_ranges(); - query_ranges + pub fn modifiers_override(&mut self, new_override: bool) -> Option { + if self.modifiers_override == new_override { + return None; + } + self.modifiers_override = new_override; + if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) + { + self.clear(); + Some(false) } else { - let mut non_cached_query_ranges = query_ranges; - non_cached_query_ranges.before_visible = non_cached_query_ranges - .before_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - non_cached_query_ranges.visible = non_cached_query_ranges - .visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - non_cached_query_ranges.after_visible = non_cached_query_ranges - .after_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - non_cached_query_ranges - }; - - if !query_ranges.is_empty() { - self.tasks.push(spawn_task(query_ranges)); + Some(true) } } - fn remove_cached_ranges_from_query( - &mut self, - buffer_snapshot: &BufferSnapshot, - query_range: Range, - ) -> Vec> { - let mut ranges_to_query = Vec::new(); - let mut latest_cached_range = None::<&mut Range>; - for cached_range in self - .sorted_ranges - .iter_mut() - .skip_while(|cached_range| { - cached_range - .end - .cmp(&query_range.start, buffer_snapshot) - .is_lt() - }) - .take_while(|cached_range| { - cached_range - .start - .cmp(&query_range.end, buffer_snapshot) - .is_le() - }) - { - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset - { - ranges_to_query.push(latest_cached_range.end..cached_range.start); - cached_range.start = latest_cached_range.end; - } - } - None => { - if query_range - .start - .cmp(&cached_range.start, buffer_snapshot) - .is_lt() - { - ranges_to_query.push(query_range.start..cached_range.start); - cached_range.start = query_range.start; - } - } - } - latest_cached_range = Some(cached_range); + pub fn toggle(&mut self, enabled: bool) -> bool { + if self.enabled == enabled { + return false; } - - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { - ranges_to_query.push(latest_cached_range.end..query_range.end); - latest_cached_range.end = query_range.end; - } - } - None => { - ranges_to_query.push(query_range.clone()); - self.sorted_ranges.push(query_range); - self.sorted_ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); - } + self.enabled = enabled; + self.modifiers_override = false; + if !enabled { + self.clear(); } - - ranges_to_query - } - - fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range) { - self.sorted_ranges = self - .sorted_ranges - .drain(..) - .filter_map(|mut cached_range| { - if cached_range.start.cmp(&range.end, buffer).is_gt() - || cached_range.end.cmp(&range.start, buffer).is_lt() - { - Some(vec![cached_range]) - } else if cached_range.start.cmp(&range.start, buffer).is_ge() - && cached_range.end.cmp(&range.end, buffer).is_le() - { - None - } else if range.start.cmp(&cached_range.start, buffer).is_ge() - && range.end.cmp(&cached_range.end, buffer).is_le() - { - Some(vec![ - cached_range.start..range.start, - range.end..cached_range.end, - ]) - } else if cached_range.start.cmp(&range.start, buffer).is_ge() { - cached_range.start = range.end; - Some(vec![cached_range]) - } else { - cached_range.end = range.start; - Some(vec![cached_range]) - } - }) - .flatten() - .collect(); + true } -} -impl InlayHintCache { - pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self { - Self { - allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), - enabled: inlay_hint_settings.enabled, - modifiers_override: false, - enabled_in_settings: inlay_hint_settings.enabled, - hints: HashMap::default(), - update_tasks: HashMap::default(), - refresh_task: Task::ready(()), - invalidate_debounce: debounce_value(inlay_hint_settings.edit_debounce_ms), - append_debounce: debounce_value(inlay_hint_settings.scroll_debounce_ms), - version: 0, - lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), - } + pub fn clear(&mut self) { + self.hint_refresh_tasks.clear(); + self.hint_chunk_fetched.clear(); + self.added_hints.clear(); } /// Checks inlay hint settings for enabled hint kinds and general enabled state. /// Generates corresponding inlay_map splice updates on settings changes. /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries. - pub(super) fn update_settings( + fn update_settings( &mut self, - multi_buffer: &Entity, new_hint_settings: InlayHintSettings, visible_hints: Vec, - cx: &mut Context, - ) -> ControlFlow> { + ) -> ControlFlow, Option> { let old_enabled = self.enabled; // If the setting for inlay hints has changed, update `enabled`. This condition avoids inlay // hint visibility changes when other settings change (such as theme). @@ -314,23 +135,30 @@ impl InlayHintCache { if new_allowed_hint_kinds == self.allowed_hint_kinds { ControlFlow::Break(None) } else { - let new_splice = self.new_allowed_hint_kinds_splice( - multi_buffer, - &visible_hints, - &new_allowed_hint_kinds, - cx, - ); - if new_splice.is_some() { - self.version += 1; - self.allowed_hint_kinds = new_allowed_hint_kinds; - } - ControlFlow::Break(new_splice) + self.allowed_hint_kinds = new_allowed_hint_kinds; + ControlFlow::Continue( + Some(InlaySplice { + to_remove: visible_hints + .iter() + .filter_map(|inlay| { + let inlay_kind = self.added_hints.get(&inlay.id).copied()?; + if !self.allowed_hint_kinds.contains(&inlay_kind) { + Some(inlay.id) + } else { + None + } + }) + .collect(), + to_insert: Vec::new(), + }) + .filter(|splice| !splice.is_empty()), + ) } } (true, false) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; - if self.hints.is_empty() { + if visible_hints.is_empty() { ControlFlow::Break(None) } else { self.clear(); @@ -343,978 +171,770 @@ impl InlayHintCache { (false, true) => { self.modifiers_override = false; self.allowed_hint_kinds = new_allowed_hint_kinds; - ControlFlow::Continue(()) + ControlFlow::Continue( + Some(InlaySplice { + to_remove: visible_hints + .iter() + .filter_map(|inlay| { + let inlay_kind = self.added_hints.get(&inlay.id).copied()?; + if !self.allowed_hint_kinds.contains(&inlay_kind) { + Some(inlay.id) + } else { + None + } + }) + .collect(), + to_insert: Vec::new(), + }) + .filter(|splice| !splice.is_empty()), + ) } } } - pub(super) fn modifiers_override(&mut self, new_override: bool) -> Option { - if self.modifiers_override == new_override { - return None; - } - self.modifiers_override = new_override; - if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) - { - self.clear(); - Some(false) - } else { - Some(true) + pub(crate) fn remove_inlay_chunk_data<'a>( + &'a mut self, + removed_buffer_ids: impl IntoIterator + 'a, + ) { + for buffer_id in removed_buffer_ids { + self.hint_refresh_tasks.remove(buffer_id); + self.hint_chunk_fetched.remove(buffer_id); } } +} - pub(super) fn toggle(&mut self, enabled: bool) -> bool { - if self.enabled == enabled { +#[derive(Debug, Clone)] +pub enum InlayHintRefreshReason { + ModifiersChanged(bool), + Toggle(bool), + SettingsChange(InlayHintSettings), + NewLinesShown, + BufferEdited(BufferId), + RefreshRequested(LanguageServerId), + ExcerptsRemoved(Vec), +} + +impl Editor { + pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { + let Some(provider) = self.semantics_provider.as_ref() else { return false; - } - self.enabled = enabled; - self.modifiers_override = false; - if !enabled { - self.clear(); - } - true + }; + + let mut supports = false; + self.buffer().update(cx, |this, cx| { + this.for_each_buffer(|buffer| { + supports |= provider.supports_inlay_hints(buffer, cx); + }); + }); + + supports } - /// If needed, queries LSP for new inlay hints, using the invalidation strategy given. - /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first, - /// followed by the delayed queries of the same range above and below the visible one. - /// This way, subsequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. - pub(super) fn spawn_hint_refresh( + pub fn toggle_inline_values( &mut self, - reason_description: &'static str, - excerpts_to_query: HashMap, Global, Range)>, - invalidate: InvalidationStrategy, - ignore_debounce: bool, - cx: &mut Context, - ) -> Option { - if (self.enabled && self.modifiers_override) || (!self.enabled && !self.modifiers_override) - { - return None; - } - let mut invalidated_hints = Vec::new(); - if invalidate.should_invalidate() { - self.update_tasks - .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); - self.hints.retain(|cached_excerpt, cached_hints| { - let retain = excerpts_to_query.contains_key(cached_excerpt); - if !retain { - invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied()); - } - retain - }); - } - if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { - return None; - } + _: &ToggleInlineValues, + _: &mut Window, + cx: &mut Context, + ) { + self.inline_value_cache.enabled = !self.inline_value_cache.enabled; - let cache_version = self.version + 1; - let debounce_duration = if ignore_debounce { - None - } else if invalidate.should_invalidate() { - self.invalidate_debounce - } else { - self.append_debounce - }; - self.refresh_task = cx.spawn(async move |editor, cx| { - if let Some(debounce_duration) = debounce_duration { - cx.background_executor().timer(debounce_duration).await; - } + self.refresh_inline_values(cx); + } - editor - .update(cx, |editor, cx| { - spawn_new_update_tasks( - editor, - reason_description, - excerpts_to_query, - invalidate, - cache_version, - cx, - ) - }) - .ok(); - }); + pub fn toggle_inlay_hints( + &mut self, + _: &ToggleInlayHints, + _: &mut Window, + cx: &mut Context, + ) { + self.refresh_inlay_hints( + InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), + cx, + ); + } - if invalidated_hints.is_empty() { - None - } else { - Some(InlaySplice { - to_remove: invalidated_hints, - to_insert: Vec::new(), - }) - } + pub fn inlay_hints_enabled(&self) -> bool { + self.inlay_hints.as_ref().is_some_and(|cache| cache.enabled) } - fn new_allowed_hint_kinds_splice( - &self, - multi_buffer: &Entity, - visible_hints: &[Inlay], - new_kinds: &HashSet>, - cx: &mut Context, - ) -> Option { - let old_kinds = &self.allowed_hint_kinds; - if new_kinds == old_kinds { - return None; + /// Updates inlay hints for the visible ranges of the singleton buffer(s). + /// Based on its parameters, either invalidates the previous data, or appends to it. + pub(crate) fn refresh_inlay_hints( + &mut self, + reason: InlayHintRefreshReason, + cx: &mut Context, + ) { + if !self.mode.is_full() || self.inlay_hints.is_none() { + return; } + let Some(semantics_provider) = self.semantics_provider() else { + return; + }; + let Some(invalidate_cache) = self.refresh_editor_data(&reason, cx) else { + return; + }; - let mut to_remove = Vec::new(); - let mut to_insert = Vec::new(); - let mut shown_hints_to_remove = visible_hints.iter().fold( - HashMap::>::default(), - |mut current_hints, inlay| { - current_hints - .entry(inlay.position.excerpt_id) - .or_default() - .push((inlay.position, inlay.id)); - current_hints - }, - ); + let debounce = match &reason { + InlayHintRefreshReason::SettingsChange(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) + | InlayHintRefreshReason::ModifiersChanged(_) => None, + _may_need_lsp_call => self.inlay_hints.as_ref().and_then(|inlay_hints| { + if invalidate_cache.should_invalidate() { + inlay_hints.invalidate_debounce + } else { + inlay_hints.append_debounce + } + }), + }; - let multi_buffer = multi_buffer.read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - - for (excerpt_id, excerpt_cached_hints) in &self.hints { - let shown_excerpt_hints_to_remove = - shown_hints_to_remove.entry(*excerpt_id).or_default(); - let excerpt_cached_hints = excerpt_cached_hints.read(); - let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable(); - shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { - let Some(buffer) = multi_buffer.buffer_for_anchor(*shown_anchor, cx) else { - return false; + let mut visible_excerpts = self.visible_excerpts(cx); + let mut all_affected_buffers = HashSet::default(); + let ignore_previous_fetches = match reason { + InlayHintRefreshReason::ModifiersChanged(_) + | InlayHintRefreshReason::Toggle(_) + | InlayHintRefreshReason::SettingsChange(_) => true, + InlayHintRefreshReason::NewLinesShown + | InlayHintRefreshReason::RefreshRequested(_) + | InlayHintRefreshReason::ExcerptsRemoved(_) => false, + InlayHintRefreshReason::BufferEdited(buffer_id) => { + let Some(affected_language) = self + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| buffer.read(cx).language().cloned()) + else { + return; }; - let buffer_snapshot = buffer.read(cx).snapshot(); - loop { - match excerpt_cache.peek() { - Some(&cached_hint_id) => { - let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; - if cached_hint_id == shown_hint_id { - excerpt_cache.next(); - return !new_kinds.contains(&cached_hint.kind); - } - match cached_hint - .position - .cmp(&shown_anchor.text_anchor, &buffer_snapshot) - { - cmp::Ordering::Less | cmp::Ordering::Equal => { - if !old_kinds.contains(&cached_hint.kind) - && new_kinds.contains(&cached_hint.kind) - && let Some(anchor) = multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - cached_hint, - )); - } - excerpt_cache.next(); - } - cmp::Ordering::Greater => return true, + all_affected_buffers.extend( + self.buffer() + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + if buffer.language() == Some(&affected_language) { + Some(buffer.remote_id()) + } else { + None } - } - None => return true, - } - } - }); + }), + ); - for cached_hint_id in excerpt_cache { - let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id]; - let cached_hint_kind = maybe_missed_cached_hint.kind; - if !old_kinds.contains(&cached_hint_kind) - && new_kinds.contains(&cached_hint_kind) - && let Some(anchor) = multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position) - { - to_insert.push(Inlay::hint( - cached_hint_id.id(), - anchor, - maybe_missed_cached_hint, - )); - } + semantics_provider.invalidate_inlay_hints(&all_affected_buffers, cx); + visible_excerpts.retain(|_, (visible_buffer, _, _)| { + visible_buffer.read(cx).language() == Some(&affected_language) + }); + false } - } + }; - to_remove.extend( - shown_hints_to_remove - .into_values() - .flatten() - .map(|(_, hint_id)| hint_id), - ); - if to_remove.is_empty() && to_insert.is_empty() { - None - } else { - Some(InlaySplice { - to_remove, - to_insert, - }) + let Some(inlay_hints) = self.inlay_hints.as_mut() else { + return; + }; + + if invalidate_cache.should_invalidate() { + inlay_hints.clear(); } - } - /// Completely forget of certain excerpts that were removed from the multibuffer. - pub(super) fn remove_excerpts( - &mut self, - excerpts_removed: &[ExcerptId], - ) -> Option { - let mut to_remove = Vec::new(); - for excerpt_to_remove in excerpts_removed { - self.update_tasks.remove(excerpt_to_remove); - if let Some(cached_hints) = self.hints.remove(excerpt_to_remove) { - let cached_hints = cached_hints.read(); - to_remove.extend(cached_hints.ordered_hints.iter().copied()); + let mut buffers_to_query = HashMap::default(); + for (excerpt_id, (buffer, buffer_version, visible_range)) in visible_excerpts { + let buffer_id = buffer.read(cx).remote_id(); + if !self.registered_buffers.contains_key(&buffer_id) { + continue; } - } - if to_remove.is_empty() { - None - } else { - self.version += 1; - Some(InlaySplice { - to_remove, - to_insert: Vec::new(), - }) - } - } - pub(super) fn clear(&mut self) { - if !self.update_tasks.is_empty() || !self.hints.is_empty() { - self.version += 1; + let buffer_snapshot = buffer.read(cx).snapshot(); + let buffer_anchor_range = buffer_snapshot.anchor_before(visible_range.start) + ..buffer_snapshot.anchor_after(visible_range.end); + + let visible_excerpts = + buffers_to_query + .entry(buffer_id) + .or_insert_with(|| VisibleExcerpts { + excerpts: Vec::new(), + ranges: Vec::new(), + buffer_version: buffer_version.clone(), + buffer: buffer.clone(), + }); + visible_excerpts.buffer_version = buffer_version; + visible_excerpts.excerpts.push(excerpt_id); + visible_excerpts.ranges.push(buffer_anchor_range); } - self.update_tasks.clear(); - self.refresh_task = Task::ready(()); - self.hints.clear(); - } - pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { - self.hints - .get(&excerpt_id)? - .read() - .hints_by_id - .get(&hint_id) - .cloned() - } + let all_affected_buffers = Arc::new(Mutex::new(all_affected_buffers)); + for (buffer_id, visible_excerpts) in buffers_to_query { + let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default(); + if visible_excerpts + .buffer_version + .changed_since(&fetched_tasks.0) + { + fetched_tasks.1.clear(); + fetched_tasks.0 = visible_excerpts.buffer_version.clone(); + inlay_hints.hint_refresh_tasks.remove(&buffer_id); + } - pub fn hints(&self) -> Vec { - let mut hints = Vec::new(); - for excerpt_hints in self.hints.values() { - let excerpt_hints = excerpt_hints.read(); - hints.extend( - excerpt_hints - .ordered_hints - .iter() - .map(|id| &excerpt_hints.hints_by_id[id]) - .cloned(), - ); - } - hints - } + let applicable_chunks = + semantics_provider.applicable_inlay_chunks(buffer_id, &visible_excerpts.ranges, cx); - /// Queries a certain hint from the cache for extra data via the LSP resolve request. - pub(super) fn spawn_hint_resolve( - &self, - buffer_id: BufferId, - excerpt_id: ExcerptId, - id: InlayId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) - && let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state + match inlay_hints + .hint_refresh_tasks + .entry(buffer_id) + .or_default() + .entry(applicable_chunks) { - let hint_to_resolve = cached_hint.clone(); - let server_id = *server_id; - cached_hint.resolve_state = ResolveState::Resolving; - drop(guard); - cx.spawn_in(window, async move |editor, cx| { - let resolved_hint_task = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).buffer(buffer_id)?; - editor.semantics_provider.as_ref()?.resolve_inlay_hint( - hint_to_resolve, - buffer, - server_id, + hash_map::Entry::Occupied(mut o) => { + if invalidate_cache.should_invalidate() || ignore_previous_fetches { + o.get_mut().push(spawn_editor_hints_refresh( + buffer_id, + invalidate_cache, + ignore_previous_fetches, + debounce, + visible_excerpts, + all_affected_buffers.clone(), cx, - ) - })?; - if let Some(resolved_hint_task) = resolved_hint_task { - let mut resolved_hint = - resolved_hint_task.await.context("hint resolve task")?; - editor.read_with(cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) - { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) - && cached_hint.resolve_state == ResolveState::Resolving - { - resolved_hint.resolve_state = ResolveState::Resolved; - *cached_hint = resolved_hint; - } - } - })?; + )); } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + } + hash_map::Entry::Vacant(v) => { + v.insert(Vec::new()).push(spawn_editor_hints_refresh( + buffer_id, + invalidate_cache, + ignore_previous_fetches, + debounce, + visible_excerpts, + all_affected_buffers.clone(), + cx, + )); + } } } } -} -fn debounce_value(debounce_ms: u64) -> Option { - if debounce_ms > 0 { - Some(Duration::from_millis(debounce_ms)) - } else { - None + pub fn clear_inlay_hints(&mut self, cx: &mut Context) { + let to_remove = self + .visible_inlay_hints(cx) + .into_iter() + .map(|inlay| { + let inlay_id = inlay.id; + if let Some(inlay_hints) = &mut self.inlay_hints { + inlay_hints.added_hints.remove(&inlay_id); + } + inlay_id + }) + .collect::>(); + self.splice_inlays(&to_remove, Vec::new(), cx); } -} - -fn spawn_new_update_tasks( - editor: &mut Editor, - reason: &'static str, - excerpts_to_query: HashMap, Global, Range)>, - invalidate: InvalidationStrategy, - update_cache_version: usize, - cx: &mut Context, -) { - for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in - excerpts_to_query - { - if excerpt_visible_range.is_empty() { - continue; - } - let buffer = excerpt_buffer.read(cx); - let buffer_id = buffer.remote_id(); - let buffer_snapshot = buffer.snapshot(); - if buffer_snapshot - .version() - .changed_since(&new_task_buffer_version) - { - continue; - } - - if let Some(cached_excerpt_hints) = editor.inlay_hint_cache.hints.get(&excerpt_id) { - let cached_excerpt_hints = cached_excerpt_hints.read(); - let cached_buffer_version = &cached_excerpt_hints.buffer_version; - if cached_excerpt_hints.version > update_cache_version - || cached_buffer_version.changed_since(&new_task_buffer_version) - { - continue; - } - }; - let Some(query_ranges) = editor.buffer.update(cx, |multi_buffer, cx| { - determine_query_ranges( - multi_buffer, - excerpt_id, - &excerpt_buffer, - excerpt_visible_range, - cx, - ) - }) else { - return; - }; - let query = ExcerptQuery { - buffer_id, - excerpt_id, - cache_version: update_cache_version, - invalidate, - reason, + fn refresh_editor_data( + &mut self, + reason: &InlayHintRefreshReason, + cx: &mut Context<'_, Editor>, + ) -> Option { + let visible_inlay_hints = self.visible_inlay_hints(cx); + let Some(inlay_hints) = self.inlay_hints.as_mut() else { + return None; }; - let mut new_update_task = - |query_ranges| new_update_task(query, query_ranges, excerpt_buffer.clone(), cx); - - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - o.get_mut().update_cached_tasks( - &buffer_snapshot, - query_ranges, - invalidate, - new_update_task, - ); - } - hash_map::Entry::Vacant(v) => { - v.insert(TasksForRanges::new( - query_ranges.clone(), - new_update_task(query_ranges), - )); + let invalidate_cache = match reason { + InlayHintRefreshReason::ModifiersChanged(enabled) => { + match inlay_hints.modifiers_override(*enabled) { + Some(enabled) => { + if enabled { + InvalidationStrategy::None + } else { + self.clear_inlay_hints(cx); + return None; + } + } + None => return None, + } } - } - } -} - -#[derive(Debug, Clone)] -struct QueryRanges { - before_visible: Vec>, - visible: Vec>, - after_visible: Vec>, -} - -impl QueryRanges { - fn is_empty(&self) -> bool { - self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty() - } - - fn into_sorted_query_ranges(self) -> Vec> { - let mut sorted_ranges = Vec::with_capacity( - self.before_visible.len() + self.visible.len() + self.after_visible.len(), - ); - sorted_ranges.extend(self.before_visible); - sorted_ranges.extend(self.visible); - sorted_ranges.extend(self.after_visible); - sorted_ranges - } -} - -fn determine_query_ranges( - multi_buffer: &mut MultiBuffer, - excerpt_id: ExcerptId, - excerpt_buffer: &Entity, - excerpt_visible_range: Range, - cx: &mut Context, -) -> Option { - let buffer = excerpt_buffer.read(cx); - let full_excerpt_range = multi_buffer - .excerpts_for_buffer(buffer.remote_id(), cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context)?; - let snapshot = buffer.snapshot(); - let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - - let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { - return None; - } else { - vec![ - buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) - ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), - ] - }; - - let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); - let after_visible_range_start = excerpt_visible_range - .end - .saturating_add(1) - .min(full_excerpt_range_end_offset) - .min(buffer.len()); - let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset { - Vec::new() - } else { - let after_range_end_offset = after_visible_range_start - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range_end_offset) - .min(buffer.len()); - vec![ - buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) - ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), - ] - }; - - let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); - let before_visible_range_end = excerpt_visible_range - .start - .saturating_sub(1) - .max(full_excerpt_range_start_offset); - let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset { - Vec::new() - } else { - let before_range_start_offset = before_visible_range_end - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range_start_offset); - vec![ - buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) - ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), - ] - }; - - Some(QueryRanges { - before_visible: before_visible_range, - visible: visible_range, - after_visible: after_visible_range, - }) -} - -const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; -const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; - -fn new_update_task( - query: ExcerptQuery, - query_ranges: QueryRanges, - excerpt_buffer: Entity, - cx: &mut Context, -) -> Task<()> { - cx.spawn(async move |editor, cx| { - let visible_range_update_results = future::join_all( - query_ranges - .visible - .into_iter() - .filter_map(|visible_range| { - let fetch_task = editor - .update(cx, |_, cx| { - fetch_and_update_hints( - excerpt_buffer.clone(), - query, - visible_range.clone(), - query.invalidate.should_invalidate(), - cx, - ) - }) - .log_err()?; - Some(async move { (visible_range, fetch_task.await) }) - }), - ) - .await; - - let hint_delay = cx.background_executor().timer(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - )); - - let query_range_failed = - |range: &Range, e: anyhow::Error, cx: &mut AsyncApp| { - log::error!("inlay hint update task for range failed: {e:#?}"); - editor - .update(cx, |editor, cx| { - if let Some(task_ranges) = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) + InlayHintRefreshReason::Toggle(enabled) => { + if inlay_hints.toggle(*enabled) { + if *enabled { + InvalidationStrategy::None + } else { + self.clear_inlay_hints(cx); + return None; + } + } else { + return None; + } + } + InlayHintRefreshReason::SettingsChange(new_settings) => { + match inlay_hints.update_settings(*new_settings, visible_inlay_hints) { + ControlFlow::Break(Some(InlaySplice { + to_remove, + to_insert, + })) => { + self.splice_inlays(&to_remove, to_insert, cx); + return None; + } + ControlFlow::Break(None) => return None, + ControlFlow::Continue(splice) => { + if let Some(InlaySplice { + to_remove, + to_insert, + }) = splice { - let buffer_snapshot = excerpt_buffer.read(cx).snapshot(); - task_ranges.invalidate_range(&buffer_snapshot, range); + self.splice_inlays(&to_remove, to_insert, cx); } - }) - .ok() - }; - - for (range, result) in visible_range_update_results { - if let Err(e) = result { - query_range_failed(&range, e, cx); + InvalidationStrategy::None + } + } } - } - - hint_delay.await; - let invisible_range_update_results = future::join_all( - query_ranges - .before_visible - .into_iter() - .chain(query_ranges.after_visible.into_iter()) - .filter_map(|invisible_range| { - let fetch_task = editor - .update(cx, |_, cx| { - fetch_and_update_hints( - excerpt_buffer.clone(), - query, - invisible_range.clone(), - false, // visible screen request already invalidated the entries - cx, - ) - }) - .log_err()?; - Some(async move { (invisible_range, fetch_task.await) }) - }), - ) - .await; - for (range, result) in invisible_range_update_results { - if let Err(e) = result { - query_range_failed(&range, e, cx); + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + let to_remove = self + .display_map + .read(cx) + .current_inlays() + .filter_map(|inlay| { + if excerpts_removed.contains(&inlay.position.excerpt_id) { + Some(inlay.id) + } else { + None + } + }) + .collect::>(); + self.splice_inlays(&to_remove, Vec::new(), cx); + return None; } - } - }) -} - -fn fetch_and_update_hints( - excerpt_buffer: Entity, - query: ExcerptQuery, - fetch_range: Range, - invalidate: bool, - cx: &mut Context, -) -> Task> { - cx.spawn(async move |editor, cx|{ - let buffer_snapshot = excerpt_buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let (lsp_request_limiter, multi_buffer_snapshot) = - editor.update(cx, |editor, cx| { - let multi_buffer_snapshot = - editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); - let lsp_request_limiter = Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter); - (lsp_request_limiter, multi_buffer_snapshot) - })?; - - let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { - (None, false) - } else { - match lsp_request_limiter.try_acquire() { - Some(guard) => (Some(guard), false), - None => (Some(lsp_request_limiter.acquire().await), true), + InlayHintRefreshReason::NewLinesShown => InvalidationStrategy::None, + InlayHintRefreshReason::BufferEdited(_) => InvalidationStrategy::BufferEdited, + InlayHintRefreshReason::RefreshRequested(server_id) => { + InvalidationStrategy::RefreshRequested(*server_id) } }; - let fetch_range_to_log = fetch_range.start.to_point(&buffer_snapshot) - ..fetch_range.end.to_point(&buffer_snapshot); - let inlay_hints_fetch_task = editor - .update(cx, |editor, cx| { - if got_throttled { - let query_not_around_visible_range = match editor - .visible_excerpts(None, cx) - .remove(&query.excerpt_id) - { - Some((_, _, current_visible_range)) => { - let visible_offset_length = current_visible_range.len(); - let double_visible_range = current_visible_range - .start - .saturating_sub(visible_offset_length) - ..current_visible_range - .end - .saturating_add(visible_offset_length) - .min(buffer_snapshot.len()); - !double_visible_range - .contains(&fetch_range.start.to_offset(&buffer_snapshot)) - && !double_visible_range - .contains(&fetch_range.end.to_offset(&buffer_snapshot)) - } - None => true, - }; - if query_not_around_visible_range { - log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); - if let Some(task_ranges) = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - { - task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); - } - return None; - } - } - let buffer = editor.buffer().read(cx).buffer(query.buffer_id)?; + match &mut self.inlay_hints { + Some(inlay_hints) => { + if !inlay_hints.enabled + && !matches!(reason, InlayHintRefreshReason::ModifiersChanged(_)) + { + return None; + } + } + None => return None, + } - if !editor.registered_buffers.contains_key(&query.buffer_id) - && let Some(project) = editor.project.as_ref() { - project.update(cx, |project, cx| { - editor.registered_buffers.insert( - query.buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } + Some(invalidate_cache) + } - editor - .semantics_provider - .as_ref()? - .inlay_hints(buffer, fetch_range.clone(), cx) - }) - .ok() - .flatten(); + pub(crate) fn visible_inlay_hints(&self, cx: &Context) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) + .cloned() + .collect() + } - let cached_excerpt_hints = editor.read_with(cx, |editor, _| { - editor - .inlay_hint_cache - .hints - .get(&query.excerpt_id) - .cloned() - })?; - - let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::>())?; - let new_hints = match inlay_hints_fetch_task { - Some(fetch_task) => { - log::debug!( - "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", - query_reason = query.reason, - ); - log::trace!( - "Currently visible hints: {visible_hints:?}, cached hints present: {}", - cached_excerpt_hints.is_some(), - ); - fetch_task.await.context("inlay hint fetch task")? - } - None => return Ok(()), + pub fn update_inlay_link_and_hover_points( + &mut self, + snapshot: &EditorSnapshot, + point_for_position: PointForPosition, + secondary_held: bool, + shift_held: bool, + window: &mut Window, + cx: &mut Context, + ) { + let Some(lsp_store) = self.project().map(|project| project.read(cx).lsp_store()) else { + return; }; - drop(lsp_request_guard); - log::debug!( - "Fetched {} hints for range {fetch_range_to_log:?}", - new_hints.len() - ); - log::trace!("Fetched hints: {new_hints:?}"); - - let background_task_buffer_snapshot = buffer_snapshot.clone(); - let background_fetch_range = fetch_range.clone(); - let new_update = cx.background_spawn(async move { - calculate_hint_updates( - query.excerpt_id, - invalidate, - background_fetch_range, - new_hints, - &background_task_buffer_snapshot, - cached_excerpt_hints, - &visible_hints, + let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { + Some( + snapshot + .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), ) - }) - .await; - if let Some(new_update) = new_update { - log::debug!( - "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", - new_update.remove_from_visible.len(), - new_update.remove_from_cache.len(), - new_update.add_to_cache.len() + } else { + None + }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = self.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, ); - log::trace!("New update: {new_update:?}"); - editor - .update(cx, |editor, cx| { - apply_hint_update( - editor, - new_update, - query, - invalidate, - buffer_snapshot, - multi_buffer_snapshot, - cx, - ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = self + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() }) - .ok(); - } - anyhow::Ok(()) - }) -} - -fn calculate_hint_updates( - excerpt_id: ExcerptId, - invalidate: bool, - fetch_range: Range, - new_excerpt_hints: Vec, - buffer_snapshot: &BufferSnapshot, - cached_excerpt_hints: Option>>, - visible_hints: &[Inlay], -) -> Option { - let mut add_to_cache = Vec::::new(); - let mut excerpt_hints_to_persist = HashMap::default(); - for new_hint in new_excerpt_hints { - if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { - continue; - } - let missing_from_cache = match &cached_excerpt_hints { - Some(cached_excerpt_hints) => { - let cached_excerpt_hints = cached_excerpt_hints.read(); - match cached_excerpt_hints - .ordered_hints - .binary_search_by(|probe| { - cached_excerpt_hints.hints_by_id[probe] - .position - .cmp(&new_hint.position, buffer_snapshot) - }) { - Ok(ix) => { - let mut missing_from_cache = true; - for id in &cached_excerpt_hints.ordered_hints[ix..] { - let cached_hint = &cached_excerpt_hints.hints_by_id[id]; - if new_hint - .position - .cmp(&cached_hint.position, buffer_snapshot) - .is_gt() - { - break; + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + if let Some(ResolvedHint::Resolved(cached_hint)) = + hovered_hint.position.buffer_id.and_then(|buffer_id| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store.resolved_hint(buffer_id, hovered_hint.id, cx) + }) + }) + { + match cached_hint.resolve_state { + ResolveState::Resolved => { + let mut extra_shift_left = 0; + let mut extra_shift_right = 0; + if cached_hint.padding_left { + extra_shift_left += 1; + extra_shift_right += 1; } - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*id, cached_hint.kind); - missing_from_cache = false; + if cached_hint.padding_right { + extra_shift_right += 1; } + match cached_hint.label { + InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + self, + InlayHover { + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + range: InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: extra_shift_left + ..hovered_hint.text().len() + + extra_shift_right, + }, + }, + window, + cx, + ); + hover_updated = true; + } + } + InlayHintLabel::LabelParts(label_parts) => { + let hint_start = + snapshot.anchor_to_inlay_offset(hovered_hint.position); + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start, + hovered_offset, + ) + { + let highlight_start = + (part_range.start - hint_start).0 + extra_shift_left; + let highlight_end = + (part_range.end - hint_start).0 + extra_shift_right; + let highlight = InlayHighlight { + inlay: hovered_hint.id, + inlay_position: hovered_hint.position, + range: highlight_start..highlight_end, + }; + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + self, + InlayHover { + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + range: highlight.clone(), + }, + window, + cx, + ); + hover_updated = true; + } + if let Some((language_server_id, location)) = + hovered_hint_part.location + && secondary_held + && !self.has_pending_nonempty_selection() + { + go_to_definition_updated = true; + show_link_definition( + shift_held, + self, + TriggerPoint::InlayHint( + highlight, + location, + language_server_id, + ), + snapshot, + window, + cx, + ); + } + } + } + }; } - missing_from_cache + ResolveState::CanResolve(_, _) => debug_panic!( + "Expected resolved_hint retrieval to return a resolved hint" + ), + ResolveState::Resolving => {} } - Err(_) => true, } } - None => true, - }; - if missing_from_cache { - add_to_cache.push(new_hint); + } + + if !go_to_definition_updated { + self.hide_hovered_link(cx) + } + if !hover_updated { + hover_popover::hover_at(self, None, window, cx); } } - let mut remove_from_visible = HashSet::default(); - let mut remove_from_cache = HashSet::default(); - if invalidate { - remove_from_visible.extend( - visible_hints - .iter() - .filter(|hint| hint.position.excerpt_id == excerpt_id) - .map(|inlay_hint| inlay_hint.id) - .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), - ); + fn inlay_hints_for_buffer( + &mut self, + invalidate_cache: InvalidationStrategy, + ignore_previous_fetches: bool, + buffer_excerpts: VisibleExcerpts, + cx: &mut Context, + ) -> Option, anyhow::Result)>>> { + let semantics_provider = self.semantics_provider()?; + let inlay_hints = self.inlay_hints.as_mut()?; + let buffer_id = buffer_excerpts.buffer.read(cx).remote_id(); + + let new_hint_tasks = semantics_provider + .inlay_hints( + invalidate_cache, + buffer_excerpts.buffer, + buffer_excerpts.ranges, + inlay_hints + .hint_chunk_fetched + .get(&buffer_id) + .filter(|_| !ignore_previous_fetches && !invalidate_cache.should_invalidate()) + .cloned(), + cx, + ) + .unwrap_or_default(); + + let (known_version, known_chunks) = + inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default(); + if buffer_excerpts.buffer_version.changed_since(known_version) { + known_chunks.clear(); + *known_version = buffer_excerpts.buffer_version; + } - if let Some(cached_excerpt_hints) = &cached_excerpt_hints { - let cached_excerpt_hints = cached_excerpt_hints.read(); - remove_from_cache.extend( - cached_excerpt_hints - .ordered_hints + let mut hint_tasks = Vec::new(); + for (row_range, new_hints_task) in new_hint_tasks { + let inserted = known_chunks.insert(row_range.clone()); + if inserted || ignore_previous_fetches || invalidate_cache.should_invalidate() { + hint_tasks.push(cx.spawn(async move |_, _| (row_range, new_hints_task.await))); + } + } + + Some(hint_tasks) + } + + fn apply_fetched_hints( + &mut self, + buffer_id: BufferId, + query_version: Global, + invalidate_cache: InvalidationStrategy, + new_hints: Vec<(Range, anyhow::Result)>, + all_affected_buffers: Arc>>, + cx: &mut Context, + ) { + let visible_inlay_hint_ids = self + .visible_inlay_hints(cx) + .iter() + .filter(|inlay| inlay.position.buffer_id == Some(buffer_id)) + .map(|inlay| inlay.id) + .collect::>(); + let Some(inlay_hints) = &mut self.inlay_hints else { + return; + }; + + let mut hints_to_remove = Vec::new(); + let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); + + // If we've received hints from the cache, it means `invalidate_cache` had invalidated whatever possible there, + // and most probably there are no more hints with IDs from `visible_inlay_hint_ids` in the cache. + // So, if we hover such hints, no resolve will happen. + // + // Another issue is in the fact that changing one buffer may lead to other buffers' hints changing, so more cache entries may be removed. + // Hence, clear all excerpts' hints in the multi buffer: later, the invalidated ones will re-trigger the LSP query, the rest will be restored + // from the cache. + if invalidate_cache.should_invalidate() { + hints_to_remove.extend(visible_inlay_hint_ids); + } + + let excerpts = self.buffer.read(cx).excerpt_ids(); + let hints_to_insert = new_hints + .into_iter() + .filter_map(|(chunk_range, hints_result)| match hints_result { + Ok(new_hints) => Some(new_hints), + Err(e) => { + log::error!( + "Failed to query inlays for buffer row range {chunk_range:?}, {e:#}" + ); + if let Some((for_version, chunks_fetched)) = + inlay_hints.hint_chunk_fetched.get_mut(&buffer_id) + { + if for_version == &query_version { + chunks_fetched.remove(&chunk_range); + } + } + None + } + }) + .flat_map(|hints| hints.into_values()) + .flatten() + .filter_map(|(hint_id, lsp_hint)| { + if inlay_hints.allowed_hint_kinds.contains(&lsp_hint.kind) + && inlay_hints + .added_hints + .insert(hint_id, lsp_hint.kind) + .is_none() + { + let position = excerpts.iter().find_map(|excerpt_id| { + multi_buffer_snapshot.anchor_in_excerpt(*excerpt_id, lsp_hint.position) + })?; + return Some(Inlay::hint(hint_id, position, &lsp_hint)); + } + None + }) + .collect::>(); + + // We need to invalidate excerpts all buffers with the same language, do that once only, after first new data chunk is inserted. + let all_other_affected_buffers = all_affected_buffers + .lock() + .drain() + .filter(|id| buffer_id != *id) + .collect::>(); + if !all_other_affected_buffers.is_empty() { + hints_to_remove.extend( + self.visible_inlay_hints(cx) .iter() - .filter(|cached_inlay_id| { - !excerpt_hints_to_persist.contains_key(cached_inlay_id) + .filter(|inlay| { + inlay + .position + .buffer_id + .is_none_or(|buffer_id| all_other_affected_buffers.contains(&buffer_id)) }) - .copied(), + .map(|inlay| inlay.id), ); - remove_from_visible.extend(remove_from_cache.iter().cloned()); } - } - if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() { - None - } else { - Some(ExcerptHintsUpdate { - excerpt_id, - remove_from_visible, - remove_from_cache, - add_to_cache, - }) + self.splice_inlays(&hints_to_remove, hints_to_insert, cx); } } -fn contains_position( - range: &Range, - position: language::Anchor, - buffer_snapshot: &BufferSnapshot, -) -> bool { - range.start.cmp(&position, buffer_snapshot).is_le() - && range.end.cmp(&position, buffer_snapshot).is_ge() +#[derive(Debug)] +struct VisibleExcerpts { + excerpts: Vec, + ranges: Vec>, + buffer_version: Global, + buffer: Entity, } -fn apply_hint_update( - editor: &mut Editor, - new_update: ExcerptHintsUpdate, - query: ExcerptQuery, - invalidate: bool, - buffer_snapshot: BufferSnapshot, - multi_buffer_snapshot: MultiBufferSnapshot, - cx: &mut Context, -) { - let cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .entry(new_update.excerpt_id) - .or_insert_with(|| { - Arc::new(RwLock::new(CachedExcerptHints { - version: query.cache_version, - buffer_version: buffer_snapshot.version().clone(), - buffer_id: query.buffer_id, - ordered_hints: Vec::new(), - hints_by_id: HashMap::default(), - })) - }); - let mut cached_excerpt_hints = cached_excerpt_hints.write(); - match query.cache_version.cmp(&cached_excerpt_hints.version) { - cmp::Ordering::Less => return, - cmp::Ordering::Greater | cmp::Ordering::Equal => { - cached_excerpt_hints.version = query.cache_version; +fn spawn_editor_hints_refresh( + buffer_id: BufferId, + invalidate_cache: InvalidationStrategy, + ignore_previous_fetches: bool, + debounce: Option, + buffer_excerpts: VisibleExcerpts, + all_affected_buffers: Arc>>, + cx: &mut Context<'_, Editor>, +) -> Task<()> { + cx.spawn(async move |editor, cx| { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; } - } - let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); - cached_excerpt_hints - .ordered_hints - .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id)); - cached_excerpt_hints - .hints_by_id - .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id)); - let mut splice = InlaySplice::default(); - splice.to_remove.extend(new_update.remove_from_visible); - for new_hint in new_update.add_to_cache { - let insert_position = match cached_excerpt_hints - .ordered_hints - .binary_search_by(|probe| { - cached_excerpt_hints.hints_by_id[probe] - .position - .cmp(&new_hint.position, &buffer_snapshot) - }) { - Ok(i) => { - // When a hint is added to the same position where existing ones are present, - // do not deduplicate it: we split hint queries into non-overlapping ranges - // and each hint batch returned by the server should already contain unique hints. - i + cached_excerpt_hints.ordered_hints[i..].len() + 1 - } - Err(i) => i, + let query_version = buffer_excerpts.buffer_version.clone(); + let Some(hint_tasks) = editor + .update(cx, |editor, cx| { + editor.inlay_hints_for_buffer( + invalidate_cache, + ignore_previous_fetches, + buffer_excerpts, + cx, + ) + }) + .ok() + else { + return; }; - - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - && let Some(new_hint_position) = - multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position) - { - splice - .to_insert - .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); - } - let new_id = InlayId::Hint(new_inlay_id); - cached_excerpt_hints.hints_by_id.insert(new_id, new_hint); - if cached_excerpt_hints.ordered_hints.len() <= insert_position { - cached_excerpt_hints.ordered_hints.push(new_id); - } else { - cached_excerpt_hints - .ordered_hints - .insert(insert_position, new_id); - } - - cached_inlays_changed = true; - } - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - drop(cached_excerpt_hints); - - if invalidate { - let mut outdated_excerpt_caches = HashSet::default(); - for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - if excerpt_hints.buffer_id == query.buffer_id - && excerpt_id != &query.excerpt_id - && buffer_snapshot - .version() - .changed_since(&excerpt_hints.buffer_version) - { - outdated_excerpt_caches.insert(*excerpt_id); - splice - .to_remove - .extend(excerpt_hints.ordered_hints.iter().copied()); - } + let hint_tasks = hint_tasks.unwrap_or_default(); + if hint_tasks.is_empty() { + return; } - cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + let new_hints = join_all(hint_tasks).await; editor - .inlay_hint_cache - .hints - .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); - } - - let InlaySplice { - to_remove, - to_insert, - } = splice; - let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); - if cached_inlays_changed || displayed_inlays_changed { - editor.inlay_hint_cache.version += 1; - } - if displayed_inlays_changed { - editor.splice_inlays(&to_remove, to_insert, cx) - } + .update(cx, |editor, cx| { + editor.apply_fetched_hints( + buffer_id, + query_version, + invalidate_cache, + new_hints, + all_affected_buffers, + cx, + ); + }) + .ok(); + }) } #[cfg(test)] pub mod tests { - use crate::SelectionEffects; use crate::editor_tests::update_test_language_settings; + use crate::inlays::inlay_hints::InlayHintRefreshReason; use crate::scroll::ScrollAmount; - use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang}; - use futures::StreamExt; + use crate::{Editor, SelectionEffects}; + use crate::{ExcerptRange, scroll::Autoscroll}; + use collections::HashSet; + use futures::{StreamExt, future}; use gpui::{AppContext as _, Context, SemanticVersion, TestAppContext, WindowHandle}; use itertools::Itertools as _; + use language::language_settings::InlayHintKind; use language::{Capability, FakeLspAdapter}; use language::{Language, LanguageConfig, LanguageMatcher}; + use languages::rust_lang; use lsp::FakeLanguageServer; + use multi_buffer::MultiBuffer; use parking_lot::Mutex; + use pretty_assertions::assert_eq; use project::{FakeFs, Project}; use serde_json::json; use settings::{AllLanguageSettingsContent, InlayHintSettingsContent, SettingsStore}; + use std::ops::Range; + use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; - use text::Point; + use std::time::Duration; + use text::{OffsetRangeExt, Point}; + use ui::App; use util::path; - - use super::*; + use util::paths::natural_sort; #[gpui::test] async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { @@ -1348,7 +968,110 @@ pub mod tests { Ok(Some(vec![lsp::InlayHint { position: lsp::Position::new(0, i), label: lsp::InlayHintLabel::String(i.to_string()), - kind: None, + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + }) + .await; + cx.executor().run_until_parked(); + + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec!["1".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor, cx), + "Should get its first hints when opening the editor" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([13..13]) + }); + editor.handle_input("some change", window, cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor, cx), + "Should get new hints after an edit" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + }) + .unwrap(); + + fake_server + .request::(()) + .await + .into_response() + .expect("inlay refresh request failed"); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor, cx), + "Should get new hints after hint refresh/ request" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_racy_cache_updates(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + let (_, editor, fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| { + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server.set_request_handler::( + move |params, _| { + let task_lsp_request_count = Arc::clone(&lsp_request_count); + async move { + let i = task_lsp_request_count.fetch_add(1, Ordering::Release) + 1; + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: Some(lsp::InlayHintKind::TYPE), text_edits: None, tooltip: None, padding_left: None, @@ -1360,6 +1083,7 @@ pub mod tests { ); }) .await; + cx.executor().advance_clock(Duration::from_secs(1)); cx.executor().run_until_parked(); editor @@ -1367,64 +1091,41 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); }) .unwrap(); + // Emulate simultaneous events: both editing, refresh and, slightly after, scroll updates are triggered. editor .update(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([13..13]) - }); - editor.handle_input("some change", window, cx); + editor.handle_input("foo", window, cx); }) .unwrap(); - cx.executor().run_until_parked(); + cx.executor().advance_clock(Duration::from_millis(5)); editor .update(cx, |editor, _window, cx| { - let expected_hints = vec!["2".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get new hints after an edit" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" + editor.refresh_inlay_hints( + InlayHintRefreshReason::RefreshRequested(fake_server.server.server_id()), + cx, ); }) .unwrap(); - - fake_server - .request::(()) - .await - .into_response() - .expect("inlay refresh request failed"); + cx.executor().advance_clock(Duration::from_millis(5)); + editor + .update(cx, |editor, _window, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_secs(1)); cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { - let expected_hints = vec!["3".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should get new hints after hint refresh/ request" - ); + let expected_hints = vec!["2".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor, cx), "Despite multiple simultaneous refreshes, only one inlay hint query should be issued"); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, - "Cache should use editor settings to get the allowed hint kinds" - ); }) .unwrap(); } @@ -1479,7 +1180,7 @@ pub mod tests { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1508,7 +1209,7 @@ pub mod tests { let expected_hints = vec!["0".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should not update hints while the work task is running" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1528,7 +1229,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "New hints should be queried after the work task is done" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1663,7 +1364,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1688,7 +1389,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Markdown editor should have a separate version, repeating Rust editor rules" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1706,15 +1407,10 @@ pub mod tests { cx.executor().run_until_parked(); rs_editor .update(cx, |editor, _window, cx| { - // TODO: Here, we do not get "2", because inserting another language server will trigger `RefreshInlayHints` event from the `LspStore` - // A project is listened in every editor, so each of them will react to this event. - // - // We do not have language server IDs for remote projects, so cannot easily say on the editor level, - // whether we should ignore a particular `RefreshInlayHints` event. - let expected_hints = vec!["3".to_string()]; + let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Rust inlay cache should change after the edit" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1725,7 +1421,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Markdown editor should not be affected by Rust editor changes" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1746,7 +1442,7 @@ pub mod tests { let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Rust editor should not be affected by Markdown editor changes" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1754,10 +1450,10 @@ pub mod tests { .unwrap(); rs_editor .update(cx, |editor, _window, cx| { - let expected_hints = vec!["3".to_string()]; + let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Markdown editor should also change independently" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -1852,16 +1548,16 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its first hints when opening the editor" ); assert_eq!( vec!["type hint".to_string(), "other hint".to_string()], visible_hint_labels(editor, cx) ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds" ); }) @@ -1886,7 +1582,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Cached hints should not change due to allowed hint kinds settings update" ); assert_eq!( @@ -1961,7 +1657,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" ); assert_eq!( @@ -1969,9 +1665,9 @@ pub mod tests { visible_hint_labels(editor, cx), "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + new_allowed_hint_kinds, "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" ); }).unwrap(); @@ -2003,17 +1699,23 @@ pub mod tests { 2, "Should not load new hints when hints got disabled" ); - assert!( - cached_hint_labels(editor).is_empty(), - "Should clear the cache when hints got disabled" + assert_eq!( + vec![ + "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), + ], + cached_hint_labels(editor, cx), + "Should not clear the cache when hints got disabled" ); - assert!( - visible_hint_labels(editor, cx).is_empty(), + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), "Should clear visible hints when hints got disabled" ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + another_allowed_hint_kinds, "Should update its allowed hint kinds even when hints got disabled" ); }) @@ -2032,8 +1734,15 @@ pub mod tests { 2, "Should not load new hints when they got disabled" ); - assert!(cached_hint_labels(editor).is_empty()); - assert!(visible_hint_labels(editor, cx).is_empty()); + assert_eq!( + vec![ + "type hint".to_string(), + "parameter hint".to_string(), + "other hint".to_string(), + ], + cached_hint_labels(editor, cx) + ); + assert_eq!(Vec::::new(), visible_hint_labels(editor, cx)); }) .unwrap(); @@ -2060,8 +1769,8 @@ pub mod tests { .update(cx, |editor, _, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), - 3, - "Should query for new hints when they got re-enabled" + 2, + "Should not query for new hints when they got re-enabled, as the file version did not change" ); assert_eq!( vec![ @@ -2069,7 +1778,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get its cached hints fully repopulated after the hints got re-enabled" ); assert_eq!( @@ -2077,9 +1786,9 @@ pub mod tests { visible_hint_labels(editor, cx), "Should get its visible hints repopulated and filtered after the h" ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, + allowed_hint_kinds_for_editor(editor), + final_allowed_hint_kinds, "Cache should update editor settings when hints got re-enabled" ); }) @@ -2095,7 +1804,7 @@ pub mod tests { .update(cx, |editor, _, cx| { assert_eq!( lsp_request_count.load(Ordering::Relaxed), - 4, + 3, "Should query for new hints again" ); assert_eq!( @@ -2104,7 +1813,7 @@ pub mod tests { "parameter hint".to_string(), "other hint".to_string(), ], - cached_hint_labels(editor), + cached_hint_labels(editor, cx), ); assert_eq!( vec!["parameter hint".to_string()], @@ -2197,7 +1906,7 @@ pub mod tests { let expected_hints = vec!["2".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -2243,7 +1952,7 @@ pub mod tests { let expected_hints = vec!["3".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -2289,7 +1998,7 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, initializer: Some(Box::new({ let lsp_request_ranges = lsp_request_ranges.clone(); @@ -2327,7 +2036,7 @@ pub mod tests { ); } })), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -2339,55 +2048,20 @@ pub mod tests { .unwrap(); let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx)); - cx.executor().run_until_parked(); - let _fake_server = fake_servers.next().await.unwrap(); + cx.executor().run_until_parked(); - // in large buffers, requests are made for more than visible range of a buffer. - // invisible parts are queried later, to avoid excessive requests on quick typing. - // wait the timeout needed to get all requests. - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); - cx.executor().run_until_parked(); - let initial_visible_range = editor_visible_range(&editor, cx); - let lsp_initial_visible_range = lsp::Range::new( - lsp::Position::new( - initial_visible_range.start.row, - initial_visible_range.start.column, - ), - lsp::Position::new( - initial_visible_range.end.row, - initial_visible_range.end.column, - ), + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 1, + "Should query 1 range initially, but got: {ranges:?}" ); - let expected_initial_query_range_end = - lsp::Position::new(initial_visible_range.end.row * 2, 2); - let mut expected_invisible_query_start = lsp_initial_visible_range.end; - expected_invisible_query_start.character += 1; - editor.update(cx, |editor, _window, cx| { - let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 2, - "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); - let visible_query_range = &ranges[0]; - assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); - assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); - let invisible_query_range = &ranges[1]; - - assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); - assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); - - let requests_count = lsp_request_count.load(Ordering::Acquire); - assert_eq!(requests_count, 2, "Visible + invisible request"); - let expected_hints = vec!["47".to_string(), "94".to_string()]; - assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should have hints from both LSP requests made for a big file" - ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); - }).unwrap(); editor .update(cx, |editor, window, cx| { @@ -2402,9 +2076,7 @@ pub mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), window, cx); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); let visible_range_after_scrolls = editor_visible_range(&editor, cx); let visible_line_count = editor @@ -2427,37 +2099,25 @@ pub mod tests { let first_scroll = &ranges[0]; let second_scroll = &ranges[1]; assert_eq!( - first_scroll.end, second_scroll.start, + first_scroll.end.line, second_scroll.start.line, "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" ); - assert_eq!( - first_scroll.start, expected_initial_query_range_end, - "First scroll should start the query right after the end of the original scroll", - ); - assert_eq!( - second_scroll.end, - lsp::Position::new( - visible_range_after_scrolls.end.row - + visible_line_count.ceil() as u32, - 1, - ), - "Second scroll should query one more screen down after the end of the visible range" - ); let lsp_requests = lsp_request_count.load(Ordering::Acquire); - assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); - let expected_hints = vec![ - "47".to_string(), - "94".to_string(), - "139".to_string(), - "184".to_string(), - ]; assert_eq!( - expected_hints, - cached_hint_labels(editor), - "Should have hints from the new LSP response after the edit" + lsp_requests, 3, + "Should query hints initially, and after each scroll (2 times)" + ); + assert_eq!( + vec!["50".to_string(), "100".to_string(), "150".to_string()], + cached_hint_labels(editor, cx), + "Chunks of 50 line width should have been queried each time" + ); + assert_eq!( + vec!["50".to_string(), "100".to_string(), "150".to_string()], + visible_hint_labels(editor, cx), + "Editor should show only hints that it's scrolled to" ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let mut selection_in_cached_range = visible_range_after_scrolls.end; selection_in_cached_range.row -= visible_line_count.ceil() as u32; @@ -2475,9 +2135,6 @@ pub mod tests { ); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); cx.executor().run_until_parked(); editor.update(cx, |_, _, _| { let ranges = lsp_request_ranges @@ -2486,7 +2143,7 @@ pub mod tests { .sorted_by_key(|r| r.start) .collect::>(); assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "No new requests should be made when selecting within cached chunks"); }).unwrap(); editor @@ -2494,38 +2151,25 @@ pub mod tests { editor.handle_input("++++more text++++", window, cx); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); cx.executor().run_until_parked(); editor.update(cx, |editor, _window, cx| { let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); ranges.sort_by_key(|r| r.start); - assert_eq!(ranges.len(), 3, - "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); - let above_query_range = &ranges[0]; - let visible_query_range = &ranges[1]; - let below_query_range = &ranges[2]; - assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, - "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); - assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, - "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); - assert!(above_query_range.start.line < selection_in_cached_range.row, + assert_eq!(ranges.len(), 2, + "On edit, should scroll to selection and query a range around it: that range should split into 2 50 rows wide chunks. Instead, got query ranges {ranges:?}"); + let first_chunk = &ranges[0]; + let second_chunk = &ranges[1]; + assert!(first_chunk.end.line == second_chunk.start.line, + "First chunk {first_chunk:?} should be before second chunk {second_chunk:?}"); + assert!(first_chunk.start.line < selection_in_cached_range.row, "Hints should be queried with the selected range after the query range start"); - assert!(below_query_range.end.line > selection_in_cached_range.row, - "Hints should be queried with the selected range before the query range end"); - assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, - "Hints query range should contain one more screen before"); - assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, - "Hints query range should contain one more screen after"); let lsp_requests = lsp_request_count.load(Ordering::Acquire); - assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); - let expected_hints = vec!["67".to_string(), "115".to_string(), "163".to_string()]; - assert_eq!(expected_hints, cached_hint_labels(editor), - "Should have hints from the new LSP response after the edit"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(lsp_requests, 5, "Two chunks should be re-queried"); + assert_eq!(vec!["100".to_string(), "150".to_string()], cached_hint_labels(editor, cx), + "Should have (less) hints from the new LSP response after the edit"); + assert_eq!(vec!["100".to_string(), "150".to_string()], visible_hint_labels(editor, cx), "Should show only visible hints (in the center) from the new cached set"); }).unwrap(); } @@ -2534,7 +2178,7 @@ pub mod tests { cx: &mut gpui::TestAppContext, ) -> Range { let ranges = editor - .update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx)) + .update(cx, |editor, _window, cx| editor.visible_excerpts(cx)) .unwrap(); assert_eq!( ranges.len(), @@ -2543,14 +2187,7 @@ pub mod tests { ); let (_, (excerpt_buffer, _, excerpt_visible_range)) = ranges.into_iter().next().unwrap(); excerpt_buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let start = buffer - .anchor_before(excerpt_visible_range.start) - .to_point(&snapshot); - let end = buffer - .anchor_after(excerpt_visible_range.end) - .to_point(&snapshot); - start..end + excerpt_visible_range.to_point(&buffer.snapshot()) }) } @@ -2590,9 +2227,9 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -2724,7 +2361,7 @@ pub mod tests { ]; assert_eq!( expected_hints, - sorted_cached_hint_labels(editor), + sorted_cached_hint_labels(editor, cx), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -2749,7 +2386,7 @@ pub mod tests { SelectionEffects::scroll(Autoscroll::Next), window, cx, - |s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]), + |s| s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]), ); }) .unwrap(); @@ -2764,7 +2401,7 @@ pub mod tests { "main hint #4".to_string(), "main hint #5".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), + assert_eq!(expected_hints, sorted_cached_hint_labels(editor, cx), "New hints are not shown right after scrolling, we need to wait for the buffer to be registered"); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) @@ -2783,10 +2420,17 @@ pub mod tests { "other hint #0".to_string(), "other hint #1".to_string(), "other hint #2".to_string(), + "other hint #3".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), "After scrolling to the new buffer and waiting for it to be registered, new hints should appear"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Editor should show only visible hints", + ); }) .unwrap(); @@ -2800,9 +2444,7 @@ pub mod tests { ); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { @@ -2820,9 +2462,16 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), - "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Editor shows only hints for excerpts that were visible when scrolling" + ); }) .unwrap(); @@ -2836,9 +2485,6 @@ pub mod tests { ); }) .unwrap(); - cx.executor().advance_clock(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, - )); cx.executor().run_until_parked(); editor .update(cx, |editor, _window, cx| { @@ -2856,44 +2502,301 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_hints, sorted_cached_hint_labels(editor), - "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + ); }) .unwrap(); - editor_edited.store(true, Ordering::Release); + // We prepare to change the scrolling on edit, but do not scroll yet editor .update(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(57, 0)..Point::new(57, 0)]) }); + }) + .unwrap(); + cx.executor().run_until_parked(); + // Edit triggers the scrolling too + editor_edited.store(true, Ordering::Release); + editor + .update(cx, |editor, window, cx| { editor.handle_input("++++more text++++", window, cx); }) .unwrap(); cx.executor().run_until_parked(); - // Wait again to trigger the inlay hints fetch on scroll - cx.executor().advance_clock(Duration::from_millis(100)); - cx.executor().run_until_parked(); + // Wait again to trigger the inlay hints fetch on scroll + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _window, cx| { + let expected_hints = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), + "other hint(edited) #2".to_string(), + "other hint(edited) #3".to_string(), + ]; + assert_eq!( + expected_hints, + sorted_cached_hint_labels(editor, cx), + "After multibuffer edit, editor gets scrolled back to the last selection; \ + all hints should be invalidated and required for all of its visible excerpts" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "All excerpts should get their hints" + ); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_editing_in_multi_buffer(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/a"), + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..200).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "lib.rs": r#"let a = 1; +let b = 2; +let c = 3;"# + }), + ) + .await; + + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let language = rust_lang(); + language_registry.add(language); + + let closure_ranges_fetched = lsp_request_ranges.clone(); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + initializer: Some(Box::new(move |fake_server| { + let closure_ranges_fetched = closure_ranges_fetched.clone(); + fake_server.set_request_handler::( + move |params, _| { + let closure_ranges_fetched = closure_ranges_fetched.clone(); + async move { + let prefix = if params.text_document.uri + == lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap() + { + closure_ranges_fetched + .lock() + .push(("main.rs", params.range)); + "main.rs" + } else if params.text_document.uri + == lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap() + { + closure_ranges_fetched.lock().push(("lib.rs", params.range)); + "lib.rs" + } else { + panic!("Unexpected file path {:?}", params.text_document.uri); + }; + Ok(Some( + (params.range.start.line..params.range.end.line) + .map(|row| lsp::InlayHint { + position: lsp::Position::new(row, 0), + label: lsp::InlayHintLabel::String(format!( + "{prefix} Inlay hint #{row}" + )), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + .collect(), + )) + } + }, + ); + })), + ..FakeLspAdapter::default() + }, + ); + + let (buffer_1, _handle_1) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx) + }) + .await + .unwrap(); + let (buffer_2, _handle_2) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/a/lib.rs"), cx) + }) + .await + .unwrap(); + let multi_buffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + // Have first excerpt to spawn over 2 chunks (50 lines each). + ExcerptRange::new(Point::new(49, 0)..Point::new(53, 0)), + // Have 2nd excerpt to be in the 2nd chunk only. + ExcerptRange::new(Point::new(70, 0)..Point::new(73, 0)), + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange::new(Point::new(0, 0)..Point::new(4, 0))], + cx, + ); + multibuffer + }); + + let editor = cx.add_window(|window, cx| { + let mut editor = + Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx); + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_ranges([0..0]) + }); + editor + }); + + let _fake_server = fake_servers.next().await.unwrap(); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + + assert_eq!( + vec![ + ( + "lib.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 11)) + ), + ], + lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start)) + .collect::>(), + "For large buffers, should query chunks that cover both visible excerpt" + ); + editor + .update(cx, |editor, _window, cx| { + assert_eq!( + (0..2) + .map(|i| format!("lib.rs Inlay hint #{i}")) + .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}"))) + .collect::>(), + sorted_cached_hint_labels(editor, cx), + "Both chunks should provide their inlay hints" + ); + assert_eq!( + vec![ + "main.rs Inlay hint #49".to_owned(), + "main.rs Inlay hint #50".to_owned(), + "main.rs Inlay hint #51".to_owned(), + "main.rs Inlay hint #52".to_owned(), + "main.rs Inlay hint #53".to_owned(), + "main.rs Inlay hint #70".to_owned(), + "main.rs Inlay hint #71".to_owned(), + "main.rs Inlay hint #72".to_owned(), + "main.rs Inlay hint #73".to_owned(), + "lib.rs Inlay hint #0".to_owned(), + "lib.rs Inlay hint #1".to_owned(), + ], + visible_hint_labels(editor, cx), + "Only hints from visible excerpt should be added into the editor" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, window, cx| { + editor.handle_input("a", window, cx); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_millis(1000)); + cx.executor().run_until_parked(); + assert_eq!( + vec![ + ( + "lib.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(2, 10)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(50, 0)) + ), + ( + "main.rs", + lsp::Range::new(lsp::Position::new(50, 0), lsp::Position::new(100, 11)) + ), + ], + lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|(prefix, r)| (prefix.to_owned(), r.start)) + .collect::>(), + "Same chunks should be re-queried on edit" + ); editor .update(cx, |editor, _window, cx| { - let expected_hints = vec![ - "main hint(edited) #0".to_string(), - "main hint(edited) #1".to_string(), - "main hint(edited) #2".to_string(), - "main hint(edited) #3".to_string(), - "main hint(edited) #4".to_string(), - "main hint(edited) #5".to_string(), - "other hint(edited) #0".to_string(), - "other hint(edited) #1".to_string(), - ]; assert_eq!( - expected_hints, - sorted_cached_hint_labels(editor), - "After multibuffer edit, editor gets scrolled back to the last selection; \ - all hints should be invalidated and required for all of its visible excerpts" + (0..2) + .map(|i| format!("lib.rs Inlay hint #{i}")) + .chain((0..100).map(|i| format!("main.rs Inlay hint #{i}"))) + .collect::>(), + sorted_cached_hint_labels(editor, cx), + "Same hints should be re-inserted after the edit" + ); + assert_eq!( + vec![ + "main.rs Inlay hint #49".to_owned(), + "main.rs Inlay hint #50".to_owned(), + "main.rs Inlay hint #51".to_owned(), + "main.rs Inlay hint #52".to_owned(), + "main.rs Inlay hint #53".to_owned(), + "main.rs Inlay hint #70".to_owned(), + "main.rs Inlay hint #71".to_owned(), + "main.rs Inlay hint #72".to_owned(), + "main.rs Inlay hint #73".to_owned(), + "lib.rs Inlay hint #0".to_owned(), + "lib.rs Inlay hint #1".to_owned(), + ], + visible_hint_labels(editor, cx), + "Same hints should be re-inserted into the editor after the edit" ); - assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) .unwrap(); } @@ -2933,9 +2836,9 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -3040,18 +2943,29 @@ pub mod tests { }) .next() .await; + cx.executor().advance_clock(Duration::from_millis(100)); cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { assert_eq!( - vec!["main hint #0".to_string(), "other hint #0".to_string()], - sorted_cached_hint_labels(editor), - "Cache should update for both excerpts despite hints display was disabled" + vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + ], + sorted_cached_hint_labels(editor, cx), + "Cache should update for both excerpts despite hints display was disabled; after selecting 2nd buffer, it's now registered with the langserever and should get its hints" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "All hints are disabled and should not be shown despite being present in the cache" ); - assert!( - visible_hint_labels(editor, cx).is_empty(), - "All hints are disabled and should not be shown despite being present in the cache" - ); }) .unwrap(); @@ -3066,9 +2980,14 @@ pub mod tests { editor .update(cx, |editor, _, cx| { assert_eq!( - vec!["main hint #0".to_string()], - cached_hint_labels(editor), - "For the removed excerpt, should clean corresponding cached hints" + vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + ], + cached_hint_labels(editor, cx), + "For the removed excerpt, should clean corresponding cached hints as its buffer was dropped" ); assert!( visible_hint_labels(editor, cx).is_empty(), @@ -3093,16 +3012,22 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - let expected_hints = vec!["main hint #0".to_string()]; assert_eq!( - expected_hints, - cached_hint_labels(editor), + vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + ], + cached_hint_labels(editor, cx), "Hint display settings change should not change the cache" ); assert_eq!( - expected_hints, + vec![ + "main hint #0".to_string(), + ], visible_hint_labels(editor, cx), - "Settings change should make cached hints visible" + "Settings change should make cached hints visible, but only the visible ones, from the remaining excerpt" ); }) .unwrap(); @@ -3143,7 +3068,7 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, initializer: Some(Box::new(move |fake_server| { let lsp_request_count = Arc::new(AtomicU32::new(0)); @@ -3170,7 +3095,7 @@ pub mod tests { }, ); })), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -3195,7 +3120,7 @@ pub mod tests { editor .update(cx, |editor, _, cx| { let expected_hints = vec!["1".to_string()]; - assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, cached_hint_labels(editor, cx)); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) .unwrap(); @@ -3228,7 +3153,7 @@ pub mod tests { lsp::Uri::from_file_path(file_with_hints).unwrap(), ); - let i = lsp_request_count.fetch_add(1, Ordering::SeqCst) + 1; + let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1; Ok(Some(vec![lsp::InlayHint { position: lsp::Position::new(0, i), label: lsp::InlayHintLabel::String(i.to_string()), @@ -3257,7 +3182,7 @@ pub mod tests { let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Should display inlays after toggle despite them disabled in settings" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -3272,11 +3197,16 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - assert!( - cached_hint_labels(editor).is_empty(), + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Cache does not change because of toggles in the editor" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), "Should clear hints after 2nd toggle" ); - assert!(visible_hint_labels(editor, cx).is_empty()); }) .unwrap(); @@ -3296,11 +3226,11 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - let expected_hints = vec!["2".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), - "Should query LSP hints for the 2nd time after enabling hints in settings" + cached_hint_labels(editor, cx), + "Should not query LSP hints after enabling hints in settings, as file version is the same" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }) @@ -3314,11 +3244,16 @@ pub mod tests { cx.executor().run_until_parked(); editor .update(cx, |editor, _, cx| { - assert!( - cached_hint_labels(editor).is_empty(), + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Cache does not change because of toggles in the editor" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), "Should clear hints after enabling in settings and a 3rd toggle" ); - assert!(visible_hint_labels(editor, cx).is_empty()); }) .unwrap(); @@ -3329,16 +3264,242 @@ pub mod tests { .unwrap(); cx.executor().run_until_parked(); editor.update(cx, |editor, _, cx| { - let expected_hints = vec!["3".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( expected_hints, - cached_hint_labels(editor), - "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on" + cached_hint_labels(editor,cx), + "Should not query LSP hints after enabling hints in settings and toggling them back on" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); }).unwrap(); } + #[gpui::test] + async fn test_modifiers_change(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettingsContent { + show_value_hints: Some(true), + enabled: Some(true), + edit_debounce_ms: Some(0), + scroll_debounce_ms: Some(0), + show_type_hints: Some(true), + show_parameter_hints: Some(true), + show_other_hints: Some(true), + show_background: Some(false), + toggle_on_modifiers_press: None, + }) + }); + + let (_, editor, _fake_server) = prepare_test_objects(cx, |fake_server, file_with_hints| { + let lsp_request_count = Arc::new(AtomicU32::new(0)); + fake_server.set_request_handler::( + move |params, _| { + let lsp_request_count = lsp_request_count.clone(); + async move { + assert_eq!( + params.text_document.uri, + lsp::Uri::from_file_path(file_with_hints).unwrap(), + ); + + let i = lsp_request_count.fetch_add(1, Ordering::AcqRel) + 1; + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, i), + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }, + ); + }) + .await; + + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Should display inlays after toggle despite them disabled in settings" + ); + assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx)); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Nothing happens with the cache on modifiers change" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "On modifiers change and hints toggled on, should hide editor inlays" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "When modifiers change is off, no extra requests are sent" + ); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "When modifiers change is off, hints are back into the editor" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind (2)" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, window, cx| { + editor.toggle_inlay_hints(&crate::ToggleInlayHints, window, cx) + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Nothing happens with the cache on modifiers change" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "When toggled off, should hide editor inlays" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "Nothing happens with the cache on modifiers change" + ); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "On modifiers change & hints toggled off, should show editor inlays" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(true), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + vec!["1".to_string()], + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind" + ); + }) + .unwrap(); + + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!( + vec!["1".to_string()], + cached_hint_labels(editor, cx), + "When modifiers change is off, no extra requests are sent" + ); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "When modifiers change is off, editor hints are back into their toggled off state" + ); + }) + .unwrap(); + editor + .update(cx, |editor, _, cx| { + editor.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); + }) + .unwrap(); + cx.executor().run_until_parked(); + editor + .update(cx, |editor, _, cx| { + assert_eq!(vec!["1".to_string()], cached_hint_labels(editor, cx)); + assert_eq!( + Vec::::new(), + visible_hint_labels(editor, cx), + "Nothing changes on consequent modifiers change of the same kind (3)" + ); + }) + .unwrap(); + } + #[gpui::test] async fn test_inlays_at_the_same_place(cx: &mut gpui::TestAppContext) { init_test(cx, |settings| { @@ -3485,7 +3646,7 @@ pub mod tests { ]; assert_eq!( expected_hints, - cached_hint_labels(editor), + cached_hint_labels(editor, cx), "Editor inlay hints should repeat server's order when placed at the same spot" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); @@ -3533,10 +3694,10 @@ pub mod tests { FakeLspAdapter { capabilities: lsp::ServerCapabilities { inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() + ..lsp::ServerCapabilities::default() }, initializer: Some(Box::new(move |server| initialize(server, file_path))), - ..Default::default() + ..FakeLspAdapter::default() }, ); @@ -3551,7 +3712,7 @@ pub mod tests { editor .update(cx, |editor, _, cx| { - assert!(cached_hint_labels(editor).is_empty()); + assert!(cached_hint_labels(editor, cx).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty()); }) .unwrap(); @@ -3563,36 +3724,51 @@ pub mod tests { // Inlay hints in the cache are stored per excerpt as a key, and those keys are guaranteed to be ordered same as in the multi buffer. // Ensure a stable order for testing. - fn sorted_cached_hint_labels(editor: &Editor) -> Vec { - let mut labels = cached_hint_labels(editor); - labels.sort(); + fn sorted_cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec { + let mut labels = cached_hint_labels(editor, cx); + labels.sort_by(|a, b| natural_sort(a, b)); labels } - pub fn cached_hint_labels(editor: &Editor) -> Vec { - let mut labels = Vec::new(); - for excerpt_hints in editor.inlay_hint_cache().hints.values() { - let excerpt_hints = excerpt_hints.read(); - for id in &excerpt_hints.ordered_hints { - let hint = &excerpt_hints.hints_by_id[id]; - let mut label = hint.text().to_string(); - if hint.padding_left { - label.insert(0, ' '); - } - if hint.padding_right { - label.push_str(" "); - } - labels.push(label); - } + pub fn cached_hint_labels(editor: &Editor, cx: &mut App) -> Vec { + let lsp_store = editor.project().unwrap().read(cx).lsp_store(); + + let mut all_cached_labels = Vec::new(); + let mut all_fetched_hints = Vec::new(); + for buffer in editor.buffer.read(cx).all_buffers() { + lsp_store.update(cx, |lsp_store, cx| { + let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints(); + all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| { + let mut label = hint.text().to_string(); + if hint.padding_left { + label.insert(0, ' '); + } + if hint.padding_right { + label.push_str(" "); + } + label + })); + all_fetched_hints.extend(hints.all_fetched_hints()); + }); } - labels + all_cached_labels } pub fn visible_hint_labels(editor: &Editor, cx: &Context) -> Vec { editor .visible_inlay_hints(cx) + .into_iter() .map(|hint| hint.text().to_string()) .collect() } + + fn allowed_hint_kinds_for_editor(editor: &Editor) -> HashSet> { + editor + .inlay_hints + .as_ref() + .unwrap() + .allowed_hint_kinds + .clone() + } } diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index e99cab2aa938614be5478bdf17ef78b1f626a6f2..050363f219ee5579a73cf168cce82778df8810ab 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -6,15 +6,15 @@ use gpui::{Hsla, Rgba, Task}; use itertools::Itertools; use language::point_from_lsp; use multi_buffer::Anchor; -use project::DocumentColor; +use project::{DocumentColor, InlayId}; use settings::Settings as _; use text::{Bias, BufferId, OffsetRangeExt as _}; use ui::{App, Context, Window}; use util::post_inc; use crate::{ - DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, InlayId, - InlaySplice, RangeToAnchorExt, display_map::Inlay, editor_settings::DocumentColorsRenderMode, + DisplayPoint, Editor, EditorSettings, EditorSnapshot, FETCH_COLORS_DEBOUNCE_TIMEOUT, + InlaySplice, RangeToAnchorExt, editor_settings::DocumentColorsRenderMode, inlays::Inlay, }; #[derive(Debug)] @@ -164,7 +164,7 @@ impl Editor { } let visible_buffers = self - .visible_excerpts(None, cx) + .visible_excerpts(cx) .into_values() .map(|(buffer, ..)| buffer) .filter(|editor_buffer| { @@ -400,8 +400,7 @@ impl Editor { } if colors.render_mode == DocumentColorsRenderMode::Inlay - && (!colors_splice.to_insert.is_empty() - || !colors_splice.to_remove.is_empty()) + && !colors_splice.is_empty() { editor.splice_inlays(&colors_splice.to_remove, colors_splice.to_insert, cx); updated = true; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 486a14e3741989c1632e361e6ae6324d697cf2c7..418fa4fcb442b1de133972457497c0e592e77d15 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -872,7 +872,7 @@ mod tests { use super::*; use crate::{ Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, MultiBuffer, - display_map::Inlay, + inlays::Inlay, test::{editor_test_context::EditorTestContext, marked_display_snapshot}, }; use gpui::{AppContext as _, font, px}; diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 2d4710a8d44a023f0c3206ad0c327a34c36fdac4..d32c0412e3707de2fb20be96a4472ec82d59726a 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -1,14 +1,14 @@ use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider}; use buffer_diff::BufferDiff; -use collections::HashSet; +use collections::{HashMap, HashSet}; use futures::{channel::mpsc, future::join_all}; use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task}; -use language::{Buffer, BufferEvent, Capability}; +use language::{Buffer, BufferEvent, BufferRow, Capability}; use multi_buffer::{ExcerptRange, MultiBuffer}; -use project::Project; +use project::{InvalidationStrategy, Project, lsp_store::CacheInlayHints}; use smol::stream::StreamExt; use std::{any::TypeId, ops::Range, rc::Rc, time::Duration}; -use text::ToOffset; +use text::{BufferId, ToOffset}; use ui::{ButtonLike, KeyBinding, prelude::*}; use workspace::{ Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -436,14 +436,34 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { self.0.hover(&buffer, position, cx) } + fn applicable_inlay_chunks( + &self, + buffer_id: BufferId, + ranges: &[Range], + cx: &App, + ) -> Vec> { + self.0.applicable_inlay_chunks(buffer_id, ranges, cx) + } + + fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App) { + self.0.invalidate_inlay_hints(for_buffers, cx); + } + fn inlay_hints( &self, + invalidate: InvalidationStrategy, buffer: Entity, - range: Range, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut App, - ) -> Option>>> { - let buffer = self.to_base(&buffer, &[range.start, range.end], cx)?; - self.0.inlay_hints(buffer, range, cx) + ) -> Option, Task>>> { + let positions = ranges + .iter() + .flat_map(|range| [range.start, range.end]) + .collect::>(); + let buffer = self.to_base(&buffer, &positions, cx)?; + self.0 + .inlay_hints(invalidate, buffer, ranges, known_chunks, cx) } fn inline_values( @@ -455,17 +475,6 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { None } - fn resolve_inlay_hint( - &self, - hint: project::InlayHint, - buffer: Entity, - server_id: lsp::LanguageServerId, - cx: &mut App, - ) -> Option>> { - let buffer = self.to_base(&buffer, &[], cx)?; - self.0.resolve_inlay_hint(hint, buffer, server_id, cx) - } - fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { if let Some(buffer) = self.to_base(buffer, &[], cx) { self.0.supports_inlay_hints(&buffer, cx) diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 72060a11f07d297f578f933b0f6fd809dc915bb5..5a850bf4cff924b85ea5599c3d75c2b602b4dd1d 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -6,6 +6,7 @@ use std::{ }; use anyhow::Result; +use language::rust_lang; use serde_json::json; use crate::{Editor, ToPoint}; @@ -32,55 +33,6 @@ pub struct EditorLspTestContext { pub buffer_lsp_url: lsp::Uri, } -pub(crate) fn rust_lang() -> Arc { - let language = Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], - ..Default::default() - }, - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_queries(LanguageQueries { - indents: Some(Cow::from(indoc! {r#" - [ - ((where_clause) _ @end) - (field_expression) - (call_expression) - (assignment_expression) - (let_declaration) - (let_chain) - (await_expression) - ] @indent - - (_ "[" "]" @end) @indent - (_ "<" ">" @end) @indent - (_ "{" "}" @end) @indent - (_ "(" ")" @end) @indent"#})), - brackets: Some(Cow::from(indoc! {r#" - ("(" @open ")" @close) - ("[" @open "]" @close) - ("{" @open "}" @close) - ("<" @open ">" @close) - ("\"" @open "\"" @close) - (closure_parameters "|" @open "|" @close)"#})), - text_objects: Some(Cow::from(indoc! {r#" - (function_item - body: (_ - "{" - (_)* @function.inside - "}" )) @function.around - "#})), - ..Default::default() - }) - .expect("Could not parse queries"); - Arc::new(language) -} - #[cfg(test)] pub(crate) fn git_commit_lang() -> Arc { Arc::new(Language::new( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 2d1a274381224978246db618301606caf44a60cb..e3fb6733dd5176906f0a9a9d208305d67470ba15 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -2600,6 +2600,65 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { start..end } +#[doc(hidden)] +#[cfg(any(test, feature = "test-support"))] +pub fn rust_lang() -> Arc { + use std::borrow::Cow; + + let language = Language::new( + LanguageConfig { + name: "Rust".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()], + ..Default::default() + }, + Some(tree_sitter_rust::LANGUAGE.into()), + ) + .with_queries(LanguageQueries { + indents: Some(Cow::from( + r#" +[ + ((where_clause) _ @end) + (field_expression) + (call_expression) + (assignment_expression) + (let_declaration) + (let_chain) + (await_expression) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent"#, + )), + brackets: Some(Cow::from( + r#" +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) +(closure_parameters "|" @open "|" @close)"#, + )), + text_objects: Some(Cow::from( + r#" +(function_item + body: (_ + "{" + (_)* @function.inside + "}" )) @function.around + "#, + )), + ..LanguageQueries::default() + }) + .expect("Could not parse queries"); + Arc::new(language) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7d5bf2fcd514d081260e4dbe3d9c3521d2629e17..55742c284ddcc7dfa6669ea3924fc60a77b2e1ab 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -234,7 +234,7 @@ pub(crate) struct OnTypeFormatting { pub push_to_history: bool, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub(crate) struct InlayHints { pub range: Range, } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e8eaa493de9dc14493c95307b42f04711b4eaca0..dc082453fd74d9d6e046e99e1e75de0f7e4c544e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -16,16 +16,20 @@ pub mod lsp_ext_command; pub mod rust_analyzer_ext; pub mod vue_language_server_ext; +mod inlay_hint_cache; + +use self::inlay_hint_cache::BufferInlayHints; use crate::{ CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse, - CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, LocationLink, LspAction, - LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, + CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, InlayId, LocationLink, + LspAction, LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_store::{ self, + inlay_hint_cache::BufferChunk, log_store::{GlobalLogStore, LanguageServerKind}, }, manifest_tree::{ @@ -57,7 +61,7 @@ use gpui::{ use http_client::HttpClient; use itertools::Itertools as _; use language::{ - Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, + Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate, ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, @@ -85,7 +89,7 @@ use parking_lot::Mutex; use postage::{mpsc, sink::Sink, stream::Stream, watch}; use rand::prelude::*; use rpc::{ - AnyProtoClient, + AnyProtoClient, ErrorCode, ErrorExt as _, proto::{LspRequestId, LspRequestMessage as _}, }; use serde::Serialize; @@ -106,11 +110,14 @@ use std::{ path::{self, Path, PathBuf}, pin::pin, rc::Rc, - sync::Arc, + sync::{ + Arc, + atomic::{self, AtomicUsize}, + }, time::{Duration, Instant}, }; use sum_tree::Dimensions; -use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _}; +use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, Point, ToPoint as _}; use util::{ ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into, @@ -121,6 +128,7 @@ use util::{ pub use fs::*; pub use language::Location; +pub use lsp_store::inlay_hint_cache::{CacheInlayHints, InvalidationStrategy}; #[cfg(any(test, feature = "test-support"))] pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use worktree::{ @@ -565,8 +573,7 @@ impl LocalLspStore { } fn setup_lsp_messages( - this: WeakEntity, - + lsp_store: WeakEntity, language_server: &LanguageServer, delegate: Arc, adapter: Arc, @@ -576,7 +583,7 @@ impl LocalLspStore { language_server .on_notification::({ let adapter = adapter.clone(); - let this = this.clone(); + let this = lsp_store.clone(); move |mut params, cx| { let adapter = adapter.clone(); if let Some(this) = this.upgrade() { @@ -620,8 +627,7 @@ impl LocalLspStore { .on_request::({ let adapter = adapter.adapter.clone(); let delegate = delegate.clone(); - let this = this.clone(); - + let this = lsp_store.clone(); move |params, cx| { let adapter = adapter.clone(); let delegate = delegate.clone(); @@ -666,7 +672,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |_, cx| { let this = this.clone(); let cx = cx.clone(); @@ -694,7 +700,7 @@ impl LocalLspStore { // to these requests when initializing. language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { let this = this.clone(); let mut cx = cx.clone(); @@ -715,7 +721,7 @@ impl LocalLspStore { language_server .on_request::({ - let lsp_store = this.clone(); + let lsp_store = lsp_store.clone(); move |params, cx| { let lsp_store = lsp_store.clone(); let mut cx = cx.clone(); @@ -744,7 +750,7 @@ impl LocalLspStore { language_server .on_request::({ - let lsp_store = this.clone(); + let lsp_store = lsp_store.clone(); move |params, cx| { let lsp_store = lsp_store.clone(); let mut cx = cx.clone(); @@ -773,7 +779,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { let mut cx = cx.clone(); let this = this.clone(); @@ -792,18 +798,22 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let lsp_store = lsp_store.clone(); move |(), cx| { - let this = this.clone(); + let this = lsp_store.clone(); let mut cx = cx.clone(); async move { - this.update(&mut cx, |this, cx| { - cx.emit(LspStoreEvent::RefreshInlayHints); - this.downstream_client.as_ref().map(|(client, project_id)| { - client.send(proto::RefreshInlayHints { - project_id: *project_id, + this.update(&mut cx, |lsp_store, cx| { + cx.emit(LspStoreEvent::RefreshInlayHints(server_id)); + lsp_store + .downstream_client + .as_ref() + .map(|(client, project_id)| { + client.send(proto::RefreshInlayHints { + project_id: *project_id, + server_id: server_id.to_proto(), + }) }) - }) })? .transpose()?; Ok(()) @@ -814,7 +824,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |(), cx| { let this = this.clone(); let mut cx = cx.clone(); @@ -836,7 +846,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); move |(), cx| { let this = this.clone(); let mut cx = cx.clone(); @@ -862,7 +872,7 @@ impl LocalLspStore { language_server .on_request::({ - let this = this.clone(); + let this = lsp_store.clone(); let name = name.to_string(); move |params, cx| { let this = this.clone(); @@ -900,7 +910,7 @@ impl LocalLspStore { .detach(); language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); let name = name.to_string(); move |params, cx| { let this = this.clone(); @@ -932,7 +942,7 @@ impl LocalLspStore { language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { if let Some(this) = this.upgrade() { this.update(cx, |this, cx| { @@ -951,7 +961,7 @@ impl LocalLspStore { language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { if let Some(this) = this.upgrade() { this.update(cx, |_, cx| { @@ -969,7 +979,7 @@ impl LocalLspStore { language_server .on_notification::({ - let this = this.clone(); + let this = lsp_store.clone(); move |params, cx| { let mut cx = cx.clone(); if let Some(this) = this.upgrade() { @@ -988,10 +998,10 @@ impl LocalLspStore { }) .detach(); - vue_language_server_ext::register_requests(this.clone(), language_server); - json_language_server_ext::register_requests(this.clone(), language_server); - rust_analyzer_ext::register_notifications(this.clone(), language_server); - clangd_ext::register_notifications(this, language_server, adapter); + vue_language_server_ext::register_requests(lsp_store.clone(), language_server); + json_language_server_ext::register_requests(lsp_store.clone(), language_server); + rust_analyzer_ext::register_notifications(lsp_store.clone(), language_server); + clangd_ext::register_notifications(lsp_store, language_server, adapter); } fn shutdown_language_servers_on_quit( @@ -3498,9 +3508,55 @@ pub struct LspStore { diagnostic_summaries: HashMap, HashMap>>, pub lsp_server_capabilities: HashMap, - lsp_document_colors: HashMap, - lsp_code_lens: HashMap, - running_lsp_requests: HashMap>)>, + lsp_data: HashMap, + next_hint_id: Arc, +} + +#[derive(Debug)] +pub struct BufferLspData { + buffer_version: Global, + document_colors: Option, + code_lens: Option, + inlay_hints: BufferInlayHints, + lsp_requests: HashMap>>, + chunk_lsp_requests: HashMap>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct LspKey { + request_type: TypeId, + server_queried: Option, +} + +impl BufferLspData { + fn new(buffer: &Entity, cx: &mut App) -> Self { + Self { + buffer_version: buffer.read(cx).version(), + document_colors: None, + code_lens: None, + inlay_hints: BufferInlayHints::new(buffer, cx), + lsp_requests: HashMap::default(), + chunk_lsp_requests: HashMap::default(), + } + } + + fn remove_server_data(&mut self, for_server: LanguageServerId) { + if let Some(document_colors) = &mut self.document_colors { + document_colors.colors.remove(&for_server); + document_colors.cache_version += 1; + } + + if let Some(code_lens) = &mut self.code_lens { + code_lens.lens.remove(&for_server); + } + + self.inlay_hints.remove_server_data(for_server); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn inlay_hints(&self) -> &BufferInlayHints { + &self.inlay_hints + } } #[derive(Debug, Default, Clone)] @@ -3514,7 +3570,6 @@ type CodeLensTask = Shared>, Arc #[derive(Debug, Default)] struct DocumentColorData { - colors_for_version: Global, colors: HashMap>, cache_version: usize, colors_update: Option<(Global, DocumentColorTask)>, @@ -3522,7 +3577,6 @@ struct DocumentColorData { #[derive(Debug, Default)] struct CodeLensData { - lens_for_version: Global, lens: HashMap>, update: Option<(Global, CodeLensTask)>, } @@ -3543,7 +3597,7 @@ pub enum LspStoreEvent { new_language: Option>, }, Notification(String), - RefreshInlayHints, + RefreshInlayHints(LanguageServerId), RefreshCodeLens, DiagnosticsUpdated { server_id: LanguageServerId, @@ -3615,7 +3669,6 @@ impl LspStore { client.add_entity_request_handler(Self::handle_apply_code_action_kind); client.add_entity_request_handler(Self::handle_resolve_completion_documentation); client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_inlay_hints); client.add_entity_request_handler(Self::handle_get_project_symbols); client.add_entity_request_handler(Self::handle_resolve_inlay_hint); client.add_entity_request_handler(Self::handle_get_color_presentation); @@ -3765,9 +3818,8 @@ impl LspStore { nonce: StdRng::from_os_rng().random(), diagnostic_summaries: HashMap::default(), lsp_server_capabilities: HashMap::default(), - lsp_document_colors: HashMap::default(), - lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), + lsp_data: HashMap::default(), + next_hint_id: Arc::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3826,9 +3878,8 @@ impl LspStore { nonce: StdRng::from_os_rng().random(), diagnostic_summaries: HashMap::default(), lsp_server_capabilities: HashMap::default(), - lsp_document_colors: HashMap::default(), - lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), + next_hint_id: Arc::default(), + lsp_data: HashMap::default(), active_entry: None, _maintain_workspace_config, @@ -4025,8 +4076,7 @@ impl LspStore { *refcount }; if refcount == 0 { - lsp_store.lsp_document_colors.remove(&buffer_id); - lsp_store.lsp_code_lens.remove(&buffer_id); + lsp_store.lsp_data.remove(&buffer_id); let local = lsp_store.as_local_mut().unwrap(); local.registered_buffers.remove(&buffer_id); local.buffers_opened_in_servers.remove(&buffer_id); @@ -4293,7 +4343,7 @@ impl LspStore { &self, buffer: &Entity, request: &R, - cx: &Context, + cx: &App, ) -> bool where R: LspCommand, @@ -4314,7 +4364,7 @@ impl LspStore { &self, buffer: &Entity, check: F, - cx: &Context, + cx: &App, ) -> bool where F: Fn(&lsp::ServerCapabilities) -> bool, @@ -4800,7 +4850,65 @@ impl LspStore { } } - pub fn resolve_inlay_hint( + pub fn resolved_hint( + &mut self, + buffer_id: BufferId, + id: InlayId, + cx: &mut Context, + ) -> Option { + let buffer = self.buffer_store.read(cx).get(buffer_id)?; + + let lsp_data = self.lsp_data.get_mut(&buffer_id)?; + let buffer_lsp_hints = &mut lsp_data.inlay_hints; + let hint = buffer_lsp_hints.hint_for_id(id)?.clone(); + let (server_id, resolve_data) = match &hint.resolve_state { + ResolveState::Resolved => return Some(ResolvedHint::Resolved(hint)), + ResolveState::Resolving => { + return Some(ResolvedHint::Resolving( + buffer_lsp_hints.hint_resolves.get(&id)?.clone(), + )); + } + ResolveState::CanResolve(server_id, resolve_data) => (*server_id, resolve_data.clone()), + }; + + let resolve_task = self.resolve_inlay_hint(hint, buffer, server_id, cx); + let buffer_lsp_hints = &mut self.lsp_data.get_mut(&buffer_id)?.inlay_hints; + let previous_task = buffer_lsp_hints.hint_resolves.insert( + id, + cx.spawn(async move |lsp_store, cx| { + let resolved_hint = resolve_task.await; + lsp_store + .update(cx, |lsp_store, _| { + if let Some(old_inlay_hint) = lsp_store + .lsp_data + .get_mut(&buffer_id) + .and_then(|buffer_lsp_data| buffer_lsp_data.inlay_hints.hint_for_id(id)) + { + match resolved_hint { + Ok(resolved_hint) => { + *old_inlay_hint = resolved_hint; + } + Err(e) => { + old_inlay_hint.resolve_state = + ResolveState::CanResolve(server_id, resolve_data); + log::error!("Inlay hint resolve failed: {e:#}"); + } + } + } + }) + .ok(); + }) + .shared(), + ); + debug_assert!( + previous_task.is_none(), + "Did not change hint's resolve state after spawning its resolve" + ); + buffer_lsp_hints.hint_for_id(id)?.resolve_state = ResolveState::Resolving; + None + } + + fn resolve_inlay_hint( &self, mut hint: InlayHint, buffer: Entity, @@ -5149,6 +5257,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5214,6 +5323,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5279,6 +5389,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5344,6 +5455,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5410,6 +5522,7 @@ impl LspStore { let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5477,6 +5590,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -5538,32 +5652,38 @@ impl LspStore { ) -> CodeLensTask { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); + let existing_servers = self.as_local().map(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + }); - if let Some(cached_data) = self.lsp_code_lens.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.lens_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(Some( - cached_data.lens.values().flatten().cloned().collect(), - ))) - .shared(); + if let Some(lsp_data) = self.current_lsp_data(buffer_id) { + if let Some(cached_lens) = &lsp_data.code_lens { + if !version_queried_for.changed_since(&lsp_data.buffer_version) { + let has_different_servers = existing_servers.is_some_and(|existing_servers| { + existing_servers != cached_lens.lens.keys().copied().collect() + }); + if !has_different_servers { + return Task::ready(Ok(Some( + cached_lens.lens.values().flatten().cloned().collect(), + ))) + .shared(); + } + } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() { + if !version_queried_for.changed_since(updating_for) { + return running_update.clone(); + } + } } } - let lsp_data = self.lsp_code_lens.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.update - && !version_queried_for.changed_since(updating_for) - { - return running_update.clone(); - } + let lens_lsp_data = self + .latest_lsp_data(buffer, cx) + .code_lens + .get_or_insert_default(); let buffer = buffer.clone(); let query_version_queried_for = version_queried_for.clone(); let new_task = cx @@ -5582,7 +5702,13 @@ impl LspStore { Err(e) => { lsp_store .update(cx, |lsp_store, _| { - lsp_store.lsp_code_lens.entry(buffer_id).or_default().update = None; + if let Some(lens_lsp_data) = lsp_store + .lsp_data + .get_mut(&buffer_id) + .and_then(|lsp_data| lsp_data.code_lens.as_mut()) + { + lens_lsp_data.update = None; + } }) .ok(); return Err(e); @@ -5591,25 +5717,26 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); + let lsp_data = lsp_store.current_lsp_data(buffer_id)?; + let code_lens = lsp_data.code_lens.as_mut()?; if let Some(fetched_lens) = fetched_lens { - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens); + if lsp_data.buffer_version == query_version_queried_for { + code_lens.lens.extend(fetched_lens); } else if !lsp_data - .lens_for_version + .buffer_version .changed_since(&query_version_queried_for) { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens; + lsp_data.buffer_version = query_version_queried_for; + code_lens.lens = fetched_lens; } } - lsp_data.update = None; - Some(lsp_data.lens.values().flatten().cloned().collect()) + code_lens.update = None; + Some(code_lens.lens.values().flatten().cloned().collect()) }) .map_err(Arc::new) }) .shared(); - lsp_data.update = Some((version_queried_for, new_task.clone())); + lens_lsp_data.update = Some((version_queried_for, new_task.clone())); new_task } @@ -5625,6 +5752,7 @@ impl LspStore { } let request_task = upstream_client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -6327,6 +6455,7 @@ impl LspStore { } let request_task = client.request_lsp( upstream_project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), @@ -6369,58 +6498,308 @@ impl LspStore { } } + pub fn applicable_inlay_chunks( + &self, + buffer_id: BufferId, + ranges: &[Range], + ) -> Vec> { + self.lsp_data + .get(&buffer_id) + .map(|data| { + data.inlay_hints + .applicable_chunks(ranges) + .map(|chunk| chunk.start..chunk.end) + .collect() + }) + .unwrap_or_default() + } + + pub fn invalidate_inlay_hints<'a>( + &'a mut self, + for_buffers: impl IntoIterator + 'a, + ) { + for buffer_id in for_buffers { + if let Some(lsp_data) = self.lsp_data.get_mut(buffer_id) { + lsp_data.inlay_hints.clear(); + } + } + } + pub fn inlay_hints( &mut self, + invalidate: InvalidationStrategy, buffer: Entity, - range: Range, + ranges: Vec>, + known_chunks: Option<(clock::Global, HashSet>)>, cx: &mut Context, - ) -> Task>> { - let range_start = range.start; - let range_end = range.end; - let buffer_id = buffer.read(cx).remote_id().into(); - let request = InlayHints { range }; + ) -> HashMap, Task>> { + let buffer_snapshot = buffer.read(cx).snapshot(); + let for_server = if let InvalidationStrategy::RefreshRequested(server_id) = invalidate { + Some(server_id) + } else { + None + }; + let invalidate_cache = invalidate.should_invalidate(); + let next_hint_id = self.next_hint_id.clone(); + let lsp_data = self.latest_lsp_data(&buffer, cx); + let existing_inlay_hints = &mut lsp_data.inlay_hints; + let known_chunks = known_chunks + .filter(|(known_version, _)| !lsp_data.buffer_version.changed_since(known_version)) + .map(|(_, known_chunks)| known_chunks) + .unwrap_or_default(); - if let Some((client, project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request(&buffer, &request, cx) { - return Task::ready(Ok(Vec::new())); + let mut hint_fetch_tasks = Vec::new(); + let mut cached_inlay_hints = HashMap::default(); + let mut ranges_to_query = Vec::new(); + let applicable_chunks = existing_inlay_hints + .applicable_chunks(ranges.as_slice()) + .filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end))) + .collect::>(); + if applicable_chunks.is_empty() { + return HashMap::default(); + } + + let last_chunk_number = applicable_chunks.len() - 1; + + for (i, row_chunk) in applicable_chunks.into_iter().enumerate() { + match ( + existing_inlay_hints + .cached_hints(&row_chunk) + .filter(|_| !invalidate_cache) + .cloned(), + existing_inlay_hints + .fetched_hints(&row_chunk) + .as_ref() + .filter(|_| !invalidate_cache) + .cloned(), + ) { + (None, None) => { + let end = if last_chunk_number == i { + Point::new(row_chunk.end, buffer_snapshot.line_len(row_chunk.end)) + } else { + Point::new(row_chunk.end, 0) + }; + ranges_to_query.push(( + row_chunk, + buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0)) + ..buffer_snapshot.anchor_after(end), + )); + } + (None, Some(fetched_hints)) => { + hint_fetch_tasks.push((row_chunk, fetched_hints.clone())) + } + (Some(cached_hints), None) => { + for (server_id, cached_hints) in cached_hints { + if for_server.is_none_or(|for_server| for_server == server_id) { + cached_inlay_hints + .entry(row_chunk.start..row_chunk.end) + .or_insert_with(HashMap::default) + .entry(server_id) + .or_insert_with(Vec::new) + .extend(cached_hints); + } + } + } + (Some(cached_hints), Some(fetched_hints)) => { + hint_fetch_tasks.push((row_chunk, fetched_hints.clone())); + for (server_id, cached_hints) in cached_hints { + if for_server.is_none_or(|for_server| for_server == server_id) { + cached_inlay_hints + .entry(row_chunk.start..row_chunk.end) + .or_insert_with(HashMap::default) + .entry(server_id) + .or_insert_with(Vec::new) + .extend(cached_hints); + } + } + } } - let proto_request = proto::InlayHints { - project_id, - buffer_id, - start: Some(serialize_anchor(&range_start)), - end: Some(serialize_anchor(&range_end)), - version: serialize_version(&buffer.read(cx).version()), - }; - cx.spawn(async move |project, cx| { - let response = client - .request(proto_request) - .await - .context("inlay hints proto request")?; - LspCommand::response_from_proto( - request, - response, - project.upgrade().context("No project")?, - buffer.clone(), - cx.clone(), + } + + let cached_chunk_data = cached_inlay_hints + .into_iter() + .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints)))) + .collect(); + if hint_fetch_tasks.is_empty() && ranges_to_query.is_empty() { + cached_chunk_data + } else { + if invalidate_cache { + lsp_data.inlay_hints.clear(); + } + + for (chunk, range_to_query) in ranges_to_query { + let next_hint_id = next_hint_id.clone(); + let buffer = buffer.clone(); + let new_inlay_hints = cx + .spawn(async move |lsp_store, cx| { + let new_fetch_task = lsp_store.update(cx, |lsp_store, cx| { + lsp_store.fetch_inlay_hints(for_server, &buffer, range_to_query, cx) + })?; + new_fetch_task + .await + .and_then(|new_hints_by_server| { + lsp_store.update(cx, |lsp_store, cx| { + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + let update_cache = !lsp_data + .buffer_version + .changed_since(&buffer.read(cx).version()); + new_hints_by_server + .into_iter() + .map(|(server_id, new_hints)| { + let new_hints = new_hints + .into_iter() + .map(|new_hint| { + ( + InlayId::Hint(next_hint_id.fetch_add( + 1, + atomic::Ordering::AcqRel, + )), + new_hint, + ) + }) + .collect::>(); + if update_cache { + lsp_data.inlay_hints.insert_new_hints( + chunk, + server_id, + new_hints.clone(), + ); + } + (server_id, new_hints) + }) + .collect() + }) + }) + .map_err(Arc::new) + }) + .shared(); + + let fetch_task = lsp_data.inlay_hints.fetched_hints(&chunk); + *fetch_task = Some(new_inlay_hints.clone()); + hint_fetch_tasks.push((chunk, new_inlay_hints)); + } + + let mut combined_data = cached_chunk_data; + combined_data.extend(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| { + ( + chunk.start..chunk.end, + cx.spawn(async move |_, _| { + hints_fetch.await.map_err(|e| { + if e.error_code() != ErrorCode::Internal { + anyhow!(e.error_code()) + } else { + anyhow!("{e:#}") + } + }) + }), ) - .await - .context("inlay hints proto response conversion") + })); + combined_data + } + } + + fn fetch_inlay_hints( + &mut self, + for_server: Option, + buffer: &Entity, + range: Range, + cx: &mut Context, + ) -> Task>>> { + let request = InlayHints { + range: range.clone(), + }; + if let Some((upstream_client, project_id)) = self.upstream_client() { + if !self.is_capable_for_proto_request(buffer, &request, cx) { + return Task::ready(Ok(HashMap::default())); + } + let request_task = upstream_client.request_lsp( + project_id, + for_server.map(|id| id.to_proto()), + LSP_REQUEST_TIMEOUT, + cx.background_executor().clone(), + request.to_proto(project_id, buffer.read(cx)), + ); + let buffer = buffer.clone(); + cx.spawn(async move |weak_lsp_store, cx| { + let Some(lsp_store) = weak_lsp_store.upgrade() else { + return Ok(HashMap::default()); + }; + let Some(responses) = request_task.await? else { + return Ok(HashMap::default()); + }; + + let inlay_hints = join_all(responses.payload.into_iter().map(|response| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + let request = request.clone(); + async move { + ( + LanguageServerId::from_proto(response.server_id), + request + .response_from_proto(response.response, lsp_store, buffer, cx) + .await, + ) + } + })) + .await; + + let mut has_errors = false; + let inlay_hints = inlay_hints + .into_iter() + .filter_map(|(server_id, inlay_hints)| match inlay_hints { + Ok(inlay_hints) => Some((server_id, inlay_hints)), + Err(e) => { + has_errors = true; + log::error!("{e:#}"); + None + } + }) + .collect::>(); + anyhow::ensure!( + !has_errors || !inlay_hints.is_empty(), + "Failed to fetch inlay hints" + ); + Ok(inlay_hints) }) } else { - let lsp_request_task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::FirstCapable, - request, - cx, - ); - cx.spawn(async move |_, cx| { - buffer - .update(cx, |buffer, _| { - buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) - })? + let inlay_hints_task = match for_server { + Some(server_id) => { + let server_task = self.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + request, + cx, + ); + cx.background_spawn(async move { + let mut responses = Vec::new(); + match server_task.await { + Ok(response) => responses.push((server_id, response)), + Err(e) => log::error!( + "Error handling response for inlay hints request: {e:#}" + ), + } + responses + }) + } + None => self.request_multiple_lsp_locally(buffer, None::, request, cx), + }; + let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + cx.background_spawn(async move { + Ok(inlay_hints_task .await - .context("waiting for inlay hint request range edits")?; - lsp_request_task.await.context("inlay hints LSP request") + .into_iter() + .map(|(server_id, mut new_hints)| { + new_hints.retain(|hint| { + hint.position.is_valid(&buffer_snapshot) + && range.start.is_valid(&buffer_snapshot) + && range.end.is_valid(&buffer_snapshot) + && hint.position.cmp(&range.start, &buffer_snapshot).is_ge() + && hint.position.cmp(&range.end, &buffer_snapshot).is_le() + }); + (server_id, new_hints) + }) + .collect()) }) } } @@ -6531,39 +6910,55 @@ impl LspStore { let version_queried_for = buffer.read(cx).version(); let buffer_id = buffer.read(cx).remote_id(); - if let Some(cached_data) = self.lsp_document_colors.get(&buffer_id) - && !version_queried_for.changed_since(&cached_data.colors_for_version) - { - let has_different_servers = self.as_local().is_some_and(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - != cached_data.colors.keys().copied().collect() - }); - if !has_different_servers { - if Some(cached_data.cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_data.colors.values().flatten().cloned().collect(), - cache_version: Some(cached_data.cache_version), - })) - .shared(), - ); + let current_language_servers = self.as_local().map(|local| { + local + .buffers_opened_in_servers + .get(&buffer_id) + .cloned() + .unwrap_or_default() + }); + + if let Some(lsp_data) = self.current_lsp_data(buffer_id) { + if let Some(cached_colors) = &lsp_data.document_colors { + if !version_queried_for.changed_since(&lsp_data.buffer_version) { + let has_different_servers = + current_language_servers.is_some_and(|current_language_servers| { + current_language_servers + != cached_colors.colors.keys().copied().collect() + }); + if !has_different_servers { + let cache_version = cached_colors.cache_version; + if Some(cache_version) == known_cache_version { + return None; + } else { + return Some( + Task::ready(Ok(DocumentColors { + colors: cached_colors + .colors + .values() + .flatten() + .cloned() + .collect(), + cache_version: Some(cache_version), + })) + .shared(), + ); + } + } } } } - let lsp_data = self.lsp_document_colors.entry(buffer_id).or_default(); - if let Some((updating_for, running_update)) = &lsp_data.colors_update + let color_lsp_data = self + .latest_lsp_data(&buffer, cx) + .document_colors + .get_or_insert_default(); + if let Some((updating_for, running_update)) = &color_lsp_data.colors_update && !version_queried_for.changed_since(updating_for) { return Some(running_update.clone()); } - let query_version_queried_for = version_queried_for.clone(); + let buffer_version_queried_for = version_queried_for.clone(); let new_task = cx .spawn(async move |lsp_store, cx| { cx.background_executor() @@ -6581,7 +6976,7 @@ impl LspStore { if Some(true) == buffer .update(cx, |buffer, _| { - buffer.version() != query_version_queried_for + buffer.version() != buffer_version_queried_for }) .ok() { @@ -6592,11 +6987,11 @@ impl LspStore { Err(e) => { lsp_store .update(cx, |lsp_store, _| { - lsp_store - .lsp_document_colors - .entry(buffer_id) - .or_default() - .colors_update = None; + if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) { + if let Some(document_colors) = &mut lsp_data.document_colors { + document_colors.colors_update = None; + } + } }) .ok(); return Err(e); @@ -6604,24 +6999,25 @@ impl LspStore { }; lsp_store - .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); + .update(cx, |lsp_store, cx| { + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + let lsp_colors = lsp_data.document_colors.get_or_insert_default(); if let Some(fetched_colors) = fetched_colors { - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors); - lsp_data.cache_version += 1; + if lsp_data.buffer_version == buffer_version_queried_for { + lsp_colors.colors.extend(fetched_colors); + lsp_colors.cache_version += 1; } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) + .buffer_version + .changed_since(&buffer_version_queried_for) { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors; - lsp_data.cache_version += 1; + lsp_data.buffer_version = buffer_version_queried_for; + lsp_colors.colors = fetched_colors; + lsp_colors.cache_version += 1; } } - lsp_data.colors_update = None; - let colors = lsp_data + lsp_colors.colors_update = None; + let colors = lsp_colors .colors .values() .flatten() @@ -6629,13 +7025,13 @@ impl LspStore { .collect::>(); DocumentColors { colors, - cache_version: Some(lsp_data.cache_version), + cache_version: Some(lsp_colors.cache_version), } }) .map_err(Arc::new) }) .shared(); - lsp_data.colors_update = Some((version_queried_for, new_task.clone())); + color_lsp_data.colors_update = Some((version_queried_for, new_task.clone())); Some(new_task) } @@ -6652,6 +7048,7 @@ impl LspStore { let request_task = client.request_lsp( project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(project_id, buffer.read(cx)), @@ -6730,6 +7127,7 @@ impl LspStore { } let request_task = client.request_lsp( upstream_project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), @@ -6793,6 +7191,7 @@ impl LspStore { } let request_task = client.request_lsp( upstream_project_id, + None, LSP_REQUEST_TIMEOUT, cx.background_executor().clone(), request.to_proto(upstream_project_id, buffer.read(cx)), @@ -7899,8 +8298,9 @@ impl LspStore { cx.background_spawn(async move { let mut responses = Vec::with_capacity(response_results.len()); while let Some((server_id, response_result)) = response_results.next().await { - if let Some(response) = response_result.log_err() { - responses.push((server_id, response)); + match response_result { + Ok(response) => responses.push((server_id, response)), + Err(e) => log::error!("Error handling response for request {request:?}: {e:#}"), } } responses @@ -7958,27 +8358,30 @@ impl LspStore { let sender_id = envelope.original_sender_id().unwrap_or_default(); let lsp_query = envelope.payload; let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); + let server_id = lsp_query.server_id.map(LanguageServerId::from_proto); match lsp_query.request.context("invalid LSP query request")? { Request::GetReferences(get_references) => { let position = get_references.position.clone().and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_references, position, - cx.clone(), + &mut cx, ) .await?; } Request::GetDocumentColor(get_document_color) => { Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_document_color, None, - cx.clone(), + &mut cx, ) .await?; } @@ -7986,22 +8389,24 @@ impl LspStore { let position = get_hover.position.clone().and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_hover, position, - cx.clone(), + &mut cx, ) .await?; } Request::GetCodeActions(get_code_actions) => { Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_code_actions, None, - cx.clone(), + &mut cx, ) .await?; } @@ -8012,22 +8417,24 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_signature_help, position, - cx.clone(), + &mut cx, ) .await?; } Request::GetCodeLens(get_code_lens) => { Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_code_lens, None, - cx.clone(), + &mut cx, ) .await?; } @@ -8035,11 +8442,12 @@ impl LspStore { let position = get_definition.position.clone().and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_definition, position, - cx.clone(), + &mut cx, ) .await?; } @@ -8050,11 +8458,12 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_declaration, position, - cx.clone(), + &mut cx, ) .await?; } @@ -8065,11 +8474,12 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_type_definition, position, - cx.clone(), + &mut cx, ) .await?; } @@ -8080,15 +8490,15 @@ impl LspStore { .and_then(deserialize_anchor); Self::query_lsp_locally::( lsp_store, + server_id, sender_id, lsp_request_id, get_implementation, position, - cx.clone(), + &mut cx, ) .await?; } - // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. Request::GetDocumentDiagnostics(get_document_diagnostics) => { let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; let version = deserialize_version(get_document_diagnostics.buffer_version()); @@ -8101,16 +8511,20 @@ impl LspStore { })? .await?; lsp_store.update(&mut cx, |lsp_store, cx| { - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + let key = LspKey { + request_type: TypeId::of::(), + server_queried: server_id, + }; if ::ProtoRequest::stop_previous_requests( - ) || buffer.read(cx).version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); + ) { + if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { + lsp_requests.clear(); + }; } - existing_queries.1.insert( + + let existing_queries = lsp_data.lsp_requests.entry(key).or_default(); + existing_queries.insert( lsp_request_id, cx.spawn(async move |lsp_store, cx| { let diagnostics_pull = lsp_store @@ -8128,6 +8542,39 @@ impl LspStore { ); })?; } + Request::InlayHints(inlay_hints) => { + let query_start = inlay_hints + .start + .clone() + .and_then(deserialize_anchor) + .context("invalid inlay hints range start")?; + let query_end = inlay_hints + .end + .clone() + .and_then(deserialize_anchor) + .context("invalid inlay hints range end")?; + Self::deduplicate_range_based_lsp_requests::( + &lsp_store, + server_id, + lsp_request_id, + &inlay_hints, + query_start..query_end, + &mut cx, + ) + .await + .context("preparing inlay hints request")?; + Self::query_lsp_locally::( + lsp_store, + server_id, + sender_id, + lsp_request_id, + inlay_hints, + None, + &mut cx, + ) + .await + .context("querying for inlay hints")? + } } Ok(proto::Ack {}) } @@ -9043,7 +9490,7 @@ impl LspStore { if let Some(work) = status.pending_work.remove(&token) && !work.is_disk_based_diagnostics_progress { - cx.emit(LspStoreEvent::RefreshInlayHints); + cx.emit(LspStoreEvent::RefreshInlayHints(language_server_id)); } cx.notify(); } @@ -9175,12 +9622,14 @@ impl LspStore { } async fn handle_refresh_inlay_hints( - this: Entity, - _: TypedEnvelope, + lsp_store: Entity, + envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::RefreshInlayHints); + lsp_store.update(&mut cx, |_, cx| { + cx.emit(LspStoreEvent::RefreshInlayHints( + LanguageServerId::from_proto(envelope.payload.server_id), + )); })?; Ok(proto::Ack {}) } @@ -9197,51 +9646,6 @@ impl LspStore { Ok(proto::Ack {}) } - async fn handle_inlay_hints( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&envelope.payload.version)) - })? - .await - .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - - let start = envelope - .payload - .start - .and_then(deserialize_anchor) - .context("missing range start")?; - let end = envelope - .payload - .end - .and_then(deserialize_anchor) - .context("missing range end")?; - let buffer_hints = this - .update(&mut cx, |lsp_store, cx| { - lsp_store.inlay_hints(buffer.clone(), start..end, cx) - })? - .await - .context("inlay hints fetch")?; - - this.update(&mut cx, |project, cx| { - InlayHints::response_to_proto( - buffer_hints, - project, - sender_id, - &buffer.read(cx).version(), - cx, - ) - }) - } - async fn handle_get_color_presentation( lsp_store: Entity, envelope: TypedEnvelope, @@ -9307,7 +9711,7 @@ impl LspStore { } async fn handle_resolve_inlay_hint( - this: Entity, + lsp_store: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { @@ -9317,13 +9721,13 @@ impl LspStore { .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); let hint = InlayHints::proto_to_project_hint(proto_hint) .context("resolved proto inlay hint conversion")?; - let buffer = this.update(&mut cx, |this, cx| { + let buffer = lsp_store.update(&mut cx, |lsp_store, cx| { let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - this.buffer_store.read(cx).get_existing(buffer_id) + lsp_store.buffer_store.read(cx).get_existing(buffer_id) })??; - let response_hint = this - .update(&mut cx, |this, cx| { - this.resolve_inlay_hint( + let response_hint = lsp_store + .update(&mut cx, |lsp_store, cx| { + lsp_store.resolve_inlay_hint( hint, buffer, LanguageServerId(envelope.payload.language_server_id as usize), @@ -10429,7 +10833,7 @@ impl LspStore { language_server.name(), Some(key.worktree_id), )); - cx.emit(LspStoreEvent::RefreshInlayHints); + cx.emit(LspStoreEvent::RefreshInlayHints(server_id)); let server_capabilities = language_server.capabilities(); if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { @@ -11047,12 +11451,8 @@ impl LspStore { fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { self.lsp_server_capabilities.remove(&for_server); - for buffer_colors in self.lsp_document_colors.values_mut() { - buffer_colors.colors.remove(&for_server); - buffer_colors.cache_version += 1; - } - for buffer_lens in self.lsp_code_lens.values_mut() { - buffer_lens.lens.remove(&for_server); + for lsp_data in self.lsp_data.values_mut() { + lsp_data.remove_server_data(for_server); } if let Some(local) = self.as_local_mut() { local.buffer_pull_diagnostics_result_ids.remove(&for_server); @@ -11661,13 +12061,71 @@ impl LspStore { Ok(()) } + async fn deduplicate_range_based_lsp_requests( + lsp_store: &Entity, + server_id: Option, + lsp_request_id: LspRequestId, + proto_request: &T::ProtoRequest, + range: Range, + cx: &mut AsyncApp, + ) -> Result<()> + where + T: LspCommand, + T::ProtoRequest: proto::LspRequestMessage, + { + let buffer_id = BufferId::new(proto_request.buffer_id())?; + let version = deserialize_version(proto_request.buffer_version()); + let buffer = lsp_store.update(cx, |this, cx| { + this.buffer_store.read(cx).get_existing(buffer_id) + })??; + buffer + .update(cx, |buffer, _| buffer.wait_for_version(version))? + .await?; + lsp_store.update(cx, |lsp_store, cx| { + let lsp_data = lsp_store + .lsp_data + .entry(buffer_id) + .or_insert_with(|| BufferLspData::new(&buffer, cx)); + let chunks_queried_for = lsp_data + .inlay_hints + .applicable_chunks(&[range]) + .collect::>(); + match chunks_queried_for.as_slice() { + &[chunk] => { + let key = LspKey { + request_type: TypeId::of::(), + server_queried: server_id, + }; + let previous_request = lsp_data + .chunk_lsp_requests + .entry(key) + .or_default() + .insert(chunk, lsp_request_id); + if let Some((previous_request, running_requests)) = + previous_request.zip(lsp_data.lsp_requests.get_mut(&key)) + { + running_requests.remove(&previous_request); + } + } + _ambiguous_chunks => { + // Have not found a unique chunk for the query range — be lenient and let the query to be spawned, + // there, a buffer version-based check will be performed and outdated requests discarded. + } + } + anyhow::Ok(()) + })??; + + Ok(()) + } + async fn query_lsp_locally( lsp_store: Entity, + for_server_id: Option, sender_id: proto::PeerId, lsp_request_id: LspRequestId, proto_request: T::ProtoRequest, position: Option, - mut cx: AsyncApp, + cx: &mut AsyncApp, ) -> Result<()> where T: LspCommand + Clone, @@ -11677,30 +12135,48 @@ impl LspStore { { let buffer_id = BufferId::new(proto_request.buffer_id())?; let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { + let buffer = lsp_store.update(cx, |this, cx| { this.buffer_store.read(cx).get_existing(buffer_id) })??; buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? + .update(cx, |buffer, _| buffer.wait_for_version(version.clone()))? .await?; - let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; + let buffer_version = buffer.read_with(cx, |buffer, _| buffer.version())?; let request = T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let request_task = - lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); - if T::ProtoRequest::stop_previous_requests() - || buffer_version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); + let key = LspKey { + request_type: TypeId::of::(), + server_queried: for_server_id, + }; + lsp_store.update(cx, |lsp_store, cx| { + let request_task = match for_server_id { + Some(server_id) => { + let server_task = lsp_store.request_lsp( + buffer.clone(), + LanguageServerToQuery::Other(server_id), + request.clone(), + cx, + ); + cx.background_spawn(async move { + let mut responses = Vec::new(); + match server_task.await { + Ok(response) => responses.push((server_id, response)), + Err(e) => log::error!( + "Error handling response for request {request:?}: {e:#}" + ), + } + responses + }) + } + None => lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx), + }; + let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); + if T::ProtoRequest::stop_previous_requests() { + if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { + lsp_requests.clear(); + } } - existing_queries.1.insert( + lsp_data.lsp_requests.entry(key).or_default().insert( lsp_request_id, cx.spawn(async move |lsp_store, cx| { let response = request_task.await; @@ -11759,8 +12235,15 @@ impl LspStore { #[cfg(any(test, feature = "test-support"))] pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { - let data = self.lsp_code_lens.get_mut(&buffer_id)?; - Some(data.update.take()?.1) + Some( + self.lsp_data + .get_mut(&buffer_id)? + .code_lens + .take()? + .update + .take()? + .1, + ) } pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { @@ -11770,6 +12253,26 @@ impl LspStore { pub fn worktree_store(&self) -> Entity { self.worktree_store.clone() } + + /// Gets what's stored in the LSP data for the given buffer. + pub fn current_lsp_data(&mut self, buffer_id: BufferId) -> Option<&mut BufferLspData> { + self.lsp_data.get_mut(&buffer_id) + } + + /// Gets the most recent LSP data for the given buffer: if the data is absent or out of date, + /// new [`BufferLspData`] will be created to replace the previous state. + pub fn latest_lsp_data(&mut self, buffer: &Entity, cx: &mut App) -> &mut BufferLspData { + let (buffer_id, buffer_version) = + buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version())); + let lsp_data = self + .lsp_data + .entry(buffer_id) + .or_insert_with(|| BufferLspData::new(buffer, cx)); + if buffer_version.changed_since(&lsp_data.buffer_version) { + *lsp_data = BufferLspData::new(buffer, cx); + } + lsp_data + } } // Registration with registerOptions as null, should fallback to true. @@ -12523,6 +13026,11 @@ impl From for CompletionDocumentation { } } +pub enum ResolvedHint { + Resolved(InlayHint), + Resolving(Shared>), +} + fn glob_literal_prefix(glob: &Path) -> PathBuf { glob.components() .take_while(|component| match component { diff --git a/crates/project/src/lsp_store/inlay_hint_cache.rs b/crates/project/src/lsp_store/inlay_hint_cache.rs new file mode 100644 index 0000000000000000000000000000000000000000..0d527b83d2eef03b9473edc2711041c0ebccadb6 --- /dev/null +++ b/crates/project/src/lsp_store/inlay_hint_cache.rs @@ -0,0 +1,221 @@ +use std::{collections::hash_map, ops::Range, sync::Arc}; + +use collections::HashMap; +use futures::future::Shared; +use gpui::{App, Entity, Task}; +use language::{Buffer, BufferRow, BufferSnapshot}; +use lsp::LanguageServerId; +use text::OffsetRangeExt; + +use crate::{InlayHint, InlayId}; + +pub type CacheInlayHints = HashMap>; +pub type CacheInlayHintsTask = Shared>>>; + +/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. +#[derive(Debug, Clone, Copy)] +pub enum InvalidationStrategy { + /// Language servers reset hints via request. + /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. + /// + /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. + RefreshRequested(LanguageServerId), + /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. + /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. + BufferEdited, + /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. + /// No invalidation should be done at all, all new hints are added to the cache. + /// + /// A special case is the editor toggles and settings change: + /// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints. + /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. + None, +} + +impl InvalidationStrategy { + pub fn should_invalidate(&self) -> bool { + matches!( + self, + InvalidationStrategy::RefreshRequested(_) | InvalidationStrategy::BufferEdited + ) + } +} + +pub struct BufferInlayHints { + snapshot: BufferSnapshot, + buffer_chunks: Vec, + hints_by_chunks: Vec>, + fetches_by_chunks: Vec>, + hints_by_id: HashMap, + pub(super) hint_resolves: HashMap>>, +} + +#[derive(Debug, Clone, Copy)] +struct HintForId { + chunk_id: usize, + server_id: LanguageServerId, + position: usize, +} + +/// An range of rows, exclusive as [`lsp::Range`] and +/// +/// denote. +/// +/// Represents an area in a text editor, adjacent to other ones. +/// Together, chunks form entire document at a particular version [`clock::Global`]. +/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via +/// +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BufferChunk { + id: usize, + pub start: BufferRow, + pub end: BufferRow, +} + +impl std::fmt::Debug for BufferInlayHints { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BufferInlayHints") + .field("buffer_chunks", &self.buffer_chunks) + .field("hints_by_chunks", &self.hints_by_chunks) + .field("fetches_by_chunks", &self.fetches_by_chunks) + .field("hints_by_id", &self.hints_by_id) + .finish_non_exhaustive() + } +} + +const MAX_ROWS_IN_A_CHUNK: u32 = 50; + +impl BufferInlayHints { + pub fn new(buffer: &Entity, cx: &mut App) -> Self { + let buffer = buffer.read(cx); + let snapshot = buffer.snapshot(); + let buffer_point_range = (0..buffer.len()).to_point(&snapshot); + let last_row = buffer_point_range.end.row; + let buffer_chunks = (buffer_point_range.start.row..=last_row) + .step_by(MAX_ROWS_IN_A_CHUNK as usize) + .enumerate() + .map(|(id, chunk_start)| BufferChunk { + id, + start: chunk_start, + end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row), + }) + .collect::>(); + + Self { + hints_by_chunks: vec![None; buffer_chunks.len()], + fetches_by_chunks: vec![None; buffer_chunks.len()], + hints_by_id: HashMap::default(), + hint_resolves: HashMap::default(), + snapshot, + buffer_chunks, + } + } + + pub fn applicable_chunks( + &self, + ranges: &[Range], + ) -> impl Iterator { + let row_ranges = ranges + .iter() + .map(|range| range.to_point(&self.snapshot)) + .map(|point_range| point_range.start.row..=point_range.end.row) + .collect::>(); + self.buffer_chunks + .iter() + .filter(move |chunk| -> bool { + // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range. + // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around. + let chunk_range = chunk.start..=chunk.end; + row_ranges.iter().any(|row_range| { + chunk_range.contains(&row_range.start()) + || chunk_range.contains(&row_range.end()) + }) + }) + .copied() + } + + pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> { + self.hints_by_chunks[chunk.id].as_ref() + } + + pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option { + &mut self.fetches_by_chunks[chunk.id] + } + + #[cfg(any(test, feature = "test-support"))] + pub fn all_cached_hints(&self) -> Vec { + self.hints_by_chunks + .iter() + .filter_map(|hints| hints.as_ref()) + .flat_map(|hints| hints.values().cloned()) + .flatten() + .map(|(_, hint)| hint) + .collect() + } + + #[cfg(any(test, feature = "test-support"))] + pub fn all_fetched_hints(&self) -> Vec { + self.fetches_by_chunks + .iter() + .filter_map(|fetches| fetches.clone()) + .collect() + } + + pub fn remove_server_data(&mut self, for_server: LanguageServerId) { + for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() { + if let Some(hints) = hints { + if hints.remove(&for_server).is_some() { + self.fetches_by_chunks[chunk_index] = None; + } + } + } + } + + pub fn clear(&mut self) { + self.hints_by_chunks = vec![None; self.buffer_chunks.len()]; + self.fetches_by_chunks = vec![None; self.buffer_chunks.len()]; + self.hints_by_id.clear(); + self.hint_resolves.clear(); + } + + pub fn insert_new_hints( + &mut self, + chunk: BufferChunk, + server_id: LanguageServerId, + new_hints: Vec<(InlayId, InlayHint)>, + ) { + let existing_hints = self.hints_by_chunks[chunk.id] + .get_or_insert_default() + .entry(server_id) + .or_insert_with(Vec::new); + let existing_count = existing_hints.len(); + existing_hints.extend(new_hints.into_iter().enumerate().filter_map( + |(i, (id, new_hint))| { + let new_hint_for_id = HintForId { + chunk_id: chunk.id, + server_id, + position: existing_count + i, + }; + if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) { + vacant_entry.insert(new_hint_for_id); + Some((id, new_hint)) + } else { + None + } + }, + )); + *self.fetched_hints(&chunk) = None; + } + + pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> { + let hint_for_id = self.hints_by_id.get(&id)?; + let (hint_id, hint) = self + .hints_by_chunks + .get_mut(hint_for_id.chunk_id)? + .as_mut()? + .get_mut(&hint_for_id.server_id)? + .get_mut(hint_for_id.position)?; + debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}"); + Some(hint) + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f301c7800a5b098ddc93a7badc1617f7842e62d1..f9a3f20fa77c41d1ec6405ea0ee7b245fe4e0845 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -145,9 +145,9 @@ pub use task_inventory::{ pub use buffer_store::ProjectTransaction; pub use lsp_store::{ - DiagnosticSummary, LanguageServerLogType, LanguageServerProgress, LanguageServerPromptRequest, - LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent, - SERVER_PROGRESS_THROTTLE_TIMEOUT, + DiagnosticSummary, InvalidationStrategy, LanguageServerLogType, LanguageServerProgress, + LanguageServerPromptRequest, LanguageServerStatus, LanguageServerToQuery, LspStore, + LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT, }; pub use toolchain_store::{ToolchainStore, Toolchains}; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; @@ -338,7 +338,7 @@ pub enum Event { HostReshared, Reshared, Rejoined, - RefreshInlayHints, + RefreshInlayHints(LanguageServerId), RefreshCodeLens, RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), @@ -402,6 +402,26 @@ pub enum PrepareRenameResponse { InvalidPosition, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InlayId { + EditPrediction(usize), + DebuggerValue(usize), + // LSP + Hint(usize), + Color(usize), +} + +impl InlayId { + pub fn id(&self) -> usize { + match self { + Self::EditPrediction(id) => *id, + Self::DebuggerValue(id) => *id, + Self::Hint(id) => *id, + Self::Color(id) => *id, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { pub position: language::Anchor, @@ -3058,7 +3078,9 @@ impl Project { return; }; } - LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints), + LspStoreEvent::RefreshInlayHints(server_id) => { + cx.emit(Event::RefreshInlayHints(*server_id)) + } LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens), LspStoreEvent::LanguageServerPrompt(prompt) => { cx.emit(Event::LanguageServerPrompt(prompt.clone())) @@ -3978,31 +4000,6 @@ impl Project { }) } - pub fn inlay_hints( - &mut self, - buffer_handle: Entity, - range: Range, - cx: &mut Context, - ) -> Task>> { - let buffer = buffer_handle.read(cx); - let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.inlay_hints(buffer_handle, range, cx) - }) - } - - pub fn resolve_inlay_hint( - &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task> { - self.lsp_store.update(cx, |lsp_store, cx| { - lsp_store.resolve_inlay_hint(hint, buffer_handle, server_id, cx) - }) - } - pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { let (result_tx, result_rx) = smol::channel::unbounded(); @@ -5262,6 +5259,7 @@ impl Project { }) } + #[cfg(any(test, feature = "test-support"))] pub fn has_language_servers_for(&self, buffer: &Buffer, cx: &mut App) -> bool { self.lsp_store.update(cx, |this, cx| { this.language_servers_for_local_buffer(buffer, cx) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 7f504a676c8ef6bd46efdd5f4fd570e69921d652..89a49c6fb0185d36cf2dab3f07cfc6efedd1b6d1 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1815,7 +1815,10 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { fake_server .start_progress(format!("{}/0", progress_token)) .await; - assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints); + assert_eq!( + events.next().await.unwrap(), + Event::RefreshInlayHints(fake_server.server.server_id()) + ); assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted { @@ -1954,7 +1957,10 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC Some(worktree_id) ) ); - assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints); + assert_eq!( + events.next().await.unwrap(), + Event::RefreshInlayHints(fake_server.server.server_id()) + ); fake_server.start_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index d50c1924cdf237d603b78062b3335354a6d6127f..7e446a915febbc03f2dd5920faf12a58a5d9b639 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -465,6 +465,7 @@ message ResolveInlayHintResponse { message RefreshInlayHints { uint64 project_id = 1; + uint64 server_id = 2; } message CodeLens { @@ -781,6 +782,7 @@ message TextEdit { message LspQuery { uint64 project_id = 1; uint64 lsp_request_id = 2; + optional uint64 server_id = 15; oneof request { GetReferences get_references = 3; GetDocumentColor get_document_color = 4; @@ -793,6 +795,7 @@ message LspQuery { GetDeclaration get_declaration = 11; GetTypeDefinition get_type_definition = 12; GetImplementation get_implementation = 13; + InlayHints inlay_hints = 14; } } @@ -815,6 +818,7 @@ message LspResponse { GetTypeDefinitionResponse get_type_definition_response = 10; GetImplementationResponse get_implementation_response = 11; GetReferencesResponse get_references_response = 12; + InlayHintsResponse inlay_hints_response = 13; } uint64 server_id = 7; } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 3710d77262f872d6a422827c9cf1829d21d8f221..433c4c355c6e5c7d32713f6b37060e6a47a4c687 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -517,6 +517,7 @@ lsp_messages!( (GetDeclaration, GetDeclarationResponse, true), (GetTypeDefinition, GetTypeDefinitionResponse, true), (GetImplementation, GetImplementationResponse, true), + (InlayHints, InlayHintsResponse, false), ); entity_messages!( @@ -847,6 +848,7 @@ impl LspQuery { Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), + Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false), None => ("", true), } } diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index a90797ff5dfb44c22fa7aa61751ad3baefd2b745..d7e3ba1e461b28ac264afcc05a8ae941e6da0c32 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -226,6 +226,7 @@ impl AnyProtoClient { pub fn request_lsp( &self, project_id: u64, + server_id: Option, timeout: Duration, executor: BackgroundExecutor, request: T, @@ -247,6 +248,7 @@ impl AnyProtoClient { let query = proto::LspQuery { project_id, + server_id, lsp_request_id: new_id.0, request: Some(request.to_proto_query()), }; @@ -361,6 +363,9 @@ impl AnyProtoClient { Response::GetImplementationResponse(response) => { to_any_envelope(&envelope, response) } + Response::InlayHintsResponse(response) => { + to_any_envelope(&envelope, response) + } }; Some(proto::ProtoLspResponse { server_id, diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index e6bea2e4dd634ca915242f8d86fc31e22bb61c95..7d8efbb11a5f1461da5b63152e2277a38ad272b4 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -47,5 +47,7 @@ zed_actions.workspace = true client = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } +language = { workspace = true, features = ["test-support"] } +lsp.workspace = true unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e4ff5e6e540fa5626699e98725c5713a09e7cce8..97882994d2f8ea452e45dd830b777ec445d3768f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2357,9 +2357,10 @@ pub mod tests { use super::*; use editor::{DisplayPoint, display_map::DisplayRow}; use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle}; + use language::{FakeLspAdapter, rust_lang}; use project::FakeFs; use serde_json::json; - use settings::SettingsStore; + use settings::{InlayHintSettingsContent, SettingsStore}; use util::{path, paths::PathStyle, rel_path::rel_path}; use util_macros::perf; use workspace::DeploySearch; @@ -4226,6 +4227,101 @@ pub mod tests { .unwrap(); } + #[perf] + #[gpui::test] + async fn test_search_with_inlays(cx: &mut TestAppContext) { + init_test(cx); + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.all_languages.defaults.inlay_hints = + Some(InlayHintSettingsContent { + enabled: Some(true), + ..InlayHintSettingsContent::default() + }) + }); + }); + }); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/dir"), + // `\n` , a trailing line on the end, is important for the test case + json!({ + "main.rs": "fn main() { let a = 2; }\n", + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let language = rust_lang(); + language_registry.add(language); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..lsp::ServerCapabilities::default() + }, + initializer: Some(Box::new(|fake_server| { + fake_server.set_request_handler::( + move |_, _| async move { + Ok(Some(vec![lsp::InlayHint { + position: lsp::Position::new(0, 17), + label: lsp::InlayHintLabel::String(": i32".to_owned()), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + }, + ); + })), + ..FakeLspAdapter::default() + }, + ); + + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let workspace = window.root(cx).unwrap(); + let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); + let search_view = cx.add_window(|window, cx| { + ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) + }); + + perform_search(search_view, "let ", cx); + let _fake_server = fake_servers.next().await.unwrap(); + cx.executor().advance_clock(Duration::from_secs(1)); + cx.executor().run_until_parked(); + search_view + .update(cx, |search_view, _, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nfn main() { let a: i32 = 2; }\n" + ); + }) + .unwrap(); + + // Can do the 2nd search without any panics + perform_search(search_view, "let ", cx); + cx.executor().advance_clock(Duration::from_millis(100)); + cx.executor().run_until_parked(); + search_view + .update(cx, |search_view, _, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nfn main() { let a: i32 = 2; }\n" + ); + }) + .unwrap(); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings = SettingsStore::test(cx); diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 59efaff200ce98fd2150932b24492e42a07fa265..0743601839cc31e0e3a4c9d6c936aab7edce5837 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -934,7 +934,7 @@ where /// 2. When encountering digits, treating consecutive digits as a single number /// 3. Comparing numbers by their numeric value rather than lexicographically /// 4. For non-numeric characters, using case-sensitive comparison with lowercase priority -fn natural_sort(a: &str, b: &str) -> Ordering { +pub fn natural_sort(a: &str, b: &str) -> Ordering { let mut a_iter = a.chars().peekable(); let mut b_iter = b.chars().peekable(); diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e44039914f801848ce362b081f6e1bcd18b3c1fa..1a617e36c18ffa52906cac06d4b9eddb11a91f8e 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3083,7 +3083,7 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; - use editor::display_map::Inlay; + use editor::Inlay; use indoc::indoc; use language::Point; use multi_buffer::MultiBufferRow; From f393138711fa2fc48d0441835b86540b19ba3c0d Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 22 Oct 2025 21:52:38 +0200 Subject: [PATCH 164/202] Fix keybind hints flickering in certain scenarios (#40927) Closes #39172 This refactors when we resolve UI keybindings in an effort to reduce flickering whilst painting these: Previously, we would always resolve these upon creating the binding. This could lead to cases where the corresponding context was not yet available and no binding could be resolved, even if the binding was then available on the next presented frame. Following that, on the next rerender of whatever requested this keybinding, the keybind for that context would then be found, we would render that and then also win a layout shift in that process, as we went from nothing rendered to something rendered between these frames. With these changes, this now happens less often, because we only look for the keybinding once the context can actually be resolved in the window. | Before | After | | --- | --- | | https://github.com/user-attachments/assets/adebf8ac-217d-4c7f-ae5a-bab3aa0b0ee8 | https://github.com/user-attachments/assets/70a82b4b-488f-4a9f-94d7-b6d0a49aada9 | Also reduced cloning in the keymap editor in this process, since that requiered changing due to this anyway. Release Notes: - Fixed some cases where keybinds would appear with a slight delay, causing a flicker in the process --- crates/agent_ui/src/acp/mode_selector.rs | 8 +- .../src/acp/model_selector_popover.rs | 10 +- crates/agent_ui/src/acp/thread_history.rs | 8 +- crates/agent_ui/src/acp/thread_view.rs | 69 ++-- .../add_llm_provider_modal.rs | 4 +- .../configure_context_server_modal.rs | 11 +- .../manage_profiles_modal.rs | 19 +- crates/agent_ui/src/agent_diff.rs | 60 +--- crates/agent_ui/src/agent_model_selector.rs | 10 +- crates/agent_ui/src/agent_panel.rs | 27 +- crates/agent_ui/src/context_strip.rs | 6 +- crates/agent_ui/src/inline_prompt_editor.rs | 19 +- crates/agent_ui/src/profile_selector.rs | 3 +- crates/agent_ui/src/text_thread_editor.rs | 28 +- crates/agent_ui/src/ui/burn_mode_tooltip.rs | 7 +- crates/agent_ui/src/ui/context_pill.rs | 4 +- crates/breadcrumbs/src/breadcrumbs.rs | 4 +- crates/collab_ui/src/notification_panel.rs | 4 +- crates/command_palette/src/command_palette.rs | 5 +- crates/debugger_ui/src/debugger_panel.rs | 27 +- crates/debugger_ui/src/new_process_modal.rs | 91 +++-- crates/debugger_ui/src/session/running.rs | 3 +- .../src/session/running/breakpoint_list.rs | 24 +- .../src/session/running/console.rs | 3 +- .../src/session/running/stack_frame_list.rs | 4 +- .../src/session/running/variable_list.rs | 10 +- crates/diagnostics/src/items.rs | 7 +- .../src/edit_prediction_button.rs | 35 +- crates/editor/src/editor.rs | 35 +- crates/editor/src/element.rs | 17 +- crates/editor/src/proposed_changes_editor.rs | 8 +- crates/editor/src/signature_help.rs | 13 +- crates/file_finder/src/file_finder.rs | 23 +- crates/git_ui/src/branch_picker.rs | 3 +- crates/git_ui/src/commit_modal.rs | 9 +- crates/git_ui/src/git_panel.rs | 16 +- crates/git_ui/src/git_ui.rs | 20 +- crates/git_ui/src/project_diff.rs | 3 +- crates/git_ui/src/stash_picker.rs | 18 +- crates/go_to_line/src/cursor_position.rs | 4 +- crates/gpui/src/keymap.rs | 8 +- crates/keymap_editor/src/keymap_editor.rs | 71 ++-- .../src/active_buffer_language.rs | 4 +- crates/language_tools/src/key_context_view.rs | 5 +- crates/language_tools/src/lsp_button.rs | 10 +- .../src/line_ending_indicator.rs | 4 +- crates/onboarding/src/onboarding.rs | 3 +- crates/onboarding/src/welcome.rs | 37 +- crates/project_panel/src/project_panel.rs | 4 +- crates/recent_projects/src/recent_projects.rs | 9 +- crates/repl/src/notebook/notebook_ui.rs | 22 +- crates/repl/src/repl_sessions_ui.rs | 4 +- crates/rules_library/src/rules_library.rs | 27 +- crates/search/src/buffer_search.rs | 3 +- crates/search/src/project_search.rs | 36 +- crates/search/src/search.rs | 4 +- crates/search/src/search_bar.rs | 2 +- crates/search/src/search_status_button.rs | 9 +- crates/settings_ui/src/settings_ui.rs | 22 +- crates/tasks_ui/src/modal.rs | 63 ++-- crates/terminal_view/src/terminal_panel.rs | 13 +- crates/terminal_view/src/terminal_view.rs | 4 +- crates/title_bar/src/collab.rs | 9 +- crates/title_bar/src/onboarding_banner.rs | 3 +- crates/title_bar/src/title_bar.rs | 9 +- .../src/toolchain_selector.rs | 3 - crates/ui/src/components/context_menu.rs | 39 +-- crates/ui/src/components/keybinding.rs | 316 ++++++++++-------- crates/ui/src/components/keybinding_hint.rs | 27 +- .../ui/src/components/stories/keybinding.rs | 85 +++-- crates/ui/src/components/tooltip.rs | 25 +- crates/workspace/src/dock.rs | 4 +- crates/workspace/src/invalid_item_view.rs | 8 +- crates/workspace/src/notifications.rs | 10 +- crates/workspace/src/pane.rs | 14 +- crates/workspace/src/theme_preview.rs | 8 +- crates/zed/src/zed/quick_action_bar.rs | 4 +- .../zed/src/zed/quick_action_bar/preview.rs | 3 +- crates/zeta/src/rate_completion_modal.rs | 10 +- crates/zeta2_tools/src/zeta2_tools.rs | 23 +- 80 files changed, 665 insertions(+), 978 deletions(-) diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs index dda33bd17dbdd166d95ab4554b882fd440a067a5..36970a29ab7fd30f175d8128f8bbd3c55b71b605 100644 --- a/crates/agent_ui/src/acp/mode_selector.rs +++ b/crates/agent_ui/src/acp/mode_selector.rs @@ -194,7 +194,7 @@ impl Render for ModeSelector { trigger_button, Tooltip::element({ let focus_handle = self.focus_handle.clone(); - move |window, cx| { + move |_window, cx| { v_flex() .gap_1() .child( @@ -205,10 +205,9 @@ impl Render for ModeSelector { .border_b_1() .border_color(cx.theme().colors().border_variant) .child(Label::new("Cycle Through Modes")) - .children(KeyBinding::for_action_in( + .child(KeyBinding::for_action_in( &CycleModeSelector, &focus_handle, - window, cx, )), ) @@ -217,10 +216,9 @@ impl Render for ModeSelector { .gap_2() .justify_between() .child(Label::new("Toggle Mode Menu")) - .children(KeyBinding::for_action_in( + .child(KeyBinding::for_action_in( &ToggleProfileSelector, &focus_handle, - window, cx, )), ) diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs index 474125d69d3173db82510ab80261b93474e0386c..bd64756483032bee00ba8f56794bcb228bf91246 100644 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ b/crates/agent_ui/src/acp/model_selector_popover.rs @@ -77,14 +77,8 @@ impl Render for AcpModelSelectorPopover { .ml_0p5(), ) .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) + move |_window, cx| { + Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) }, gpui::Corner::BottomRight, cx, diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index ee280eb9a123e46ba5cf3b75cdeaf67c4b98b71c..aacae785a1f6ba727089c053588e6f0bc2ae24a2 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -423,8 +423,8 @@ impl AcpThreadHistory { .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .tooltip(move |window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, cx) }) .on_click( cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)), @@ -595,8 +595,8 @@ impl RenderOnce for AcpHistoryEntryElement { .shape(IconButtonShape::Square) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .tooltip(move |window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action("Delete", &RemoveSelectedThread, cx) }) .on_click({ let thread_view = self.thread_view.clone(); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index cb2e8be2701c2152ef889f7bdc9925f8014f9519..47ddd705bd653eb5c9635dd0307e9ebbc2638378 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2157,7 +2157,6 @@ impl AcpThreadView { options, entry_ix, tool_call.id.clone(), - window, cx, )) .into_any(), @@ -2558,7 +2557,6 @@ impl AcpThreadView { options: &[acp::PermissionOption], entry_ix: usize, tool_call_id: acp::ToolCallId, - window: &Window, cx: &Context, ) -> Div { let is_first = self.thread().is_some_and(|thread| { @@ -2615,7 +2613,7 @@ impl AcpThreadView { seen_kinds.push(option.kind); this.key_binding( - KeyBinding::for_action_in(action, &self.focus_handle, window, cx) + KeyBinding::for_action_in(action, &self.focus_handle, cx) .map(|kb| kb.size(rems_from_px(10.))), ) }) @@ -2796,12 +2794,11 @@ impl AcpThreadView { .icon_size(IconSize::Small) .icon_color(Color::Error) .label_size(LabelSize::Small) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Stop This Command", None, "Also possible by placing your cursor inside the terminal and using regular terminal bindings.", - window, cx, ) }) @@ -3102,7 +3099,7 @@ impl AcpThreadView { ) } - fn render_recent_history(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + fn render_recent_history(&self, cx: &mut Context) -> AnyElement { let render_history = self .agent .clone() @@ -3131,7 +3128,6 @@ impl AcpThreadView { KeyBinding::for_action_in( &OpenHistory, &self.focus_handle(cx), - window, cx, ) .map(|kb| kb.size(rems_from_px(12.))), @@ -3459,7 +3455,6 @@ impl AcpThreadView { &changed_buffers, self.edits_expanded, pending_edits, - window, cx, )) .when(self.edits_expanded, |parent| { @@ -3619,7 +3614,6 @@ impl AcpThreadView { changed_buffers: &BTreeMap, Entity>, expanded: bool, pending_edits: bool, - window: &mut Window, cx: &Context, ) -> Div { const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete."; @@ -3695,12 +3689,11 @@ impl AcpThreadView { .icon_size(IconSize::Small) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Review Changes", &OpenAgentDiff, &focus_handle, - window, cx, ) } @@ -3718,13 +3711,8 @@ impl AcpThreadView { this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) }) .key_binding( - KeyBinding::for_action_in( - &RejectAll, - &focus_handle.clone(), - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), + KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx) + .map(|kb| kb.size(rems_from_px(10.))), ) .on_click(cx.listener(move |this, _, window, cx| { this.reject_all(&RejectAll, window, cx); @@ -3738,7 +3726,7 @@ impl AcpThreadView { this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) }) .key_binding( - KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx) + KeyBinding::for_action_in(&KeepAll, &focus_handle, cx) .map(|kb| kb.size(rems_from_px(10.))), ) .on_click(cx.listener(move |this, _, window, cx| { @@ -3968,12 +3956,11 @@ impl AcpThreadView { .icon_size(IconSize::Small) .icon_color(Color::Muted) .tooltip({ - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( expand_tooltip, &ExpandMessageEditor, &focus_handle, - window, cx, ) } @@ -4198,8 +4185,8 @@ impl AcpThreadView { IconButton::new("stop-generation", IconName::Stop) .icon_color(Color::Error) .style(ButtonStyle::Tinted(ui::TintColor::Error)) - .tooltip(move |window, cx| { - Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx) }) .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) .into_any_element() @@ -4221,7 +4208,7 @@ impl AcpThreadView { this.icon_color(Color::Accent) } }) - .tooltip(move |window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, window, cx)) + .tooltip(move |_window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, cx)) .on_click(cx.listener(|this, _, window, cx| { this.send(window, cx); })) @@ -4282,15 +4269,14 @@ impl AcpThreadView { .icon_color(Color::Muted) .toggle_state(following) .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if following { - Tooltip::for_action(tooltip_label.clone(), &Follow, window, cx) + Tooltip::for_action(tooltip_label.clone(), &Follow, cx) } else { Tooltip::with_meta( tooltip_label.clone(), Some(&Follow), "Track the agent's location as it reads and edits files.", - window, cx, ) } @@ -5079,7 +5065,7 @@ impl AcpThreadView { } } - fn render_thread_error(&self, window: &mut Window, cx: &mut Context) -> Option
{ + fn render_thread_error(&self, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx), ThreadError::Refusal => self.render_refusal_error(cx), @@ -5090,9 +5076,7 @@ impl AcpThreadView { ThreadError::ModelRequestLimitReached(plan) => { self.render_model_request_limit_reached_error(*plan, cx) } - ThreadError::ToolUseLimitReached => { - self.render_tool_use_limit_reached_error(window, cx)? - } + ThreadError::ToolUseLimitReached => self.render_tool_use_limit_reached_error(cx)?, }; Some(div().child(content)) @@ -5283,11 +5267,7 @@ impl AcpThreadView { .dismiss_action(self.dismiss_error_button(cx)) } - fn render_tool_use_limit_reached_error( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Option { + fn render_tool_use_limit_reached_error(&self, cx: &mut Context) -> Option { let thread = self.as_native_thread(cx)?; let supports_burn_mode = thread .read(cx) @@ -5314,7 +5294,6 @@ impl AcpThreadView { KeyBinding::for_action_in( &ContinueWithBurnMode, &focus_handle, - window, cx, ) .map(|kb| kb.size(rems_from_px(10.))), @@ -5338,13 +5317,8 @@ impl AcpThreadView { .layer(ElevationIndex::ModalSurface) .label_size(LabelSize::Small) .key_binding( - KeyBinding::for_action_in( - &ContinueThread, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), + KeyBinding::for_action_in(&ContinueThread, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(10.))), ) .on_click(cx.listener(|this, _, _window, cx| { this.resume_chat(cx); @@ -5520,7 +5494,7 @@ impl Render for AcpThreadView { .into_any(), ThreadState::Loading { .. } => v_flex() .flex_1() - .child(self.render_recent_history(window, cx)) + .child(self.render_recent_history(cx)) .into_any(), ThreadState::LoadError(e) => v_flex() .flex_1() @@ -5551,8 +5525,7 @@ impl Render for AcpThreadView { .vertical_scrollbar_for(self.list_state.clone(), window, cx) .into_any() } else { - this.child(self.render_recent_history(window, cx)) - .into_any() + this.child(self.render_recent_history(cx)).into_any() } }), }) @@ -5576,7 +5549,7 @@ impl Render for AcpThreadView { Vec::::new() } }) - .children(self.render_thread_error(window, cx)) + .children(self.render_thread_error(cx)) .when_some( self.new_server_version_available.as_ref().filter(|_| { !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. }) 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 97e2cc3e8bac47df5105e94d52bd5bd21799f830..8f4fdeacf303c9869e903bde95326c80fba10126 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 @@ -431,7 +431,7 @@ impl Focusable for AddLlmProviderModal { impl ModalView for AddLlmProviderModal {} impl Render for AddLlmProviderModal { - fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context) -> impl IntoElement { + fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); div() @@ -484,7 +484,6 @@ impl Render for AddLlmProviderModal { KeyBinding::for_action_in( &menu::Cancel, &focus_handle, - window, cx, ) .map(|kb| kb.size(rems_from_px(12.))), @@ -499,7 +498,6 @@ impl Render for AddLlmProviderModal { KeyBinding::for_action_in( &menu::Confirm, &focus_handle, - window, cx, ) .map(|kb| kb.size(rems_from_px(12.))), diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index ce8e167dab3ed2e4d84c4afd747cb266740f1d42..88896f51086dc5f7d3eddb2fffef2fa3a7039c79 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -566,7 +566,7 @@ impl ConfigureContextServerModal { .into_any_element() } - fn render_modal_footer(&self, window: &mut Window, cx: &mut Context) -> ModalFooter { + fn render_modal_footer(&self, cx: &mut Context) -> ModalFooter { let focus_handle = self.focus_handle(cx); let is_connecting = matches!(self.state, State::Waiting); @@ -584,12 +584,11 @@ impl ConfigureContextServerModal { .icon_size(IconSize::Small) .tooltip({ let repository_url = repository_url.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::with_meta( "Open Repository", None, repository_url.clone(), - window, cx, ) } @@ -616,7 +615,7 @@ impl ConfigureContextServerModal { }, ) .key_binding( - KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx) + KeyBinding::for_action_in(&menu::Cancel, &focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click( @@ -634,7 +633,7 @@ impl ConfigureContextServerModal { ) .disabled(is_connecting) .key_binding( - KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx) + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click( @@ -709,7 +708,7 @@ impl Render for ConfigureContextServerModal { State::Error(error) => Self::render_modal_error(error.clone()), }), ) - .footer(self.render_modal_footer(window, cx)), + .footer(self.render_modal_footer(cx)), ) } } diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs index ad23d68d02c16c1379479684091f77a41c758a7a..e583bb7d5425ec4c6f233ac0eed67c358ccac98d 100644 --- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs +++ b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs @@ -352,10 +352,9 @@ impl ManageProfilesModal { .size(LabelSize::Small) .color(Color::Muted), ) - .children(KeyBinding::for_action_in( + .child(KeyBinding::for_action_in( &menu::Confirm, &self.focus_handle, - window, cx, )), ) @@ -649,14 +648,13 @@ impl ManageProfilesModal { ) .child(Label::new("Go Back")) .end_slot( - div().children( + div().child( KeyBinding::for_action_in( &menu::Cancel, &self.focus_handle, - window, cx, ) - .map(|kb| kb.size(rems_from_px(12.))), + .size(rems_from_px(12.)), ), ) .on_click({ @@ -700,14 +698,9 @@ impl Render for ManageProfilesModal { ) .child(Label::new("Go Back")) .end_slot( - div().children( - KeyBinding::for_action_in( - &menu::Cancel, - &self.focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), + div().child( + KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle, cx) + .size(rems_from_px(12.)), ), ) .on_click({ diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e463c0eb48816021bc2665d385804c926f1c63f4..dd11a3f2ccb88e38138d5c5f0e77805833a9a358 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -671,7 +671,7 @@ impl Item for AgentDiffPane { } impl Render for AgentDiffPane { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_empty = self.multibuffer.read(cx).is_empty(); let focus_handle = &self.focus_handle; @@ -704,7 +704,6 @@ impl Render for AgentDiffPane { .key_binding(KeyBinding::for_action_in( &ToggleFocus, &focus_handle.clone(), - window, cx, )) .on_click(|_event, window, cx| { @@ -721,14 +720,7 @@ fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControl let thread = thread.clone(); Arc::new( - move |row, - status: &DiffHunkStatus, - hunk_range, - is_created_file, - line_height, - editor: &Entity, - window: &mut Window, - cx: &mut App| { + move |row, status, hunk_range, is_created_file, line_height, editor, _, cx| { { render_diff_hunk_controls( row, @@ -738,7 +730,6 @@ fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControl line_height, &thread, editor, - window, cx, ) } @@ -754,7 +745,6 @@ fn render_diff_hunk_controls( line_height: Pixels, thread: &AgentDiffThread, editor: &Entity, - window: &mut Window, cx: &mut App, ) -> AnyElement { let editor = editor.clone(); @@ -777,13 +767,8 @@ fn render_diff_hunk_controls( Button::new(("reject", row as u64), "Reject") .disabled(is_created_file) .key_binding( - KeyBinding::for_action_in( - &Reject, - &editor.read(cx).focus_handle(cx), - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), + KeyBinding::for_action_in(&Reject, &editor.read(cx).focus_handle(cx), cx) + .map(|kb| kb.size(rems_from_px(12.))), ) .on_click({ let editor = editor.clone(); @@ -804,7 +789,7 @@ fn render_diff_hunk_controls( }), Button::new(("keep", row as u64), "Keep") .key_binding( - KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), window, cx) + KeyBinding::for_action_in(&Keep, &editor.read(cx).focus_handle(cx), cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click({ @@ -835,14 +820,8 @@ fn render_diff_hunk_controls( // .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - window, - cx, - ) + move |_window, cx| { + Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx) } }) .on_click({ @@ -871,12 +850,11 @@ fn render_diff_hunk_controls( // .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Previous Hunk", &GoToPreviousHunk, &focus_handle, - window, cx, ) } @@ -1041,7 +1019,7 @@ impl ToolbarItemView for AgentDiffToolbar { } impl Render for AgentDiffToolbar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let spinner_icon = div() .px_0p5() .id("generating") @@ -1116,7 +1094,6 @@ impl Render for AgentDiffToolbar { KeyBinding::for_action_in( &RejectAll, &editor_focus_handle, - window, cx, ) .map(|kb| kb.size(rems_from_px(12.))) @@ -1131,7 +1108,6 @@ impl Render for AgentDiffToolbar { KeyBinding::for_action_in( &KeepAll, &editor_focus_handle, - window, cx, ) .map(|kb| kb.size(rems_from_px(12.))) @@ -1208,13 +1184,8 @@ impl Render for AgentDiffToolbar { .child( Button::new("reject-all", "Reject All") .key_binding({ - KeyBinding::for_action_in( - &RejectAll, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))) + KeyBinding::for_action_in(&RejectAll, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))) }) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&RejectAll, window, cx) @@ -1223,13 +1194,8 @@ impl Render for AgentDiffToolbar { .child( Button::new("keep-all", "Keep All") .key_binding({ - KeyBinding::for_action_in( - &KeepAll, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))) + KeyBinding::for_action_in(&KeepAll, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))) }) .on_click(cx.listener(|this, _, window, cx| { this.dispatch_action(&KeepAll, window, cx) diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index c368ee73b32154550304898938372a278a8b1bba..df7d166064da20aa4bc958ebd6a9df806164eb7a 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -96,14 +96,8 @@ impl Render for AgentModelSelector { .color(color) .size(IconSize::XSmall), ), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) + move |_window, cx| { + Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) }, gpui::Corner::TopRight, cx, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 444ee22fd9098deb83614fdfc7dbd26d90783c34..02403b0e8d48ed2bee58c79e15d27d28ae2b49d3 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1595,12 +1595,11 @@ impl AgentPanel { .icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Agent Menu", &ToggleOptionsMenu, &focus_handle, - window, cx, ) } @@ -1691,12 +1690,11 @@ impl AgentPanel { .trigger_with_tooltip( IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small), { - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Recent Threads", &ToggleNavigationMenu, &focus_handle, - window, cx, ) } @@ -1730,8 +1728,8 @@ impl AgentPanel { this.go_back(&workspace::GoBack, window, cx); })) .tooltip({ - move |window, cx| { - Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, window, cx) + move |_window, cx| { + Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx) } }) } @@ -1752,12 +1750,11 @@ impl AgentPanel { IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "New…", &ToggleNewThreadMenu, &focus_handle, - window, cx, ) } @@ -2003,14 +2000,8 @@ impl AgentPanel { .when_some(self.selected_agent.icon(), |this, icon| { this.px(DynamicSpacing::Base02.rems(cx)) .child(Icon::new(icon).color(Color::Muted)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - selected_agent_label.clone(), - None, - "Selected Agent", - window, - cx, - ) + .tooltip(move |_window, cx| { + Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx) }) }) .into_any_element(); @@ -2186,7 +2177,6 @@ impl AgentPanel { border_bottom: bool, configuration_error: &ConfigurationError, focus_handle: &FocusHandle, - window: &mut Window, cx: &mut App, ) -> impl IntoElement { let zed_provider_configured = AgentSettings::get_global(cx) @@ -2235,7 +2225,7 @@ impl AgentPanel { .style(ButtonStyle::Tinted(ui::TintColor::Warning)) .label_size(LabelSize::Small) .key_binding( - KeyBinding::for_action_in(&OpenSettings, focus_handle, window, cx) + KeyBinding::for_action_in(&OpenSettings, focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_event, window, cx| { @@ -2453,7 +2443,6 @@ impl Render for AgentPanel { true, err, &self.focus_handle(cx), - window, cx, )) } else { diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 1f40da3d945df5f066289932b83065dc33d8e169..3eaf59aba39cbaef12e7a4079209956e0e8bed17 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -483,12 +483,11 @@ impl Render for ContextStrip { .style(ui::ButtonStyle::Filled), { let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Add Context", &ToggleContextPicker, &focus_handle, - window, cx, ) } @@ -558,12 +557,11 @@ impl Render for ContextStrip { .icon_size(IconSize::Small) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Remove All Context", &RemoveAllContext, &focus_handle, - window, cx, ) } diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 70d6009e466e3e2f6ba3cd65076f77f7d12b22e0..89bfd50e37e8ea681e70fadd78cbbd047f7258cb 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -468,12 +468,11 @@ impl PromptEditor { IconButton::new("stop", IconName::Stop) .icon_color(Color::Error) .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( mode.tooltip_interrupt(), Some(&menu::Cancel), "Changes won't be discarded", - window, cx, ) }) @@ -487,12 +486,11 @@ impl PromptEditor { IconButton::new("restart", IconName::RotateCw) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( mode.tooltip_restart(), Some(&menu::Confirm), "Changes will be discarded", - window, cx, ) }) @@ -505,8 +503,8 @@ impl PromptEditor { let accept = IconButton::new("accept", IconName::Check) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx) }) .on_click(cx.listener(|_, _, _, cx| { cx.emit(PromptEditorEvent::ConfirmRequested { execute: false }); @@ -519,11 +517,10 @@ impl PromptEditor { IconButton::new("confirm", IconName::PlayFilled) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::for_action( "Execute Generated Command", &menu::SecondaryConfirm, - window, cx, ) }) @@ -615,13 +612,12 @@ impl PromptEditor { .shape(IconButtonShape::Square) .tooltip({ let focus_handle = self.editor.focus_handle(cx); - move |window, cx| { + move |_window, cx| { cx.new(|cx| { let mut tooltip = Tooltip::new("Previous Alternative").key_binding( KeyBinding::for_action_in( &CyclePreviousInlineAssist, &focus_handle, - window, cx, ), ); @@ -657,13 +653,12 @@ impl PromptEditor { .shape(IconButtonShape::Square) .tooltip({ let focus_handle = self.editor.focus_handle(cx); - move |window, cx| { + move |_window, cx| { cx.new(|cx| { let mut tooltip = Tooltip::new("Next Alternative").key_binding( KeyBinding::for_action_in( &CycleNextInlineAssist, &focus_handle, - window, cx, ), ); diff --git a/crates/agent_ui/src/profile_selector.rs b/crates/agent_ui/src/profile_selector.rs index ef9e1e691753a4a005bd1bc91b60a75c7716132f..2f9fe19eb33667d6ca6bb2f5502fbd1c9f094e9c 100644 --- a/crates/agent_ui/src/profile_selector.rs +++ b/crates/agent_ui/src/profile_selector.rs @@ -162,12 +162,11 @@ impl Render for ProfileSelector { PickerPopoverMenu::new( picker, trigger_button, - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Profile Menu", &ToggleProfileSelector, &focus_handle, - window, cx, ) }, diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 408aecccfa7fa71aaf15e4c085ad31dce8a1d922..5aa6f1f6d9405dc7556cb87c82d5300308f059d1 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1084,12 +1084,11 @@ impl TextThreadEditor { .child(label) .children(spinner), ) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Toggle message role", None, "Available roles: You (User), Agent, System", - window, cx, ) }) @@ -1125,12 +1124,11 @@ impl TextThreadEditor { .size(IconSize::XSmall) .color(Color::Hint), ) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Context Cached", None, "Large messages cached to optimize performance", - window, cx, ) }) @@ -1946,7 +1944,7 @@ impl TextThreadEditor { }) .layer(ElevationIndex::ModalSurface) .key_binding( - KeyBinding::for_action_in(&Assist, &focus_handle, window, cx) + KeyBinding::for_action_in(&Assist, &focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(move |_event, window, cx| { @@ -1981,14 +1979,8 @@ impl TextThreadEditor { .icon_color(Color::Muted) .selected_icon_color(Color::Accent) .selected_style(ButtonStyle::Filled), - move |window, cx| { - Tooltip::with_meta( - "Add Context", - None, - "Type / to insert via keyboard", - window, - cx, - ) + move |_window, cx| { + Tooltip::with_meta("Add Context", None, "Type / to insert via keyboard", cx) }, ) } @@ -2077,14 +2069,8 @@ impl TextThreadEditor { ) .child(Icon::new(icon).color(color).size(IconSize::XSmall)), ), - move |window, cx| { - Tooltip::for_action_in( - "Change Model", - &ToggleModelSelector, - &focus_handle, - window, - cx, - ) + move |_window, cx| { + Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) }, gpui::Corner::BottomRight, cx, diff --git a/crates/agent_ui/src/ui/burn_mode_tooltip.rs b/crates/agent_ui/src/ui/burn_mode_tooltip.rs index f95dc1250e36bba388452ce11e6ec783e44248e1..ccd7d4bf3190c0d879327dc0ea152994c4a33163 100644 --- a/crates/agent_ui/src/ui/burn_mode_tooltip.rs +++ b/crates/agent_ui/src/ui/burn_mode_tooltip.rs @@ -18,7 +18,7 @@ impl BurnModeTooltip { } impl Render for BurnModeTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let (icon, color) = if self.selected { (IconName::ZedBurnModeOn, Color::Error) } else { @@ -45,8 +45,7 @@ impl Render for BurnModeTooltip { .child(Label::new("Burn Mode")) .when(self.selected, |title| title.child(turned_on)); - let keybinding = KeyBinding::for_action(&ToggleBurnMode, window, cx) - .map(|kb| kb.size(rems_from_px(12.))); + let keybinding = KeyBinding::for_action(&ToggleBurnMode, cx).size(rems_from_px(12.)); tooltip_container(cx, |this, _| { this @@ -54,7 +53,7 @@ impl Render for BurnModeTooltip { h_flex() .justify_between() .child(title) - .children(keybinding) + .child(keybinding) ) .child( div() diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index ea1f1136794e1ac3a23e2caeaa3006acccf9bce0..43d3799d697e28d43c71fc6e6e77cc058eaec5b2 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -244,8 +244,8 @@ impl RenderOnce for ContextPill { .truncate(), ), ) - .tooltip(|window, cx| { - Tooltip::with_meta("Suggested Context", None, "Click to add it", window, cx) + .tooltip(|_window, cx| { + Tooltip::with_meta("Suggested Context", None, "Click to add it", cx) }) .when_some(on_click.as_ref(), |element, on_click| { let on_click = on_click.clone(); diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index a6b27476fe36b1143103e1acd035bda6cda15132..08c0915c58ae50741238574cec5b6f2474d06eb8 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -119,21 +119,19 @@ impl Render for Breadcrumbs { } } }) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if let Some(editor) = editor.upgrade() { let focus_handle = editor.read(cx).focus_handle(cx); Tooltip::for_action_in( "Show Symbol Outline", &zed_actions::outline::ToggleOutline, &focus_handle, - window, cx, ) } else { Tooltip::for_action( "Show Symbol Outline", &zed_actions::outline::ToggleOutline, - window, cx, ) } diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index bab4234f14ed3bba6b408efcc0170f7e15efaf50..99203bc867ff7da9e140bc4a886e291252a5153d 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -738,19 +738,17 @@ impl Render for NotificationToast { .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify())) .child( IconButton::new(close_id, close_icon) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if suppress { Tooltip::for_action( "Suppress.\nClose with click.", &workspace::SuppressNotification, - window, cx, ) } else { Tooltip::for_action( "Close.\nSuppress with shift-click", &menu::Cancel, - window, cx, ) } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index f9ed9ec6faf6b1cefbd9159a06e145b32c752c1f..4b883d890b3ca5b54459bd0ead3322acfe5b6f41 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -443,7 +443,7 @@ impl PickerDelegate for CommandPaletteDelegate { &self, ix: usize, selected: bool, - window: &mut Window, + _: &mut Window, cx: &mut Context>, ) -> Option { let matching_command = self.matches.get(ix)?; @@ -462,10 +462,9 @@ impl PickerDelegate for CommandPaletteDelegate { command.name.clone(), matching_command.positions.clone(), )) - .children(KeyBinding::for_action_in( + .child(KeyBinding::for_action_in( &*command.action, &self.previous_focus_handle, - window, cx, )), ), diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 11d8683209eeac56c7f5a156c367a627e27ad459..12c303675aed7fe6c8d7f7dc52d1f9e7d1af1966 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -616,12 +616,11 @@ impl DebugPanel { }) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Start Debug Session", &crate::Start, &focus_handle, - window, cx, ) } @@ -694,12 +693,11 @@ impl DebugPanel { )) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Pause Program", &Pause, &focus_handle, - window, cx, ) } @@ -719,12 +717,11 @@ impl DebugPanel { .disabled(thread_status != ThreadStatus::Stopped) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Continue Program", &Continue, &focus_handle, - window, cx, ) } @@ -744,12 +741,11 @@ impl DebugPanel { .disabled(thread_status != ThreadStatus::Stopped) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Step Over", &StepOver, &focus_handle, - window, cx, ) } @@ -770,12 +766,11 @@ impl DebugPanel { .disabled(thread_status != ThreadStatus::Stopped) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Step In", &StepInto, &focus_handle, - window, cx, ) } @@ -793,12 +788,11 @@ impl DebugPanel { .disabled(thread_status != ThreadStatus::Stopped) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Step Out", &StepOut, &focus_handle, - window, cx, ) } @@ -816,12 +810,11 @@ impl DebugPanel { )) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Rerun Session", &RerunSession, &focus_handle, - window, cx, ) } @@ -861,12 +854,11 @@ impl DebugPanel { } else { "Terminate All Threads" }; - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( label, &Stop, &focus_handle, - window, cx, ) } @@ -893,12 +885,11 @@ impl DebugPanel { )) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Detach", &Detach, &focus_handle, - window, cx, ) } diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 56c4a690325a0f5d8387fa76c1121206ff8f05fb..b56c0a5d3b46c4a6b6b43bbf843178c85f5c8d9f 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -745,22 +745,15 @@ impl Render for NewProcessModal { == 0; let secondary_action = menu::SecondaryConfirm.boxed_clone(); container - .child(div().children( - KeyBinding::for_action(&*secondary_action, window, cx).map( - |keybind| { - Button::new("edit-attach-task", "Edit in debug.json") - .label_size(LabelSize::Small) - .key_binding(keybind) - .on_click(move |_, window, cx| { - window.dispatch_action( - secondary_action.boxed_clone(), - cx, - ) - }) - .disabled(disabled) - }, - ), - )) + .child(div().child({ + Button::new("edit-attach-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action(&*secondary_action, cx)) + .on_click(move |_, window, cx| { + window.dispatch_action(secondary_action.boxed_clone(), cx) + }) + .disabled(disabled) + })) .child( h_flex() .child(div().child(self.adapter_drop_down_menu(window, cx))), @@ -1447,56 +1440,48 @@ impl PickerDelegate for DebugDelegate { .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) - .children({ + .child({ let action = menu::SecondaryConfirm.boxed_clone(); if self.matches.is_empty() { - Some( - Button::new("edit-debug-json", "Edit debug.json") - .label_size(LabelSize::Small) - .on_click(cx.listener(|_picker, _, window, cx| { - window.dispatch_action( - zed_actions::OpenProjectDebugTasks.boxed_clone(), - cx, - ); - cx.emit(DismissEvent); - })), - ) + Button::new("edit-debug-json", "Edit debug.json") + .label_size(LabelSize::Small) + .on_click(cx.listener(|_picker, _, window, cx| { + window.dispatch_action( + zed_actions::OpenProjectDebugTasks.boxed_clone(), + cx, + ); + cx.emit(DismissEvent); + })) } else { - KeyBinding::for_action(&*action, window, cx).map(|keybind| { - Button::new("edit-debug-task", "Edit in debug.json") - .label_size(LabelSize::Small) - .key_binding(keybind) - .on_click(move |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx) - }) - }) + Button::new("edit-debug-task", "Edit in debug.json") + .label_size(LabelSize::Small) + .key_binding(KeyBinding::for_action(&*action, cx)) + .on_click(move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) } }) .map(|this| { if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { let action = picker::ConfirmInput { secondary: false }.boxed_clone(); - this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { + this.child({ Button::new("launch-custom", "Launch Custom") - .key_binding(keybind) + .key_binding(KeyBinding::for_action(&*action, cx)) .on_click(move |_, window, cx| { window.dispatch_action(action.boxed_clone(), cx) }) - })) + }) } else { - this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map( - |keybind| { - let is_recent_selected = - self.divider_index >= Some(self.selected_index); - let run_entry_label = - if is_recent_selected { "Rerun" } else { "Spawn" }; - - Button::new("spawn", run_entry_label) - .key_binding(keybind) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - }) - }, - )) + this.child({ + let is_recent_selected = self.divider_index >= Some(self.selected_index); + let run_entry_label = if is_recent_selected { "Rerun" } else { "Spawn" }; + + Button::new("spawn", run_entry_label) + .key_binding(KeyBinding::for_action(&menu::Confirm, cx)) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx); + }) + }) } }); Some(footer.into_any_element()) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 7340f2591623fcf8b61916fc3aea3337bcad3149..0e21ef1268412418c381fc14617a917f9529834d 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -566,14 +566,13 @@ pub(crate) fn new_debugger_pane( })) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { let zoomed_text = if zoomed { "Minimize" } else { "Expand" }; Tooltip::for_action_in( zoomed_text, &ToggleExpandItem, &focus_handle, - window, cx, ) } diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index cec906e293485f3ab7b3685f65834d2b143ef8e2..c9f2a58dae28c2e41e49aecc847857ca6191c0eb 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -607,13 +607,12 @@ impl BreakpointList { .when_some(toggle_label, |this, (label, meta)| { this.tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::with_meta_in( label, Some(&ToggleEnableBreakpoint), meta, &focus_handle, - window, cx, ) } @@ -634,13 +633,12 @@ impl BreakpointList { .when_some(remove_breakpoint_tooltip, |this, tooltip| { this.tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::with_meta_in( "Remove Breakpoint", Some(&UnsetBreakpoint), tooltip, &focus_handle, - window, cx, ) } @@ -819,7 +817,7 @@ impl LineBreakpoint { ) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( if is_enabled { "Disable Breakpoint" @@ -828,7 +826,6 @@ impl LineBreakpoint { }, &ToggleEnableBreakpoint, &focus_handle, - window, cx, ) } @@ -980,7 +977,7 @@ impl DataBreakpoint { ) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( if is_enabled { "Disable Data Breakpoint" @@ -989,7 +986,6 @@ impl DataBreakpoint { }, &ToggleEnableBreakpoint, &focus_handle, - window, cx, ) } @@ -1085,7 +1081,7 @@ impl ExceptionBreakpoint { ) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( if is_enabled { "Disable Exception Breakpoint" @@ -1094,7 +1090,6 @@ impl ExceptionBreakpoint { }, &ToggleEnableBreakpoint, &focus_handle, - window, cx, ) } @@ -1402,12 +1397,11 @@ impl RenderOnce for BreakpointOptionsStrip { .disabled(!supports_logs) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit.", - window, cx, ) }), @@ -1438,12 +1432,11 @@ impl RenderOnce for BreakpointOptionsStrip { .disabled(!supports_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition)) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met.", - window, cx, ) }), @@ -1474,12 +1467,11 @@ impl RenderOnce for BreakpointOptionsStrip { .disabled(!supports_hit_condition) .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition)) .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", - window, cx, ) }), diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 29cdf9a8067c099a8454ad21b459853cf3982f1a..2d01a325a2b0056bfbf42e519a79a4ec199c4a9d 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -484,12 +484,11 @@ impl Render for Console { .tooltip({ let query_focus_handle = query_focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Evaluate", &Confirm, &query_focus_handle, - window, cx, ) } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index 309b58e7de40f527e4ab96f8aacd668810aede64..3fc7e8ce392b5ea3982a168fcc8f6dcfad1f7313 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -872,8 +872,8 @@ impl StackFrameList { "filter-by-visible-worktree-stack-frame-list", IconName::ListFilter, ) - .tooltip(move |window, cx| { - Tooltip::for_action(tooltip_title, &ToggleUserFrames, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action(tooltip_title, &ToggleUserFrames, cx) }) .toggle_state(self.list_filter == StackFrameFilter::OnlyUserFrames) .icon_size(IconSize::Small) diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index aa8cb143ac71328920bb1a41933b456491647a03..f2b79523fe3d7329073ad618a9d5c5d219a32f3c 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1306,14 +1306,8 @@ impl VariableList { .ok(); } }) - .tooltip(move |window, cx| { - Tooltip::for_action_in( - "Remove Watch", - &RemoveWatch, - &focus_handle, - window, - cx, - ) + .tooltip(move |_window, cx| { + Tooltip::for_action_in("Remove Watch", &RemoveWatch, &focus_handle, cx) }) .icon_size(ui::IconSize::Indicator), ), diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index d3947b9b5d56b3ae71c3af7c8bf829676041123b..413bad5c0d696bfcba92a1127789c9e7c31edc30 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -67,11 +67,10 @@ impl Render for DiagnosticIndicator { Some( Button::new("diagnostic_message", SharedString::new(message)) .label_size(LabelSize::Small) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::for_action( "Next Diagnostic", &editor::actions::GoToDiagnostic::default(), - window, cx, ) }) @@ -87,8 +86,8 @@ impl Render for DiagnosticIndicator { .child( ButtonLike::new("diagnostic-indicator") .child(diagnostic_indicator) - .tooltip(|window, cx| { - Tooltip::for_action("Project Diagnostics", &Deploy, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action("Project Diagnostics", &Deploy, cx) }) .on_click(cx.listener(|this, _, window, cx| { if let Some(workspace) = this.workspace.upgrade() { diff --git a/crates/edit_prediction_button/src/edit_prediction_button.rs b/crates/edit_prediction_button/src/edit_prediction_button.rs index 95ffa2f0e66713170d4fb5d63493daf07a7a555d..8b9bfc1c50092b65892cfcee9f4da1aeb2a0993e 100644 --- a/crates/edit_prediction_button/src/edit_prediction_button.rs +++ b/crates/edit_prediction_button/src/edit_prediction_button.rs @@ -123,8 +123,8 @@ impl Render for EditPredictionButton { }); } })) - .tooltip(|window, cx| { - Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx) + .tooltip(|_window, cx| { + Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx) }), ); } @@ -146,9 +146,7 @@ impl Render for EditPredictionButton { .anchor(Corner::BottomRight) .trigger_with_tooltip( IconButton::new("copilot-icon", icon), - |window, cx| { - Tooltip::for_action("GitHub Copilot", &ToggleMenu, window, cx) - }, + |_window, cx| Tooltip::for_action("GitHub Copilot", &ToggleMenu, cx), ) .with_handle(self.popover_menu_handle.clone()), ) @@ -220,12 +218,7 @@ impl Render for EditPredictionButton { IconButton::new("supermaven-icon", icon), move |window, cx| { if has_menu { - Tooltip::for_action( - tooltip_text.clone(), - &ToggleMenu, - window, - cx, - ) + Tooltip::for_action(tooltip_text.clone(), &ToggleMenu, cx) } else { Tooltip::text(tooltip_text.clone())(window, cx) } @@ -288,9 +281,7 @@ impl Render for EditPredictionButton { cx.theme().colors().status_bar_background, )) }), - move |window, cx| { - Tooltip::for_action("Codestral", &ToggleMenu, window, cx) - }, + move |_window, cx| Tooltip::for_action("Codestral", &ToggleMenu, cx), ) .with_handle(self.popover_menu_handle.clone()), ) @@ -317,14 +308,8 @@ impl Render for EditPredictionButton { .shape(IconButtonShape::Square) .indicator(Indicator::dot().color(Color::Muted)) .indicator_border_color(Some(cx.theme().colors().status_bar_background)) - .tooltip(move |window, cx| { - Tooltip::with_meta( - "Edit Predictions", - None, - tooltip_meta, - window, - cx, - ) + .tooltip(move |_window, cx| { + Tooltip::with_meta("Edit Predictions", None, tooltip_meta, cx) }) .on_click(cx.listener(move |_, _, window, cx| { telemetry::event!( @@ -365,16 +350,15 @@ impl Render for EditPredictionButton { }, ) .when(!self.popover_menu_handle.is_deployed(), |element| { - element.tooltip(move |window, cx| { + element.tooltip(move |_window, cx| { if enabled { if show_editor_predictions { - Tooltip::for_action("Edit Prediction", &ToggleMenu, window, cx) + Tooltip::for_action("Edit Prediction", &ToggleMenu, cx) } else { Tooltip::with_meta( "Edit Prediction", Some(&ToggleMenu), "Hidden For This File", - window, cx, ) } @@ -383,7 +367,6 @@ impl Render for EditPredictionButton { "Edit Prediction", Some(&ToggleMenu), "Disabled For This File", - window, cx, ) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fb62438cebb9e7baf9f8a45a439465f34b921bce..b9075e47e4681809228ee827db5805a7b402f921 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6389,7 +6389,7 @@ impl Editor { .when(show_tooltip, |this| { this.tooltip({ let focus_handle = self.focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Code Actions", &ToggleCodeActions { @@ -6397,7 +6397,6 @@ impl Editor { quick_launch: false, }, &focus_handle, - window, cx, ) } @@ -8262,13 +8261,12 @@ impl Editor { cx, ); })) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta_in( primary_action_text, Some(&ToggleBreakpoint), meta.clone(), &focus_handle, - window, cx, ) }) @@ -24588,12 +24586,11 @@ fn render_diff_hunk_controls( .alpha(if status.is_pending() { 0.66 } else { 1.0 }) .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Stage Hunk", &::git::ToggleStaged, &focus_handle, - window, cx, ) } @@ -24615,12 +24612,11 @@ fn render_diff_hunk_controls( .alpha(if status.is_pending() { 0.66 } else { 1.0 }) .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Unstage Hunk", &::git::ToggleStaged, &focus_handle, - window, cx, ) } @@ -24642,14 +24638,8 @@ fn render_diff_hunk_controls( Button::new(("restore", row as u64), "Restore") .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Restore Hunk", - &::git::Restore, - &focus_handle, - window, - cx, - ) + move |_window, cx| { + Tooltip::for_action_in("Restore Hunk", &::git::Restore, &focus_handle, cx) } }) .on_click({ @@ -24674,14 +24664,8 @@ fn render_diff_hunk_controls( // .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - window, - cx, - ) + move |_window, cx| { + Tooltip::for_action_in("Next Hunk", &GoToHunk, &focus_handle, cx) } }) .on_click({ @@ -24710,12 +24694,11 @@ fn render_diff_hunk_controls( // .disabled(!has_multiple_hunks) .tooltip({ let focus_handle = editor.focus_handle(cx); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Previous Hunk", &GoToPreviousHunk, &focus_handle, - window, cx, ) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 944715a0dfa3747bcece23a643a144f891687b53..b5c1fecbea003d15e336738d7e68e1c4ec59f14f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3910,7 +3910,7 @@ impl EditorElement { .children(toggle_chevron_icon) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::with_meta_in( "Toggle Excerpt Fold", Some(&ToggleFold), @@ -3923,7 +3923,6 @@ impl EditorElement { ) ), &focus_handle, - window, cx, ) } @@ -4024,15 +4023,11 @@ impl EditorElement { .id("jump-to-file-button") .gap_2p5() .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), + .child(KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + cx, + )), ) }, ) diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index d32c0412e3707de2fb20be96a4472ec82d59726a..a8a03d3e5b3f7f72d58f3a12d7b265832f1b2e10 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -370,17 +370,15 @@ impl ProposedChangesEditorToolbar { } impl Render for ProposedChangesEditorToolbar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let button_like = ButtonLike::new("apply-changes").child(Label::new("Apply All")); match &self.current_editor { Some(editor) => { let focus_handle = editor.focus_handle(cx); - let keybinding = - KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, window, cx) - .map(|binding| binding.into_any_element()); + let keybinding = KeyBinding::for_action_in(&ApplyAllDiffHunks, &focus_handle, cx); - button_like.children(keybinding).on_click({ + button_like.child(keybinding).on_click({ move |_event, window, cx| { focus_handle.dispatch_action(&ApplyAllDiffHunks, window, cx) } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index 6abd3e48880a59f3ce74511013bcd048ad5a2a51..8d74638e4c2aaf356ffabdeef717b9b105487ee3 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -396,13 +396,8 @@ impl SignatureHelpPopover { .shape(IconButtonShape::Square) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) - .tooltip(move |window, cx| { - ui::Tooltip::for_action( - "Previous Signature", - &crate::SignatureHelpPrevious, - window, - cx, - ) + .tooltip(move |_window, cx| { + ui::Tooltip::for_action("Previous Signature", &crate::SignatureHelpPrevious, cx) }) .on_click(cx.listener(|editor, _, window, cx| { editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx); @@ -412,8 +407,8 @@ impl SignatureHelpPopover { .shape(IconButtonShape::Square) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) - .tooltip(move |window, cx| { - ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx) + .tooltip(move |_window, cx| { + ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, cx) }) .on_click(cx.listener(|editor, _, window, cx| { editor.signature_help_next(&crate::SignatureHelpNext, window, cx); diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 6ea815d5663f40bb66ca764533a6e79b53c6f712..d78d789b9b0c8041975da6337620b840896a61f6 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1663,11 +1663,7 @@ impl PickerDelegate for FileFinderDelegate { ) } - fn render_footer( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { + fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { let focus_handle = self.focus_handle.clone(); Some( @@ -1696,12 +1692,11 @@ impl PickerDelegate for FileFinderDelegate { }), { let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Filter Options", &ToggleFilterMenu, &focus_handle, - window, cx, ) } @@ -1751,14 +1746,13 @@ impl PickerDelegate for FileFinderDelegate { ButtonLike::new("split-trigger") .child(Label::new("Split…")) .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .children( + .child( KeyBinding::for_action_in( &ToggleSplitMenu, &focus_handle, - window, cx, ) - .map(|kb| kb.size(rems_from_px(12.))), + .size(rems_from_px(12.)), ), ) .menu({ @@ -1790,13 +1784,8 @@ impl PickerDelegate for FileFinderDelegate { .child( Button::new("open-selection", "Open") .key_binding( - KeyBinding::for_action_in( - &menu::Confirm, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { window.dispatch_action(menu::Confirm.boxed_clone(), cx) diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index cf8b03d1fd249c978de9e6bbd824e9491c5d24e1..662e1cc1d712757eb2f31b11a0d6340576c29317 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -466,11 +466,10 @@ impl PickerDelegate for BranchListDelegate { this.delegate.set_selected_index(ix, window, cx); this.delegate.confirm(true, window, cx); })) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action( format!("Create branch based off default: {default_branch}"), &menu::SecondaryConfirm, - window, cx, ) }), diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 6c93e03e4bf4009a622206195c12b49bbedf4038..45b1563dca0ceed5ed2ac488026fe94084050780 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -327,7 +327,7 @@ impl CommitModal { .anchor(Corner::TopRight) } - pub fn render_footer(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + pub fn render_footer(&self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let ( can_commit, tooltip, @@ -388,7 +388,7 @@ impl CommitModal { }); let focus_handle = self.focus_handle(cx); - let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, window, cx).map(|close_kb| { + let close_kb_hint = ui::KeyBinding::for_action(&menu::Cancel, cx).map(|close_kb| { KeybindingHint::new(close_kb, cx.theme().colors().editor_background).suffix("Cancel") }); @@ -423,7 +423,7 @@ impl CommitModal { .flex_none() .px_1() .gap_4() - .children(close_kb_hint) + .child(close_kb_hint) .child(SplitButton::new( ui::ButtonLike::new_rounded_left(ElementId::Name( format!("split-button-left-{}", commit_label).into(), @@ -452,7 +452,7 @@ impl CommitModal { .disabled(!can_commit) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { if can_commit { Tooltip::with_meta_in( tooltip, @@ -467,7 +467,6 @@ impl CommitModal { if is_signoff_enabled { " --signoff" } else { "" } ), &focus_handle.clone(), - window, cx, ) } else { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 2678d96041b4fb1123388bbd61db904a924fc6c8..2bd0fea7018a99a943efe91becd7c22962b27fb4 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -3091,13 +3091,12 @@ impl GitPanel { IconButton::new("generate-commit-message", IconName::AiEdit) .shape(ui::IconButtonShape::Square) .icon_color(Color::Muted) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if can_commit { Tooltip::for_action_in( "Generate Commit Message", &git::GenerateCommitMessage, &editor_focus_handle, - window, cx, ) } else { @@ -3459,12 +3458,11 @@ impl GitPanel { panel_icon_button("expand-commit-editor", IconName::Maximize) .icon_size(IconSize::Small) .size(ui::ButtonSize::Default) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action_in( "Open Commit Modal", &git::ExpandCommitEditor, &expand_tooltip_focus_handle, - window, cx, ) }) @@ -3526,7 +3524,7 @@ impl GitPanel { .disabled(!can_commit || self.modal_open) .tooltip({ let handle = commit_tooltip_focus_handle.clone(); - move |window, cx| { + move |_window, cx| { if can_commit { Tooltip::with_meta_in( tooltip, @@ -3537,7 +3535,6 @@ impl GitPanel { if signoff { " --signoff" } else { "" } ), &handle.clone(), - window, cx, ) } else { @@ -3640,7 +3637,7 @@ impl GitPanel { panel_icon_button("undo", IconName::Undo) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Uncommit", Some(&git::Uncommit), @@ -3649,7 +3646,6 @@ impl GitPanel { } else { "git reset HEAD^" }, - window, cx, ) }) @@ -4120,13 +4116,13 @@ impl GitPanel { .ok(); } }) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { let is_staged = entry_staging.is_fully_staged(); let action = if is_staged { "Unstage" } else { "Stage" }; let tooltip_name = action.to_string(); - Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx) + Tooltip::for_action(tooltip_name, &ToggleStaged, cx) }), ), ) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 303e23c959557efe859cb069c1e41ff8352923fe..919cdf154d438e8ee5b38422032aa150edc5dd34 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -435,13 +435,12 @@ mod remote_button { move |_, window, cx| { window.dispatch_action(Box::new(git::Fetch), cx); }, - move |window, cx| { + move |_window, cx| { git_action_tooltip( "Fetch updates from remote", &git::Fetch, "git fetch", keybinding_target.clone(), - window, cx, ) }, @@ -463,13 +462,12 @@ mod remote_button { move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, - move |window, cx| { + move |_window, cx| { git_action_tooltip( "Push committed changes to remote", &git::Push, "git push", keybinding_target.clone(), - window, cx, ) }, @@ -492,13 +490,12 @@ mod remote_button { move |_, window, cx| { window.dispatch_action(Box::new(git::Pull), cx); }, - move |window, cx| { + move |_window, cx| { git_action_tooltip( "Pull", &git::Pull, "git pull", keybinding_target.clone(), - window, cx, ) }, @@ -519,13 +516,12 @@ mod remote_button { move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, - move |window, cx| { + move |_window, cx| { git_action_tooltip( "Publish branch to remote", &git::Push, "git push --set-upstream", keybinding_target.clone(), - window, cx, ) }, @@ -546,13 +542,12 @@ mod remote_button { move |_, window, cx| { window.dispatch_action(Box::new(git::Push), cx); }, - move |window, cx| { + move |_window, cx| { git_action_tooltip( "Re-publish branch to remote", &git::Push, "git push --set-upstream", keybinding_target.clone(), - window, cx, ) }, @@ -564,16 +559,15 @@ mod remote_button { action: &dyn Action, command: impl Into, focus_handle: Option, - window: &mut Window, cx: &mut App, ) -> AnyView { let label = label.into(); let command = command.into(); if let Some(handle) = focus_handle { - Tooltip::with_meta_in(label, Some(action), command, &handle, window, cx) + Tooltip::with_meta_in(label, Some(action), command, &handle, cx) } else { - Tooltip::with_meta(label, Some(action), command, window, cx) + Tooltip::with_meta(label, Some(action), command, cx) } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index f4ee0b8934ae63433eb5d94d52e213b4458c76cd..b073b9dc3da17c10d9df1fa99c9bec53575818df 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -714,7 +714,7 @@ impl Item for ProjectDiff { } impl Render for ProjectDiff { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let is_empty = self.multibuffer.read(cx).is_empty(); div() @@ -759,7 +759,6 @@ impl Render for ProjectDiff { .key_binding(KeyBinding::for_action_in( &CloseActiveItem::default(), &keybinding_focus_handle, - window, cx, )) .on_click(move |_, window, cx| { diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 3f159035a0ada5a79d26dd0d1d8222678aed23b3..a8e725eefcafb2f3742b23adfdd75ab129052773 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -523,11 +523,7 @@ impl PickerDelegate for StashListDelegate { Some("No stashes found".into()) } - fn render_footer( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { + fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { let focus_handle = self.focus_handle.clone(); Some( @@ -541,7 +537,7 @@ impl PickerDelegate for StashListDelegate { .child( Button::new("apply-stash", "Apply") .key_binding( - KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx) + KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx) .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { @@ -551,13 +547,8 @@ impl PickerDelegate for StashListDelegate { .child( Button::new("pop-stash", "Pop") .key_binding( - KeyBinding::for_action_in( - &menu::SecondaryConfirm, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), + KeyBinding::for_action_in(&menu::SecondaryConfirm, &focus_handle, cx) + .map(|kb| kb.size(rems_from_px(12.))), ) .on_click(|_, window, cx| { window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) @@ -569,7 +560,6 @@ impl PickerDelegate for StashListDelegate { KeyBinding::for_action_in( &stash_picker::DropStashItem, &focus_handle, - window, cx, ) .map(|kb| kb.size(rems_from_px(12.))), diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 151f8be77fb3649d1feaf09cfe73323ae7dc56e3..5c10537e2869e0ca51e3178598f55c1589ceacd7 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -238,18 +238,16 @@ impl Render for CursorPosition { }); } })) - .tooltip(move |window, cx| match context.as_ref() { + .tooltip(move |_window, cx| match context.as_ref() { Some(context) => Tooltip::for_action_in( "Go to Line/Column", &editor::actions::ToggleGoToLine, context, - window, cx, ), None => Tooltip::for_action( "Go to Line/Column", &editor::actions::ToggleGoToLine, - window, cx, ), }), diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index e26123339bd65fecd6ff9e5356098e29cee30890..33d956917055942cce365e9069cbb007e202eaf2 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -118,10 +118,12 @@ impl Keymap { pub fn all_bindings_for_input(&self, input: &[Keystroke]) -> Vec { self.bindings() .rev() - .filter_map(|binding| { - binding.match_keystrokes(input).filter(|pending| !pending)?; - Some(binding.clone()) + .filter(|binding| { + binding + .match_keystrokes(input) + .is_some_and(|pending| !pending) }) + .cloned() .collect() } diff --git a/crates/keymap_editor/src/keymap_editor.rs b/crates/keymap_editor/src/keymap_editor.rs index 2740ca14f68263fb520130e36d981535ca80aa3b..8e50a7303fb98febb492eb3f8b4aed4d928a879e 100644 --- a/crates/keymap_editor/src/keymap_editor.rs +++ b/crates/keymap_editor/src/keymap_editor.rs @@ -1,6 +1,7 @@ use std::{ cmp::{self}, ops::{Not as _, Range}, + rc::Rc, sync::Arc, time::Duration, }; @@ -173,7 +174,7 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystrokes: Vec, + keystrokes: Rc<[KeybindingKeystroke]>, context: Option, } @@ -235,7 +236,7 @@ struct ConflictState { } type ConflictKeybindMapping = HashMap< - Vec, + Rc<[KeybindingKeystroke]>, Vec<( Option, Vec, @@ -257,7 +258,7 @@ impl ConflictState { .context .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); let entry = action_keybind_mapping - .entry(mapping.keystrokes) + .entry(mapping.keystrokes.clone()) .or_default(); let origin = ConflictOrigin::new(binding.source, index); if let Some((_, origins)) = @@ -685,8 +686,7 @@ impl KeymapEditor { .unwrap_or(KeybindSource::Unknown); let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); - let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) - .vim_mode(source == KeybindSource::Vim); + let binding = KeyBinding::new(key_binding, source); let context = key_binding .predicate() @@ -717,7 +717,7 @@ impl KeymapEditor { StringMatchCandidate::new(index, &action_information.humanized_name); processed_bindings.push(ProcessedBinding::new_mapped( keystroke_text, - ui_key_binding, + binding, context, source, action_information, @@ -975,12 +975,11 @@ impl KeymapEditor { if conflict.is_user_keybind_conflict() { base_button_style(index, IconName::Warning) .icon_color(Color::Warning) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "View conflicts", Some(&ToggleConflictFilter), "Use alt+click to show all conflicts", - window, cx, ) }) @@ -995,12 +994,11 @@ impl KeymapEditor { })) } else if self.search_mode.exact_match() { base_button_style(index, IconName::Info) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Edit this binding", Some(&ShowMatchingKeybinds), "This binding is overridden by other bindings.", - window, cx, ) }) @@ -1011,12 +1009,11 @@ impl KeymapEditor { })) } else { base_button_style(index, IconName::Info) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Show matching keybinds", Some(&ShowMatchingKeybinds), "This binding is overridden by other bindings.\nUse alt+click to edit this binding", - window, cx, ) }) @@ -1348,10 +1345,25 @@ impl HumanizedActionNameCache { } } +#[derive(Clone)] +struct KeyBinding { + keystrokes: Rc<[KeybindingKeystroke]>, + source: KeybindSource, +} + +impl KeyBinding { + fn new(binding: &gpui::KeyBinding, source: KeybindSource) -> Self { + Self { + keystrokes: Rc::from(binding.keystrokes()), + source, + } + } +} + #[derive(Clone)] struct KeybindInformation { keystroke_text: SharedString, - ui_binding: ui::KeyBinding, + binding: KeyBinding, context: KeybindContextString, source: KeybindSource, } @@ -1359,7 +1371,7 @@ struct KeybindInformation { impl KeybindInformation { fn get_action_mapping(&self) -> ActionMapping { ActionMapping { - keystrokes: self.ui_binding.keystrokes.clone(), + keystrokes: self.binding.keystrokes.clone(), context: self.context.local().cloned(), } } @@ -1401,7 +1413,7 @@ enum ProcessedBinding { impl ProcessedBinding { fn new_mapped( keystroke_text: impl Into, - ui_key_binding: ui::KeyBinding, + binding: KeyBinding, context: KeybindContextString, source: KeybindSource, action_information: ActionInformation, @@ -1409,7 +1421,7 @@ impl ProcessedBinding { Self::Mapped( KeybindInformation { keystroke_text: keystroke_text.into(), - ui_binding: ui_key_binding, + binding, context, source, }, @@ -1427,8 +1439,8 @@ impl ProcessedBinding { } fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { - self.ui_key_binding() - .map(|binding| binding.keystrokes.as_slice()) + self.key_binding() + .map(|binding| binding.keystrokes.as_ref()) } fn keybind_information(&self) -> Option<&KeybindInformation> { @@ -1446,9 +1458,8 @@ impl ProcessedBinding { self.keybind_information().map(|keybind| &keybind.context) } - fn ui_key_binding(&self) -> Option<&ui::KeyBinding> { - self.keybind_information() - .map(|keybind| &keybind.ui_binding) + fn key_binding(&self) -> Option<&KeyBinding> { + self.keybind_information().map(|keybind| &keybind.binding) } fn keystroke_text(&self) -> Option<&SharedString> { @@ -1599,12 +1610,11 @@ impl Render for KeymapEditor { .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Search by Keystroke", &ToggleKeystrokeSearch, &focus_handle.clone(), - window, cx, ) } @@ -1636,7 +1646,7 @@ impl Render for KeymapEditor { let filter_state = self.filter_state; let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( match filter_state { FilterState::All => "Show Conflicts", @@ -1646,7 +1656,6 @@ impl Render for KeymapEditor { }, &ToggleConflictFilter, &focus_handle.clone(), - window, cx, ) } @@ -1698,12 +1707,11 @@ impl Render for KeymapEditor { .icon_size(IconSize::Small), { let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "View Default...", &zed_actions::OpenKeymapFile, &focus_handle, - window, cx, ) } @@ -1745,12 +1753,11 @@ impl Render for KeymapEditor { let keystroke_focus_handle = self.keystroke_editor.read(cx).focus_handle(cx); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Exact Match Mode", &ToggleExactKeystrokeMatching, &keystroke_focus_handle, - window, cx, ) } @@ -1856,13 +1863,13 @@ impl Render for KeymapEditor { ) .into_any_element(); - let keystrokes = binding.ui_key_binding().cloned().map_or( + let keystrokes = binding.key_binding().map_or( binding .keystroke_text() .cloned() .unwrap_or_default() .into_any_element(), - IntoElement::into_any_element, + |binding| ui::KeyBinding::from_keystrokes(binding.keystrokes.clone(), binding.source).into_any_element() ); let action_arguments = match binding.action().arguments.clone() @@ -2301,7 +2308,7 @@ impl KeybindingEditorModal { .map_err(InputError::error)?; let action_mapping = ActionMapping { - keystrokes: new_keystrokes, + keystrokes: Rc::from(new_keystrokes.as_slice()), context: new_context.map(SharedString::from), }; diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index b86fa36657499a1c0bd1e8b3f600f387b6675ede..c75c3954cc6590c2e0cb4326c073ed004eaac280 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -62,9 +62,7 @@ impl Render for ActiveBufferLanguage { }); } })) - .tooltip(|window, cx| { - Tooltip::for_action("Select Language", &Toggle, window, cx) - }), + .tooltip(|_window, cx| Tooltip::for_action("Select Language", &Toggle, cx)), ) }) } diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 7b0b71059e9998914ce511b47e26d1fd0c3abfe5..e704d6bbf03eea18ae717f7aa11b25466dd68e9e 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -167,7 +167,7 @@ impl Item for KeyContextView { } impl Render for KeyContextView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); @@ -212,7 +212,6 @@ impl Render for KeyContextView { .style(ButtonStyle::Filled) .key_binding(ui::KeyBinding::for_action( &zed_actions::OpenDefaultKeymap, - window, cx )) .on_click(|_, window, cx| { @@ -222,7 +221,7 @@ impl Render for KeyContextView { .child( Button::new("edit_your_keymap", "Edit Keymap File") .style(ButtonStyle::Filled) - .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, window, cx)) + .key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, cx)) .on_click(|_, window, cx| { window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx); }), diff --git a/crates/language_tools/src/lsp_button.rs b/crates/language_tools/src/lsp_button.rs index 9b3ac04467569c9feabe7e3a0431bbfd2c0b7484..7dc2e93a5c707eaa3829caba6d6d2a04773883b1 100644 --- a/crates/language_tools/src/lsp_button.rs +++ b/crates/language_tools/src/lsp_button.rs @@ -1065,14 +1065,8 @@ impl Render for LspButton { .when_some(indicator, IconButton::indicator) .icon_size(IconSize::Small) .indicator_border_color(Some(cx.theme().colors().status_bar_background)), - move |window, cx| { - Tooltip::with_meta( - "Language Servers", - Some(&ToggleMenu), - description, - window, - cx, - ) + move |_window, cx| { + Tooltip::with_meta("Language Servers", Some(&ToggleMenu), description, cx) }, ), ) diff --git a/crates/line_ending_selector/src/line_ending_indicator.rs b/crates/line_ending_selector/src/line_ending_indicator.rs index 042630056a4cad93497e7b35cab7c82c1ea643e3..ee858d706b3a8152c868a5bd629c112a4d1b225f 100644 --- a/crates/line_ending_selector/src/line_ending_indicator.rs +++ b/crates/line_ending_selector/src/line_ending_indicator.rs @@ -43,9 +43,7 @@ impl Render for LineEndingIndicator { LineEndingSelector::toggle(editor, window, cx); } })) - .tooltip(|window, cx| { - Tooltip::for_action("Select Line Ending", &Toggle, window, cx) - }), + .tooltip(|_window, cx| Tooltip::for_action("Select Line Ending", &Toggle, cx)), ) }) } diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index a1139d7f25f08fa54edf7ea71438b92884c8e124..913d92d48c4018759f5ba91bb61d514160ba1b3f 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -337,10 +337,9 @@ impl Render for Onboarding { KeyBinding::for_action_in( &Finish, &self.focus_handle, - window, cx, ) - .map(|kb| kb.size(rems_from_px(12.))), + .size(rems_from_px(12.)), ) .on_click(|_, window, cx| { window.dispatch_action(Finish.boxed_clone(), cx); diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 50f0d83698adbd1b8bff0d7e73a5f342d8fe11cd..b2711cd52d61a51711bd8ec90581b981d7bcf784 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -78,13 +78,7 @@ struct Section { } impl Section { - fn render( - self, - index_offset: usize, - focus: &FocusHandle, - window: &mut Window, - cx: &mut App, - ) -> impl IntoElement { + fn render(self, index_offset: usize, focus: &FocusHandle, cx: &mut App) -> impl IntoElement { v_flex() .min_w_full() .child( @@ -104,7 +98,7 @@ impl Section { self.entries .iter() .enumerate() - .map(|(index, entry)| entry.render(index_offset + index, focus, window, cx)), + .map(|(index, entry)| entry.render(index_offset + index, focus, cx)), ) } } @@ -116,13 +110,7 @@ struct SectionEntry { } impl SectionEntry { - fn render( - &self, - button_index: usize, - focus: &FocusHandle, - window: &Window, - cx: &App, - ) -> impl IntoElement { + fn render(&self, button_index: usize, focus: &FocusHandle, cx: &App) -> impl IntoElement { ButtonLike::new(("onboarding-button-id", button_index)) .tab_index(button_index as isize) .full_width() @@ -141,9 +129,8 @@ impl SectionEntry { ) .child(Label::new(self.title)), ) - .children( - KeyBinding::for_action_in(self.action, focus, window, cx) - .map(|s| s.size(rems_from_px(12.))), + .child( + KeyBinding::for_action_in(self.action, focus, cx).size(rems_from_px(12.)), ), ) .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) @@ -151,7 +138,6 @@ impl SectionEntry { } pub struct WelcomePage { - first_paint: bool, focus_handle: FocusHandle, } @@ -168,11 +154,7 @@ impl WelcomePage { } impl Render for WelcomePage { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.first_paint { - window.request_animation_frame(); - self.first_paint = false; - } + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let (first_section, second_section) = CONTENT; let first_section_entries = first_section.entries.len(); let last_index = first_section_entries + second_section.entries.len(); @@ -220,13 +202,11 @@ impl Render for WelcomePage { .child(first_section.render( Default::default(), &self.focus_handle, - window, cx, )) .child(second_section.render( first_section_entries, &self.focus_handle, - window, cx, )) .child( @@ -316,10 +296,7 @@ impl WelcomePage { cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify()) .detach(); - WelcomePage { - first_paint: true, - focus_handle, - } + WelcomePage { focus_handle } }) } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e33dbfd9d0142f335d827b01a07ea66c10efe45a..ff5a6b661e792ad7c9188fa99b288827efe55c48 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4678,12 +4678,11 @@ impl ProjectPanel { div() .id("symlink_icon") .pr_3() - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( path.to_string(), None, "Symbolic Link", - window, cx, ) }) @@ -5863,7 +5862,6 @@ impl Render for ProjectPanel { .key_binding(KeyBinding::for_action_in( &workspace::Open, &focus_handle, - window, cx, )) .on_click(cx.listener(|this, _, window, cx| { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 12290916e2afe242b2c389da3b971fdfaa9f0eb0..13013c9189749f77b8619ac19d59f96e5adb1e1d 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -547,11 +547,7 @@ impl PickerDelegate for RecentProjectsDelegate { ) } - fn render_footer( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { + fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { Some( h_flex() .w_full() @@ -567,7 +563,6 @@ impl PickerDelegate for RecentProjectsDelegate { from_existing_connection: false, create_new_window: false, }, - window, cx, )) .on_click(|_, window, cx| { @@ -583,7 +578,7 @@ impl PickerDelegate for RecentProjectsDelegate { ) .child( Button::new("local", "Open Local Folder") - .key_binding(KeyBinding::for_action(&workspace::Open, window, cx)) + .key_binding(KeyBinding::for_action(&workspace::Open, cx)) .on_click(|_, window, cx| { window.dispatch_action(workspace::Open.boxed_clone(), cx) }), diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs index 7e523a46ddf2dfce9921a3c907de19fb91221f9b..209948685ce263361101e508ce6ab65839b132cb 100644 --- a/crates/repl/src/notebook/notebook_ui.rs +++ b/crates/repl/src/notebook/notebook_ui.rs @@ -326,7 +326,7 @@ impl NotebookEditor { cx, ) .tooltip(move |window, cx| { - Tooltip::for_action("Execute all cells", &RunAll, window, cx) + Tooltip::for_action("Execute all cells", &RunAll, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(RunAll), cx); @@ -341,12 +341,7 @@ impl NotebookEditor { ) .disabled(!has_outputs) .tooltip(move |window, cx| { - Tooltip::for_action( - "Clear all outputs", - &ClearOutputs, - window, - cx, - ) + Tooltip::for_action("Clear all outputs", &ClearOutputs, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(ClearOutputs), cx); @@ -363,7 +358,7 @@ impl NotebookEditor { cx, ) .tooltip(move |window, cx| { - Tooltip::for_action("Move cell up", &MoveCellUp, window, cx) + Tooltip::for_action("Move cell up", &MoveCellUp, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(MoveCellUp), cx); @@ -377,7 +372,7 @@ impl NotebookEditor { cx, ) .tooltip(move |window, cx| { - Tooltip::for_action("Move cell down", &MoveCellDown, window, cx) + Tooltip::for_action("Move cell down", &MoveCellDown, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(MoveCellDown), cx); @@ -394,12 +389,7 @@ impl NotebookEditor { cx, ) .tooltip(move |window, cx| { - Tooltip::for_action( - "Add markdown block", - &AddMarkdownBlock, - window, - cx, - ) + Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(AddMarkdownBlock), cx); @@ -413,7 +403,7 @@ impl NotebookEditor { cx, ) .tooltip(move |window, cx| { - Tooltip::for_action("Add code block", &AddCodeBlock, window, cx) + Tooltip::for_action("Add code block", &AddCodeBlock, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(AddCodeBlock), cx); diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs index 36936641b050012968ec4ac586c540c2567db350..d8bd8869f28ac4a9bdf396073f8948d15aef9e3e 100644 --- a/crates/repl/src/repl_sessions_ui.rs +++ b/crates/repl/src/repl_sessions_ui.rs @@ -197,7 +197,7 @@ impl Item for ReplSessionsPage { } impl Render for ReplSessionsPage { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let store = ReplStore::global(cx); let (kernel_specifications, sessions) = store.update(cx, |store, _cx| { @@ -241,7 +241,7 @@ impl Render for ReplSessionsPage { return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child( v_flex() .child(Label::new(instructions)) - .children(KeyBinding::for_action(&Run, window, cx)), + .child(KeyBinding::for_action(&Run, cx)), ); } diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index abb0b4e3a1a84cf7ecf40939b33aee19b874bcdf..1d3eb8b55690c2344a40820e3c5df472bcfc1e05 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -390,12 +390,11 @@ impl PickerDelegate for RulePickerDelegate { div() .id("built-in-rule") .child(Icon::new(IconName::FileLock).color(Color::Muted)) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Built-in rule", None, BUILT_IN_TOOLTIP_TEXT, - window, cx, ) }) @@ -426,12 +425,11 @@ impl PickerDelegate for RulePickerDelegate { "Remove from Default Rules", )) } else { - this.tooltip(move |window, cx| { + this.tooltip(move |_window, cx| { Tooltip::with_meta( "Add to Default Rules", None, "Always included in every thread.", - window, cx, ) }) @@ -1112,8 +1110,8 @@ impl RulesLibrary { .justify_end() .child( IconButton::new("new-rule", IconName::Plus) - .tooltip(move |window, cx| { - Tooltip::for_action("New Rule", &NewRule, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action("New Rule", &NewRule, cx) }) .on_click(|_, window, cx| { window.dispatch_action(Box::new(NewRule), cx); @@ -1215,7 +1213,7 @@ impl RulesLibrary { .id("token_count") .mr_1() .flex_shrink_0() - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Token Estimation", None, @@ -1226,7 +1224,6 @@ impl RulesLibrary { .map(|model| model.name().0) .unwrap_or_default() ), - window, cx, ) }) @@ -1245,23 +1242,21 @@ impl RulesLibrary { Icon::new(IconName::FileLock) .color(Color::Muted), ) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Built-in rule", None, BUILT_IN_TOOLTIP_TEXT, - window, cx, ) }) .into_any() } else { IconButton::new("delete-rule", IconName::Trash) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action( "Delete Rule", &DeleteRule, - window, cx, ) }) @@ -1273,11 +1268,10 @@ impl RulesLibrary { }) .child( IconButton::new("duplicate-rule", IconName::BookCopy) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action( "Duplicate Rule", &DuplicateRule, - window, cx, ) }) @@ -1305,12 +1299,11 @@ impl RulesLibrary { "Remove from Default Rules", )) } else { - this.tooltip(move |window, cx| { + this.tooltip(move |_window, cx| { Tooltip::with_meta( "Add to Default Rules", None, "Always included in every thread.", - window, cx, ) }) @@ -1417,7 +1410,7 @@ impl Render for RulesLibrary { .full_width() .key_binding( KeyBinding::for_action( - &NewRule, window, cx, + &NewRule, cx, ), ) .on_click(|_, window, cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 923e30e0b6878ad32fd65d210101a5a62fd38687..49c1fc5b297aedcf86c66140d0d803901b18c52a 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -266,12 +266,11 @@ impl Render for BufferSearchBar { .toggle_state(self.selection_search_enabled.is_some()) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Search Selection", &ToggleSelection, &focus_handle, - window, cx, ) } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 97882994d2f8ea452e45dd830b777ec445d3768f..3a9367db724257d4ba32c343c578ba27bea412d7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -391,7 +391,7 @@ pub enum ViewEvent { impl EventEmitter for ProjectSearchView {} impl Render for ProjectSearchView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { if self.has_matches() { div() .flex_1() @@ -426,7 +426,7 @@ impl Render for ProjectSearchView { None } } else { - Some(self.landing_text_minor(window, cx).into_any_element()) + Some(self.landing_text_minor(cx).into_any_element()) }; let page_content = page_content.map(|text| div().child(text)); @@ -1446,7 +1446,7 @@ impl ProjectSearchView { self.active_match_index.is_some() } - fn landing_text_minor(&self, window: &mut Window, cx: &App) -> impl IntoElement { + fn landing_text_minor(&self, cx: &App) -> impl IntoElement { let focus_handle = self.focus_handle.clone(); v_flex() .gap_1() @@ -1460,12 +1460,7 @@ impl ProjectSearchView { .icon(IconName::Filter) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleFilters, - &focus_handle, - window, - cx, - )) + .key_binding(KeyBinding::for_action_in(&ToggleFilters, &focus_handle, cx)) .on_click(|_event, window, cx| { window.dispatch_action(ToggleFilters.boxed_clone(), cx) }), @@ -1475,12 +1470,7 @@ impl ProjectSearchView { .icon(IconName::Replace) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleReplace, - &focus_handle, - window, - cx, - )) + .key_binding(KeyBinding::for_action_in(&ToggleReplace, &focus_handle, cx)) .on_click(|_event, window, cx| { window.dispatch_action(ToggleReplace.boxed_clone(), cx) }), @@ -1490,12 +1480,7 @@ impl ProjectSearchView { .icon(IconName::Regex) .icon_position(IconPosition::Start) .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleRegex, - &focus_handle, - window, - cx, - )) + .key_binding(KeyBinding::for_action_in(&ToggleRegex, &focus_handle, cx)) .on_click(|_event, window, cx| { window.dispatch_action(ToggleRegex.boxed_clone(), cx) }), @@ -1508,7 +1493,6 @@ impl ProjectSearchView { .key_binding(KeyBinding::for_action_in( &ToggleCaseSensitive, &focus_handle, - window, cx, )) .on_click(|_event, window, cx| { @@ -1523,7 +1507,6 @@ impl ProjectSearchView { .key_binding(KeyBinding::for_action_in( &ToggleWholeWord, &focus_handle, - window, cx, )) .on_click(|_event, window, cx| { @@ -2049,8 +2032,8 @@ impl Render for ProjectSearchBar { .child( IconButton::new("project-search-filter-button", IconName::Filter) .shape(IconButtonShape::Square) - .tooltip(|window, cx| { - Tooltip::for_action("Toggle Filters", &ToggleFilters, window, cx) + .tooltip(|_window, cx| { + Tooltip::for_action("Toggle Filters", &ToggleFilters, cx) }) .on_click(cx.listener(|this, _, window, cx| { this.toggle_filters(window, cx); @@ -2063,12 +2046,11 @@ impl Render for ProjectSearchBar { ) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_window, cx| { Tooltip::for_action_in( "Toggle Filters", &ToggleFilters, &focus_handle, - window, cx, ) } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 147ffcbbfb1956a4e258b7242729d366f4c2d1be..6663f8c3184aba9fedbcd5faa3d80d5889181074 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -158,9 +158,7 @@ impl SearchOption { .style(ButtonStyle::Subtle) .shape(IconButtonShape::Square) .toggle_state(active.contains(self.as_options())) - .tooltip({ - move |window, cx| Tooltip::for_action_in(label, action, &focus_handle, window, cx) - }) + .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx)) } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 631b96b69f3b9aedd4ed299953edf6e63665ba99..14a5fefcf7341694260da96a8f2c43d149356074 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -32,7 +32,7 @@ pub(super) fn render_action_button( window.dispatch_action(action.boxed_clone(), cx) } }) - .tooltip(move |window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, window, cx)) + .tooltip(move |_window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, cx)) .when_some(button_state, |this, state| match state { ActionButtonState::Toggled => this.toggle_state(true), ActionButtonState::Disabled => this.disabled(true), diff --git a/crates/search/src/search_status_button.rs b/crates/search/src/search_status_button.rs index 544a15155c0be789fe239f039e9d0b94b99dabdd..712a322c1094f28ea601d6d170e7be1e395e25f7 100644 --- a/crates/search/src/search_status_button.rs +++ b/crates/search/src/search_status_button.rs @@ -24,13 +24,8 @@ impl Render for SearchButton { button.child( IconButton::new("project-search-indicator", SEARCH_ICON) .icon_size(IconSize::Small) - .tooltip(|window, cx| { - Tooltip::for_action( - "Project Search", - &workspace::DeploySearch::default(), - window, - cx, - ) + .tooltip(|_window, cx| { + Tooltip::for_action("Project Search", &workspace::DeploySearch::default(), cx) }) .on_click(cx.listener(|_this, _, window, cx| { window.dispatch_action(Box::new(workspace::DeploySearch::default()), cx); diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 9fa40418df4792978de1ff7fea074e1334d9dad0..4469dacc35ba4538addee24bd673e6817832ae31 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -2159,20 +2159,16 @@ impl SettingsWindow { .flex_shrink_0() .border_t_1() .border_color(cx.theme().colors().border_variant) - .children( - KeyBinding::for_action_in( - &ToggleFocusNav, - &self.navbar_focus_handle.focus_handle(cx), - window, - cx, + .child( + KeybindingHint::new( + KeyBinding::for_action_in( + &ToggleFocusNav, + &self.navbar_focus_handle.focus_handle(cx), + cx, + ), + cx.theme().colors().surface_background.opacity(0.5), ) - .map(|this| { - KeybindingHint::new( - this, - cx.theme().colors().surface_background.opacity(0.5), - ) - .suffix(focus_keybind_label) - }), + .suffix(focus_keybind_label), ), ) } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 0563cd517225ac5781e34575cacbda54b303fe08..f82321feeb245b4ee3b6d56627387c8594d5db8e 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -664,10 +664,10 @@ impl PickerDelegate for TasksModalDelegate { .child( left_button .map(|(label, action)| { - let keybind = KeyBinding::for_action(&*action, window, cx); + let keybind = KeyBinding::for_action(&*action, cx); Button::new("edit-current-task", label) - .when_some(keybind, |this, keybind| this.key_binding(keybind)) + .key_binding(keybind) .on_click(move |_, window, cx| { window.dispatch_action(action.boxed_clone(), cx); }) @@ -682,7 +682,7 @@ impl PickerDelegate for TasksModalDelegate { secondary: current_modifiers.secondary(), } .boxed_clone(); - this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { + this.child({ let spawn_oneshot_label = if current_modifiers.secondary() { "Spawn Oneshot Without History" } else { @@ -690,44 +690,35 @@ impl PickerDelegate for TasksModalDelegate { }; Button::new("spawn-onehshot", spawn_oneshot_label) - .key_binding(keybind) + .key_binding(KeyBinding::for_action(&*action, cx)) .on_click(move |_, window, cx| { window.dispatch_action(action.boxed_clone(), cx) }) - })) + }) } else if current_modifiers.secondary() { - this.children( - KeyBinding::for_action(&menu::SecondaryConfirm, window, cx).map( - |keybind| { - let label = if is_recent_selected { - "Rerun Without History" - } else { - "Spawn Without History" - }; - Button::new("spawn", label).key_binding(keybind).on_click( - move |_, window, cx| { - window.dispatch_action( - menu::SecondaryConfirm.boxed_clone(), - cx, - ) - }, - ) - }, - ), - ) + this.child({ + let label = if is_recent_selected { + "Rerun Without History" + } else { + "Spawn Without History" + }; + Button::new("spawn", label) + .key_binding(KeyBinding::for_action(&menu::SecondaryConfirm, cx)) + .on_click(move |_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) + }) + }) } else { - this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map( - |keybind| { - let run_entry_label = - if is_recent_selected { "Rerun" } else { "Spawn" }; - - Button::new("spawn", run_entry_label) - .key_binding(keybind) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - }) - }, - )) + this.child({ + let run_entry_label = + if is_recent_selected { "Rerun" } else { "Spawn" }; + + Button::new("spawn", run_entry_label) + .key_binding(KeyBinding::for_action(&menu::Confirm, cx)) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx); + }) + }) } }) .into_any_element(), diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index df30ea4ddf5611b286c0608c7e6d51d4ff7f9e00..6568eac324552a293d64060c07f6299d2edf9f8d 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -210,11 +210,10 @@ impl TerminalPanel { .on_click(cx.listener(|pane, _, window, cx| { pane.toggle_zoom(&workspace::ToggleZoom, window, cx); })) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action( if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, - window, cx, ) }) @@ -1739,14 +1738,8 @@ impl Render for InlineAssistTabBarButton { .on_click(cx.listener(|_, _, window, cx| { window.dispatch_action(InlineAssist::default().boxed_clone(), cx); })) - .tooltip(move |window, cx| { - Tooltip::for_action_in( - "Inline Assist", - &InlineAssist::default(), - &focus_handle, - window, - cx, - ) + .tooltip(move |_window, cx| { + Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx) }) } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 65eb993d208629f16c705bbac55c3dc3e0f08261..597d1f58deab65fb995fdb1be0c782148c98b509 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -840,9 +840,7 @@ impl TerminalView { .size(ButtonSize::Compact) .icon_color(Color::Default) .shape(ui::IconButtonShape::Square) - .tooltip(move |window, cx| { - Tooltip::for_action("Rerun task", &RerunTask, window, cx) - }) + .tooltip(move |_window, cx| Tooltip::for_action("Rerun task", &RerunTask, cx)) .on_click(move |_, window, cx| { window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx); }), diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index b5a51976a01179d3a70bd6d087533866a6c2814b..5dd08ee3f9e132666520433db92279df559abdb0 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -403,14 +403,13 @@ impl TitleBar { IconName::Mic }, ) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if is_muted { if is_deafened { Tooltip::with_meta( "Unmute Microphone", None, "Audio will be unmuted", - window, cx, ) } else { @@ -444,12 +443,12 @@ impl TitleBar { .selected_style(ButtonStyle::Tinted(TintColor::Error)) .icon_size(IconSize::Small) .toggle_state(is_deafened) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if is_deafened { let label = "Unmute Audio"; if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be unmuted", window, cx) + Tooltip::with_meta(label, None, "Microphone will be unmuted", cx) } else { Tooltip::simple(label, cx) } @@ -457,7 +456,7 @@ impl TitleBar { let label = "Mute Audio"; if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be muted", window, cx) + Tooltip::with_meta(label, None, "Microphone will be muted", cx) } else { Tooltip::simple(label, cx) } diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 6adc5769498ee19a7139c3fd02bd586e32185778..750ef0a6cdc56d1e9ea87ab12807584a4e0e4bd2 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -154,12 +154,11 @@ impl Render for OnboardingBanner { telemetry::event!("Banner Dismissed", source = this.source); this.dismiss(cx) })) - .tooltip(|window, cx| { + .tooltip(|_window, cx| { Tooltip::with_meta( "Close Announcement Banner", None, "It won't show again for this feature", - window, cx, ) }), diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ec98e0d2d9cf7d941671c282164ad3e4e28b661d..3f3b009a19fa15a9e9b9c2abe09a66e90eceafb2 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -379,7 +379,7 @@ impl TitleBar { ) .child(Label::new(nickname).size(LabelSize::Small).truncate()), ) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Remote Project", Some(&OpenRemote { @@ -387,7 +387,6 @@ impl TitleBar { create_new_window: false, }), meta.clone(), - window, cx, ) }) @@ -481,13 +480,12 @@ impl TitleBar { .when(!is_project_selected, |b| b.color(Color::Muted)) .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action( "Recent Projects", &zed_actions::OpenRecent { create_new_window: false, }, - window, cx, ) }) @@ -527,12 +525,11 @@ impl TitleBar { .color(Color::Muted) .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( "Recent Branches", Some(&zed_actions::git::Branch), "Local branches only", - window, cx, ) }) diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs index e816bec2ff26e7b8db81cf800307cbab91557712..c017483a32325d13e85a5db34566a3b0bf6e15a5 100644 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -490,7 +490,6 @@ impl Render for AddToolchainState { .key_binding(KeyBinding::for_action_in( &menu::Confirm, &handle, - window, cx, )) .on_click(cx.listener(|this, _, window, cx| { @@ -1117,7 +1116,6 @@ impl PickerDelegate for ToolchainSelectorDelegate { .key_binding(KeyBinding::for_action_in( &AddToolchain, &self.focus_handle, - _window, cx, )) .on_click(|_, window, cx| { @@ -1129,7 +1127,6 @@ impl PickerDelegate for ToolchainSelectorDelegate { .key_binding(KeyBinding::for_action_in( &menu::Confirm, &self.focus_handle, - _window, cx, )) .on_click(|_, window, cx| { diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 7b61789b3c87d54ff231e1d635266d6502fb944f..bfafaee428edc47209391cd3a7abfd3d5f432fe5 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -834,9 +834,9 @@ impl ContextMenu { .disabled(true) .child(Label::new(label.clone())) .into_any_element(), - ContextMenuItem::Entry(entry) => self - .render_menu_entry(ix, entry, window, cx) - .into_any_element(), + ContextMenuItem::Entry(entry) => { + self.render_menu_entry(ix, entry, cx).into_any_element() + } ContextMenuItem::CustomEntry { entry_render, handler, @@ -883,7 +883,6 @@ impl ContextMenu { &self, ix: usize, entry: &ContextMenuEntry, - window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let ContextMenuEntry { @@ -980,18 +979,18 @@ impl ContextMenu { .justify_between() .child(label_element) .debug_selector(|| format!("MENU_ITEM-{}", label)) - .children(action.as_ref().and_then(|action| { - self.action_context + .children(action.as_ref().map(|action| { + let binding = self + .action_context .as_ref() - .and_then(|focus| { - KeyBinding::for_action_in(&**action, focus, window, cx) - }) - .or_else(|| KeyBinding::for_action(&**action, window, cx)) - .map(|binding| { - div().ml_4().child(binding.disabled(*disabled)).when( - *disabled && documentation_aside.is_some(), - |parent| parent.invisible(), - ) + .map(|focus| KeyBinding::for_action_in(&**action, focus, cx)) + .unwrap_or_else(|| KeyBinding::for_action(&**action, cx)); + + div() + .ml_4() + .child(binding.disabled(*disabled)) + .when(*disabled && documentation_aside.is_some(), |parent| { + parent.invisible() }) })) .when(*disabled && documentation_aside.is_some(), |parent| { @@ -1016,7 +1015,7 @@ impl ContextMenu { let action_context = self.action_context.clone(); let title = title.clone(); let action = action.boxed_clone(); - move |window, cx| { + move |_window, cx| { action_context .as_ref() .map(|focus| { @@ -1024,17 +1023,11 @@ impl ContextMenu { title.clone(), &*action, focus, - window, cx, ) }) .unwrap_or_else(|| { - Tooltip::for_action( - title.clone(), - &*action, - window, - cx, - ) + Tooltip::for_action(title.clone(), &*action, cx) }) } }) diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index f8ac85528ec3317bb003d3f8763f8c57a7d4bba2..bf52d7be8c7e91b230eac295dff03f2679a004af 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use crate::PlatformStyle; use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use gpui::{ @@ -5,23 +7,49 @@ use gpui::{ Modifiers, Window, relative, }; use itertools::Itertools; +use settings::KeybindSource; + +#[derive(Debug)] +enum Source { + Action { + action: Box, + focus_handle: Option, + }, + Keystrokes { + /// A keybinding consists of a set of keystrokes, + /// where each keystroke is a key and a set of modifier keys. + /// More than one keystroke produces a chord. + /// + /// This should always contain at least one keystroke. + keystrokes: Rc<[KeybindingKeystroke]>, + }, +} -#[derive(Debug, IntoElement, Clone, RegisterComponent)] -pub struct KeyBinding { - /// A keybinding consists of a set of keystrokes, - /// where each keystroke is a key and a set of modifier keys. - /// More than one keystroke produces a chord. - /// - /// This should always contain at least one keystroke. - pub keystrokes: Vec, +impl Clone for Source { + fn clone(&self) -> Self { + match self { + Source::Action { + action, + focus_handle, + } => Source::Action { + action: action.boxed_clone(), + focus_handle: focus_handle.clone(), + }, + Source::Keystrokes { keystrokes } => Source::Keystrokes { + keystrokes: keystrokes.clone(), + }, + } + } +} +#[derive(Clone, Debug, IntoElement, RegisterComponent)] +pub struct KeyBinding { + source: Source, + size: Option, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, - size: Option, - /// Determines whether the keybinding is meant for vim mode. vim_mode: bool, - /// Indicates whether the keybinding is currently disabled. disabled: bool, } @@ -32,23 +60,13 @@ impl Global for VimStyle {} impl KeyBinding { /// Returns the highest precedence keybinding for an action. This is the last binding added to /// the keymap. User bindings are added after built-in bindings so that they take precedence. - pub fn for_action(action: &dyn Action, window: &mut Window, cx: &App) -> Option { - if let Some(focused) = window.focused(cx) { - return Self::for_action_in(action, &focused, window, cx); - } - let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(Self::new_from_gpui(key_binding, cx)) + pub fn for_action(action: &dyn Action, cx: &App) -> Self { + Self::new(action, None, cx) } /// Like `for_action`, but lets you specify the context from which keybindings are matched. - pub fn for_action_in( - action: &dyn Action, - focus: &FocusHandle, - window: &Window, - cx: &App, - ) -> Option { - let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?; - Some(Self::new_from_gpui(key_binding, cx)) + pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self { + Self::new(action, Some(focus.clone()), cx) } pub fn set_vim_mode(cx: &mut App, enabled: bool) { @@ -59,18 +77,27 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(keystrokes: Vec, cx: &App) -> Self { + pub fn new(action: &dyn Action, focus_handle: Option, cx: &App) -> Self { Self { - keystrokes, - platform_style: PlatformStyle::platform(), + source: Source::Action { + action: action.boxed_clone(), + focus_handle, + }, size: None, vim_mode: KeyBinding::is_vim_mode(cx), + platform_style: PlatformStyle::platform(), disabled: false, } } - pub fn new_from_gpui(key_binding: gpui::KeyBinding, cx: &App) -> Self { - Self::new(key_binding.keystrokes().to_vec(), cx) + pub fn from_keystrokes(keystrokes: Rc<[KeybindingKeystroke]>, source: KeybindSource) -> Self { + Self { + source: Source::Keystrokes { keystrokes }, + size: None, + vim_mode: source == KeybindSource::Vim, + platform_style: PlatformStyle::platform(), + disabled: false, + } } /// Sets the [`PlatformStyle`] for this [`KeyBinding`]. @@ -91,11 +118,6 @@ impl KeyBinding { self.disabled = disabled; self } - - pub fn vim_mode(mut self, enabled: bool) -> Self { - self.vim_mode = enabled; - self - } } fn render_key( @@ -115,36 +137,54 @@ fn render_key( } impl RenderOnce for KeyBinding { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let color = self.disabled.then_some(Color::Disabled); - - h_flex() - .debug_selector(|| { - format!( - "KEY_BINDING-{}", - self.keystrokes - .iter() - .map(|k| k.key().to_string()) - .collect::>() - .join(" ") - ) - }) - .gap(DynamicSpacing::Base04.rems(cx)) - .flex_none() - .children(self.keystrokes.iter().map(|keystroke| { - h_flex() - .flex_none() - .py_0p5() - .rounded_xs() - .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( - keystroke, - color, - self.size, - self.platform_style, - self.vim_mode, - )) - })) + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let render_keybinding = |keystrokes: &[KeybindingKeystroke]| { + let color = self.disabled.then_some(Color::Disabled); + + h_flex() + .debug_selector(|| { + format!( + "KEY_BINDING-{}", + keystrokes + .iter() + .map(|k| k.key().to_string()) + .collect::>() + .join(" ") + ) + }) + .gap(DynamicSpacing::Base04.rems(cx)) + .flex_none() + .children(keystrokes.iter().map(|keystroke| { + h_flex() + .flex_none() + .py_0p5() + .rounded_xs() + .text_color(cx.theme().colors().text_muted) + .children(render_keybinding_keystroke( + keystroke, + color, + self.size, + PlatformStyle::platform(), + self.vim_mode, + )) + })) + .into_any_element() + }; + + match self.source { + Source::Action { + action, + focus_handle, + } => focus_handle + .or_else(|| window.focused(cx)) + .and_then(|focus| { + window.highest_precedence_binding_for_action_in(action.as_ref(), &focus) + }) + .or_else(|| window.highest_precedence_binding_for_action(action.as_ref())) + .map(|binding| render_keybinding(binding.keystrokes())), + Source::Keystrokes { keystrokes } => Some(render_keybinding(keystrokes.as_ref())), + } + .unwrap_or_else(|| gpui::Empty.into_any_element()) } } @@ -517,79 +557,79 @@ impl Component for KeyBinding { ) } - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - KeyBinding::new_from_gpui( - gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), - cx, - ) - .into_any_element(), - ), - single_example( - "Mac Style", - KeyBinding::new_from_gpui( - gpui::KeyBinding::new("cmd-s", gpui::NoAction, None), - cx, - ) - .platform_style(PlatformStyle::Mac) - .into_any_element(), - ), - single_example( - "Windows Style", - KeyBinding::new_from_gpui( - gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), - cx, - ) - .platform_style(PlatformStyle::Windows) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Vim Mode", - vec![single_example( - "Vim Mode Enabled", - KeyBinding::new_from_gpui( - gpui::KeyBinding::new("dd", gpui::NoAction, None), - cx, - ) - .vim_mode(true) - .into_any_element(), - )], - ), - example_group_with_title( - "Complex Bindings", - vec![ - single_example( - "Multiple Keys", - KeyBinding::new_from_gpui( - gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None), - cx, - ) - .into_any_element(), - ), - single_example( - "With Shift", - KeyBinding::new_from_gpui( - gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None), - cx, - ) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } + // fn preview(_window: &mut Window, cx: &mut App) -> Option { + // Some( + // v_flex() + // .gap_6() + // .children(vec![ + // example_group_with_title( + // "Basic Usage", + // vec![ + // single_example( + // "Default", + // KeyBinding::new_from_gpui( + // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), + // cx, + // ) + // .into_any_element(), + // ), + // single_example( + // "Mac Style", + // KeyBinding::new_from_gpui( + // gpui::KeyBinding::new("cmd-s", gpui::NoAction, None), + // cx, + // ) + // .platform_style(PlatformStyle::Mac) + // .into_any_element(), + // ), + // single_example( + // "Windows Style", + // KeyBinding::new_from_gpui( + // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), + // cx, + // ) + // .platform_style(PlatformStyle::Windows) + // .into_any_element(), + // ), + // ], + // ), + // example_group_with_title( + // "Vim Mode", + // vec![single_example( + // "Vim Mode Enabled", + // KeyBinding::new_from_gpui( + // gpui::KeyBinding::new("dd", gpui::NoAction, None), + // cx, + // ) + // .vim_mode(true) + // .into_any_element(), + // )], + // ), + // example_group_with_title( + // "Complex Bindings", + // vec![ + // single_example( + // "Multiple Keys", + // KeyBinding::new_from_gpui( + // gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None), + // cx, + // ) + // .into_any_element(), + // ), + // single_example( + // "With Shift", + // KeyBinding::new_from_gpui( + // gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None), + // cx, + // ) + // .into_any_element(), + // ), + // ], + // ), + // ]) + // .into_any_element(), + // ) + // } } #[cfg(test)] diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index 58f2793ea0ee29b55eace9e7fe9e53c606ca0a43..c998e29f0ed6f5bccab976b11080320d4d65a7dd 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -14,10 +14,11 @@ use theme::Appearance; /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; +/// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::new( -/// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())], cx), +/// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())].into(), KeybindSource::Base), /// Hsla::black() /// ) /// .prefix("Save:") @@ -45,10 +46,11 @@ impl KeybindingHint { /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; + /// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::new( - /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())], cx), + /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base), /// Hsla::black() /// ); /// # } @@ -74,11 +76,12 @@ impl KeybindingHint { /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; + /// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::with_prefix( /// "Copy:", - /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())], cx), + /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base), /// Hsla::black() /// ); /// # } @@ -108,10 +111,11 @@ impl KeybindingHint { /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; + /// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::with_suffix( - /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())], cx), + /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())].into(), KeybindSource::Base), /// "Paste", /// Hsla::black() /// ); @@ -141,10 +145,11 @@ impl KeybindingHint { /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; + /// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::new( - /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())], cx), + /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())].into(), KeybindSource::Base), /// Hsla::black() /// ) /// .prefix("Cut:"); @@ -165,10 +170,11 @@ impl KeybindingHint { /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; + /// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::new( - /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())], cx), + /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())].into(), KeybindSource::Base), /// Hsla::black() /// ) /// .suffix("Find"); @@ -189,10 +195,11 @@ impl KeybindingHint { /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; /// use ui::prelude::*; /// use ui::{KeyBinding, KeybindingHint}; + /// use settings::KeybindSource; /// /// # fn example(cx: &App) { /// let hint = KeybindingHint::new( - /// KeyBinding::new(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())], cx), + /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())].into(), KeybindSource::Base), /// Hsla::black() /// ) /// .size(Pixels::from(16.0)); @@ -265,10 +272,8 @@ impl Component for KeybindingHint { Some("Displays a keyboard shortcut hint with optional prefix and suffix text") } - fn preview(window: &mut Window, cx: &mut App) -> Option { - let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None); - let enter = KeyBinding::for_action(&menu::Confirm, window, cx) - .unwrap_or(KeyBinding::new_from_gpui(enter_fallback, cx)); + fn preview(_window: &mut Window, cx: &mut App) -> Option { + let enter = KeyBinding::for_action(&menu::Confirm, cx); let bg_color = cx.theme().colors().surface_background; diff --git a/crates/ui/src/components/stories/keybinding.rs b/crates/ui/src/components/stories/keybinding.rs index 594f70b6ab0fbafc5e997785c44c494b71320d72..5840a11cf702f7a47aed06791ab47f12e2418d9c 100644 --- a/crates/ui/src/components/stories/keybinding.rs +++ b/crates/ui/src/components/stories/keybinding.rs @@ -1,6 +1,7 @@ use gpui::NoAction; use gpui::Render; use itertools::Itertools; +use settings::KeybindSource; use story::Story; use crate::{KeyBinding, prelude::*}; @@ -15,19 +16,36 @@ impl Render for KeybindingStory { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); + const SOURCE: KeybindSource = KeybindSource::Base; + Story::container(cx) .child(Story::title_for::(cx)) .child(Story::label("Single Key", cx)) - .child(KeyBinding::new_from_gpui(binding("Z"), cx)) + .child(KeyBinding::from_keystrokes( + binding("Z").keystrokes().into(), + SOURCE, + )) .child(Story::label("Single Key with Modifier", cx)) .child( div() .flex() .gap_3() - .child(KeyBinding::new_from_gpui(binding("ctrl-c"), cx)) - .child(KeyBinding::new_from_gpui(binding("alt-c"), cx)) - .child(KeyBinding::new_from_gpui(binding("cmd-c"), cx)) - .child(KeyBinding::new_from_gpui(binding("shift-c"), cx)), + .child(KeyBinding::from_keystrokes( + binding("ctrl-c").keystrokes().into(), + SOURCE, + )) + .child(KeyBinding::from_keystrokes( + binding("alt-c").keystrokes().into(), + SOURCE, + )) + .child(KeyBinding::from_keystrokes( + binding("cmd-c").keystrokes().into(), + SOURCE, + )) + .child(KeyBinding::from_keystrokes( + binding("shift-c").keystrokes().into(), + SOURCE, + )), ) .child(Story::label("Single Key with Modifier (Permuted)", cx)) .child( @@ -41,58 +59,77 @@ impl Render for KeybindingStory { .gap_4() .py_3() .children(chunk.map(|permutation| { - KeyBinding::new_from_gpui( - binding(&(permutation.join("-") + "-x")), - cx, + KeyBinding::from_keystrokes( + binding(&(permutation.join("-") + "-x")) + .keystrokes() + .into(), + SOURCE, ) })) }), ), ) .child(Story::label("Single Key with All Modifiers", cx)) - .child(KeyBinding::new_from_gpui( - binding("ctrl-alt-cmd-shift-z"), - cx, + .child(KeyBinding::from_keystrokes( + binding("ctrl-alt-cmd-shift-z").keystrokes().into(), + SOURCE, )) .child(Story::label("Chord", cx)) - .child(KeyBinding::new_from_gpui(binding("a z"), cx)) + .child(KeyBinding::from_keystrokes( + binding("a z").keystrokes().into(), + SOURCE, + )) .child(Story::label("Chord with Modifier", cx)) - .child(KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx)) - .child(KeyBinding::new_from_gpui(binding("fn-s"), cx)) + .child(KeyBinding::from_keystrokes( + binding("ctrl-a shift-z").keystrokes().into(), + SOURCE, + )) + .child(KeyBinding::from_keystrokes( + binding("fn-s").keystrokes().into(), + SOURCE, + )) .child(Story::label("Single Key with All Modifiers (Linux)", cx)) .child( - KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx) - .platform_style(PlatformStyle::Linux), + KeyBinding::from_keystrokes( + binding("ctrl-alt-cmd-shift-z").keystrokes().into(), + SOURCE, + ) + .platform_style(PlatformStyle::Linux), ) .child(Story::label("Chord (Linux)", cx)) .child( - KeyBinding::new_from_gpui(binding("a z"), cx).platform_style(PlatformStyle::Linux), + KeyBinding::from_keystrokes(binding("a z").keystrokes().into(), SOURCE) + .platform_style(PlatformStyle::Linux), ) .child(Story::label("Chord with Modifier (Linux)", cx)) .child( - KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx) + KeyBinding::from_keystrokes(binding("ctrl-a shift-z").keystrokes().into(), SOURCE) .platform_style(PlatformStyle::Linux), ) .child( - KeyBinding::new_from_gpui(binding("fn-s"), cx).platform_style(PlatformStyle::Linux), + KeyBinding::from_keystrokes(binding("fn-s").keystrokes().into(), SOURCE) + .platform_style(PlatformStyle::Linux), ) .child(Story::label("Single Key with All Modifiers (Windows)", cx)) .child( - KeyBinding::new_from_gpui(binding("ctrl-alt-cmd-shift-z"), cx) - .platform_style(PlatformStyle::Windows), + KeyBinding::from_keystrokes( + binding("ctrl-alt-cmd-shift-z").keystrokes().into(), + SOURCE, + ) + .platform_style(PlatformStyle::Windows), ) .child(Story::label("Chord (Windows)", cx)) .child( - KeyBinding::new_from_gpui(binding("a z"), cx) + KeyBinding::from_keystrokes(binding("a z").keystrokes().into(), SOURCE) .platform_style(PlatformStyle::Windows), ) .child(Story::label("Chord with Modifier (Windows)", cx)) .child( - KeyBinding::new_from_gpui(binding("ctrl-a shift-z"), cx) + KeyBinding::from_keystrokes(binding("ctrl-a shift-z").keystrokes().into(), SOURCE) .platform_style(PlatformStyle::Windows), ) .child( - KeyBinding::new_from_gpui(binding("fn-s"), cx) + KeyBinding::from_keystrokes(binding("fn-s").keystrokes().into(), SOURCE) .platform_style(PlatformStyle::Windows), ) } diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 4bfb7d2fc3e38ba5af2d1734d28de75a51096811..8b4ff3f73163f38e19da80462e687db3d88efc6f 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -64,11 +64,11 @@ impl Tooltip { ) -> impl Fn(&mut Window, &mut App) -> AnyView + use { let title = title.into(); let action = action.boxed_clone(); - move |window, cx| { + move |_, cx| { cx.new(|cx| Self { title: Title::Str(title.clone()), meta: None, - key_binding: KeyBinding::for_action(action.as_ref(), window, cx), + key_binding: Some(KeyBinding::for_action(action.as_ref(), cx)), }) .into() } @@ -82,11 +82,15 @@ impl Tooltip { let title = title.into(); let action = action.boxed_clone(); let focus_handle = focus_handle.clone(); - move |window, cx| { + move |_, cx| { cx.new(|cx| Self { title: Title::Str(title.clone()), meta: None, - key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx), + key_binding: Some(KeyBinding::for_action_in( + action.as_ref(), + &focus_handle, + cx, + )), }) .into() } @@ -95,13 +99,12 @@ impl Tooltip { pub fn for_action( title: impl Into, action: &dyn Action, - window: &mut Window, cx: &mut App, ) -> AnyView { cx.new(|cx| Self { title: Title::Str(title.into()), meta: None, - key_binding: KeyBinding::for_action(action, window, cx), + key_binding: Some(KeyBinding::for_action(action, cx)), }) .into() } @@ -110,13 +113,12 @@ impl Tooltip { title: impl Into, action: &dyn Action, focus_handle: &FocusHandle, - window: &mut Window, cx: &mut App, ) -> AnyView { cx.new(|cx| Self { title: title.into().into(), meta: None, - key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx), + key_binding: Some(KeyBinding::for_action_in(action, focus_handle, cx)), }) .into() } @@ -125,13 +127,12 @@ impl Tooltip { title: impl Into, action: Option<&dyn Action>, meta: impl Into, - window: &mut Window, cx: &mut App, ) -> AnyView { cx.new(|cx| Self { title: title.into().into(), meta: Some(meta.into()), - key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)), + key_binding: action.map(|action| KeyBinding::for_action(action, cx)), }) .into() } @@ -141,14 +142,12 @@ impl Tooltip { action: Option<&dyn Action>, meta: impl Into, focus_handle: &FocusHandle, - window: &mut Window, cx: &mut App, ) -> AnyView { cx.new(|cx| Self { title: title.into().into(), meta: Some(meta.into()), - key_binding: action - .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)), + key_binding: action.map(|action| KeyBinding::for_action_in(action, focus_handle, cx)), }) .into() } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 5958ba210f2dc984c3a8d698013a69548bbb3fcf..05af5d080c4c965f3d53f61b5af144a456ce0074 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -948,8 +948,8 @@ impl Render for PanelButtons { } }) .when(!is_active, |this| { - this.tooltip(move |window, cx| { - Tooltip::for_action(tooltip.clone(), &*action, window, cx) + this.tooltip(move |_window, cx| { + Tooltip::for_action(tooltip.clone(), &*action, cx) }) }) }), diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs index 897190e9aecb97152434c695b823e0aee3148dcb..eb6c8f3299838c1a01777885009fa67271b924d7 100644 --- a/crates/workspace/src/invalid_item_view.rs +++ b/crates/workspace/src/invalid_item_view.rs @@ -75,7 +75,7 @@ impl Focusable for InvalidItemView { } impl Render for InvalidItemView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { let abs_path = self.abs_path.clone(); v_flex() .size_full() @@ -103,11 +103,7 @@ impl Render for InvalidItemView { cx.open_with_system(&abs_path); }) .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action( - &OpenWithSystem, - window, - cx, - )), + .key_binding(KeyBinding::for_action(&OpenWithSystem, cx)), ), ) }), diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 1a0dd2f2c8416ec604ef74d2f3c4908eb0ddb57f..70be040df7c3718ba903565100b8548dcfc8b785 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -315,19 +315,17 @@ impl Render for LanguageServerPrompt { ) .child( IconButton::new(close_id, close_icon) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if suppress { Tooltip::for_action( "Suppress.\nClose with click.", &SuppressNotification, - window, cx, ) } else { Tooltip::for_action( "Close.\nSuppress with shift-click.", &menu::Cancel, - window, cx, ) } @@ -556,23 +554,21 @@ impl RenderOnce for NotificationFrame { this.on_modifiers_changed(move |_, _, cx| cx.notify(entity)) .child( IconButton::new(close_id, close_icon) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { if suppress { Tooltip::for_action( "Suppress.\nClose with click.", &SuppressNotification, - window, cx, ) } else if show_suppress_button { Tooltip::for_action( "Close.\nSuppress with shift-click.", &menu::Cancel, - window, cx, ) } else { - Tooltip::for_action("Close", &menu::Cancel, window, cx) + Tooltip::for_action("Close", &menu::Cancel, cx) } }) .on_click({ diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 68900e1156c56e03dcc1b335a93502da771bdc33..178fbdff9f7a9ef8cf4ee293450e0a5b9ad549b3 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2730,12 +2730,11 @@ impl Pane { .map(|this| { if is_active { let focus_handle = focus_handle.clone(); - this.tooltip(move |window, cx| { + this.tooltip(move |_window, cx| { Tooltip::for_action_in( end_slot_tooltip_text, end_slot_action, &focus_handle, - window, cx, ) }) @@ -3038,9 +3037,7 @@ impl Pane { .disabled(!self.can_navigate_backward()) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, window, cx) - } + move |_window, cx| Tooltip::for_action_in("Go Back", &GoBack, &focus_handle, cx) }); let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) @@ -3056,8 +3053,8 @@ impl Pane { .disabled(!self.can_navigate_forward()) .tooltip({ let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, window, cx) + move |_window, cx| { + Tooltip::for_action_in("Go Forward", &GoForward, &focus_handle, cx) } }); @@ -3660,11 +3657,10 @@ fn default_render_tab_bar_buttons( .on_click(cx.listener(|pane, _, window, cx| { pane.toggle_zoom(&crate::ToggleZoom, window, cx); })) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::for_action( if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, - window, cx, ) }) diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index 00f083f353daab677265a2410823c69be0bc5e8f..29067400bd72fe56a62af118a0bea6b52d9356df 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -319,13 +319,7 @@ impl ThemePreview { .style(ButtonStyle::Transparent) .tooltip(move |window, cx| { let name = name.clone(); - Tooltip::with_meta( - name, - None, - format!("{:?}", color), - window, - cx, - ) + Tooltip::with_meta(name, None, format!("{:?}", color), cx) }), ) })), diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 6c8dc975b567ada889737c3f5def064b9b50e9fe..a25074d46f356bbea5de986055b93557e73a8383 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -655,8 +655,8 @@ impl RenderOnce for QuickActionBarButton { .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .toggle_state(self.toggled) - .tooltip(move |window, cx| { - Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, window, cx) + .tooltip(move |_window, cx| { + Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx) }) .on_click(move |event, window, cx| (self.on_click)(event, window, cx)) } diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs index fb5a75f78d834ab3943e9dfd87cc7744fc453fcd..630d243cf6971ecebda694091acbfd5ba4c049e4 100644 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -68,7 +68,7 @@ impl QuickActionBar { let button = IconButton::new(button_id, IconName::Eye) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) - .tooltip(move |window, cx| { + .tooltip(move |_window, cx| { Tooltip::with_meta( tooltip_text, Some(open_action_for_tooltip), @@ -76,7 +76,6 @@ impl QuickActionBar { "{} to open in a split", text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) ), - window, cx, ) }) diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index 8028865b057f0c6c3b49efc3a5c3c640208e65aa..cc1787ab01c6dd8f6429c3ac821a485355629462 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -382,11 +382,7 @@ impl RateCompletionModal { ) } - fn render_active_completion( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { + fn render_active_completion(&mut self, cx: &mut Context) -> Option { let active_completion = self.active_completion.as_ref()?; let completion_id = active_completion.completion.id; let focus_handle = &self.focus_handle(cx); @@ -500,7 +496,6 @@ impl RateCompletionModal { .key_binding(KeyBinding::for_action_in( &ThumbsDownActiveCompletion, focus_handle, - window, cx )) .on_click(cx.listener(move |this, _, window, cx| { @@ -521,7 +516,6 @@ impl RateCompletionModal { .key_binding(KeyBinding::for_action_in( &ThumbsUpActiveCompletion, focus_handle, - window, cx )) .on_click(cx.listener(move |this, _, window, cx| { @@ -658,7 +652,7 @@ impl Render for RateCompletionModal { ) ), ) - .children(self.render_active_completion(window, cx)) + .children(self.render_active_completion( cx)) .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))) } } diff --git a/crates/zeta2_tools/src/zeta2_tools.rs b/crates/zeta2_tools/src/zeta2_tools.rs index dbb7a5af7d84c6cf043451ae6412e4e2cacc6408..2319df2a49d04c7e73180830ecf9778380bbf025 100644 --- a/crates/zeta2_tools/src/zeta2_tools.rs +++ b/crates/zeta2_tools/src/zeta2_tools.rs @@ -873,16 +873,14 @@ impl Zeta2Inspector { }) } - fn render_content(&self, window: &mut Window, cx: &mut Context) -> AnyElement { + fn render_content(&self, _: &mut Window, cx: &mut Context) -> AnyElement { if !cx.has_flag::() { return Self::render_message("`zeta2` feature flag is not enabled"); } match self.last_prediction.as_ref() { None => Self::render_message("No prediction"), - Some(prediction) => self - .render_last_prediction(prediction, window, cx) - .into_any(), + Some(prediction) => self.render_last_prediction(prediction, cx).into_any(), } } @@ -895,12 +893,7 @@ impl Zeta2Inspector { .into_any() } - fn render_last_prediction( - &self, - prediction: &LastPrediction, - window: &mut Window, - cx: &mut Context, - ) -> Div { + fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context) -> Div { match &self.active_view { ActiveView::Context => div().size_full().child(prediction.context_editor.clone()), ActiveView::Inference => h_flex() @@ -989,13 +982,12 @@ impl Zeta2Inspector { *feedback_state == Some(Feedback::Positive), |this| this.style(ButtonStyle::Filled), ) - .children( + .child( KeyBinding::for_action( &Zeta2RatePredictionPositive, - window, cx, ) - .map(|k| k.size(TextSize::Small.rems(cx))), + .size(TextSize::Small.rems(cx)), ) .child(ui::Icon::new(ui::IconName::ThumbsUp)) .on_click(cx.listener( @@ -1014,13 +1006,12 @@ impl Zeta2Inspector { *feedback_state == Some(Feedback::Negative), |this| this.style(ButtonStyle::Filled), ) - .children( + .child( KeyBinding::for_action( &Zeta2RatePredictionNegative, - window, cx, ) - .map(|k| k.size(TextSize::Small.rems(cx))), + .size(TextSize::Small.rems(cx)), ) .child(ui::Icon::new(ui::IconName::ThumbsDown)) .on_click(cx.listener( From 93136a9aaaa28527e0347be81b8850bbd2b9c1a7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 15:59:39 -0400 Subject: [PATCH 165/202] Update extension docs to mention GNU GPLv3 is a valid license (#40933) Merge after: - https://github.com/zed-industries/extensions/pull/3641 Release Notes: - N/A --- docs/src/extensions/developing-extensions.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index 5fdff6ff0810f6075d43c91af5a5f22e1d154d21..0e60048075e36aa90200a790f27a2494093b0f4a 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -113,8 +113,9 @@ git submodule update As of October 1st, 2025, extension repositories must include one of the following licenses: -- [MIT](https://opensource.org/license/mit) - [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) +- [GNU GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) +- [MIT](https://opensource.org/license/mit) This allows us to distribute the resulting binary produced from your extension code to our users. Without a valid license, the pull request to add or update your extension in the following steps will fail CI. From 6ed9c0271d15ef5bbe899ebd403b1fad1ba29991 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Wed, 22 Oct 2025 13:01:45 -0700 Subject: [PATCH 166/202] Don't migrate empty formatter array (#40932) Follow up for #40409 Fix for https://github.com/zed-industries/zed/issues/40874#issuecomment-3433759849 Release Notes: - Fixed an issue where having an empty formatter array in your settings `"formatter": []` would result in an erroneous prompt to migrate settings --- .../src/migrations/m_2025_10_16/settings.rs | 3 +++ crates/migrator/src/migrator.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/crates/migrator/src/migrations/m_2025_10_16/settings.rs b/crates/migrator/src/migrations/m_2025_10_16/settings.rs index 684a331af90ecd097376e629a3eb65a24d0609ae..3fa8c509b1f3910f48603a10a0fd0f448992c151 100644 --- a/crates/migrator/src/migrations/m_2025_10_16/settings.rs +++ b/crates/migrator/src/migrations/m_2025_10_16/settings.rs @@ -37,6 +37,9 @@ fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Res } else { vec![formatter.clone()] }; + if formatter_array.is_empty() { + return Ok(()); + } let mut code_action_formatters = Vec::new(); for formatter in formatter_array { let Some(code_action) = formatter.get("code_action") else { diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index 17cedaab666cd3ee53478456fbce8198ae65d8d2..ecf44b13715530b91fbfe0419216a2b5278cdb50 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -2092,6 +2092,21 @@ mod tests { .unindent(), ), ); + + assert_migrate_settings_with_migrations( + &[MigrationType::Json( + migrations::m_2025_10_16::restore_code_actions_on_format, + )], + &r#"{ + "formatter": [], + "code_actions_on_format": { + "bar": true, + "baz": false + } + }"# + .unindent(), + None, + ); } #[test] From ab22478ed4e10593965454b1e0657219dc4b1e79 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 22 Oct 2025 16:34:38 -0400 Subject: [PATCH 167/202] Update extension docs to mention BSD 3-Clause is a valid license (#40934) Release Notes: - N/A --- docs/src/extensions/developing-extensions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/extensions/developing-extensions.md b/docs/src/extensions/developing-extensions.md index 0e60048075e36aa90200a790f27a2494093b0f4a..2b675173ce24d42b0626f2fa821a404b14e6ef4d 100644 --- a/docs/src/extensions/developing-extensions.md +++ b/docs/src/extensions/developing-extensions.md @@ -111,9 +111,11 @@ git submodule update ## Extension License Requirements -As of October 1st, 2025, extension repositories must include one of the following licenses: +As of October 1st, 2025, extension repositories must include a license. +The following licenses are accepted: - [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0) +- [BSD 3-Clause](https://opensource.org/license/bsd-3-clause) - [GNU GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html) - [MIT](https://opensource.org/license/mit) From 7880e2b961e8a036b80d87c65cd7146dcb232bac Mon Sep 17 00:00:00 2001 From: Ted Robertson <10043369+tredondo@users.noreply.github.com> Date: Wed, 22 Oct 2025 22:09:34 +0100 Subject: [PATCH 168/202] docs: Clarify providers in edit-prediction.md (#39655) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Co-authored-by: Danilo Leal --- docs/src/ai/edit-prediction.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/ai/edit-prediction.md b/docs/src/ai/edit-prediction.md index 4508e304b9c12959082a8cbe3590197a722fe1a8..3c653284b015f33c9457338c6932289e95c6babd 100644 --- a/docs/src/ai/edit-prediction.md +++ b/docs/src/ai/edit-prediction.md @@ -1,7 +1,11 @@ # Edit Prediction -Edit Prediction is Zed's native mechanism for predicting the code you want to write through AI. -Each keystroke sends a new request to our [open source, open dataset Zeta model](https://huggingface.co/zed-industries/zeta) and it returns with individual or multi-line suggestions that can be quickly accepted by pressing `tab`. +Edit Prediction is Zed's mechanism for predicting the code you want to write through AI. +Each keystroke sends a new request to the edit prediction provider, which returns individual or multi-line suggestions that can be quickly accepted by pressing `tab`. + +The default provider is [Zeta, a proprietary open source and open dataset model](https://huggingface.co/zed-industries/zeta), which [requires being signed into Zed](../accounts.md#what-features-require-signing-in). + +Alternatively, you can use other providers like [GitHub Copilot](#github-copilot) (or [Enterprise](#github-copilot-enterprise)) or [Supermaven](#supermaven). ## Configuring Zeta From 2096f256f2ba5a3ebe9abf9e242130a04d12df00 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 23 Oct 2025 03:57:10 +0530 Subject: [PATCH 169/202] editor: Reduce selection opacity when editor is not focused (#40925) Focus: image Unfocus: image Release Notes: - Reduced selection opacity when the editor is out of focus to make inactive states clearer. --- crates/editor/src/element.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b5c1fecbea003d15e336738d7e68e1c4ec59f14f..efb0bf9a1e5c8a7704eb776e050bc36b4539a99e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1422,7 +1422,11 @@ impl EditorElement { layouts.push(layout); } - let player = editor.current_user_player_color(cx); + let mut player = editor.current_user_player_color(cx); + if !editor.is_focused(window) { + const UNFOCUS_EDITOR_SELECTION_OPACITY: f32 = 0.5; + player.selection = player.selection.opacity(UNFOCUS_EDITOR_SELECTION_OPACITY); + } selections.push((player, layouts)); if let SelectionDragState::Dragging { From 731237222e7e8f8292dc39135c07fe6de4523313 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 22 Oct 2025 19:29:35 -0300 Subject: [PATCH 170/202] agent_ui: Focus the message editor after regenerating a user message (#40938) Release Notes: - agent: Improved the editing previous messages UX by focusing in the agent panel's message editor after regenerating a prompt, instead of moving focus to the nearest regular buffer. --- crates/agent_ui/src/acp/thread_view.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 47ddd705bd653eb5c9635dd0307e9ebbc2638378..adf279c82036e8f8219c5647f016ec4fc887a046 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1259,6 +1259,7 @@ impl AcpThreadView { .await?; this.update_in(cx, |this, window, cx| { this.send_impl(message_editor, window, cx); + this.focus_handle(cx).focus(window); })?; anyhow::Ok(()) }) From ca4103246fe195527133542bad9a1d19fba22b86 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:33:45 -0400 Subject: [PATCH 171/202] settings_ui: Fix file header from showing duplicate display names (#40943) After clicking on the file drop-down and selecting a file, both the selected file and the first drop-down entry would be the same file name instead of overwriting a file name. Release Notes: - settings ui: Fix bug where duplicate file names showed in the header files --- crates/settings_ui/src/settings_ui.rs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 4469dacc35ba4538addee24bd673e6817832ae31..d4c94b2b094fece6730b877f8a127b7451545ce2 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1702,7 +1702,7 @@ impl SettingsWindow { .iter() .any(|(file, _)| file == &self.current_file); if !current_file_still_exists { - self.change_file(0, window, false, cx); + self.change_file(0, false, window, cx); } } @@ -1735,8 +1735,8 @@ impl SettingsWindow { fn change_file( &mut self, ix: usize, - window: &mut Window, drop_down_file: bool, + window: &mut Window, cx: &mut Context, ) { if ix >= self.files.len() { @@ -1785,7 +1785,7 @@ impl SettingsWindow { .on_click(cx.listener({ let focus_handle = focus_handle.clone(); move |this, _: &gpui::ClickEvent, window, cx| { - this.change_file(ix, window, false, cx); + this.change_file(ix, false, window, cx); focus_handle.focus(window); } })) @@ -1834,23 +1834,35 @@ impl SettingsWindow { "more-files", format!("+{}", self.files.len() - (OVERFLOW_LIMIT + 1)), ContextMenu::build(window, cx, move |mut menu, _, _| { - for (ix, (file, focus_handle)) in self + for (mut ix, (file, focus_handle)) in self .files .iter() .enumerate() .skip(OVERFLOW_LIMIT + 1) { + let (display_name, focus_handle) = if self + .drop_down_file + .is_some_and(|drop_down_ix| drop_down_ix == ix) + { + ix = OVERFLOW_LIMIT; + ( + self.display_name(&self.files[ix].0), + self.files[ix].1.clone(), + ) + } else { + (self.display_name(&file), focus_handle.clone()) + }; + menu = menu.entry( - self.display_name(file) + display_name .expect("Files should always have a name"), None, { let this = this.clone(); - let focus_handle = focus_handle.clone(); move |window, cx| { this.update(cx, |this, cx| { this.change_file( - ix, window, true, cx, + ix, true, window, cx, ); }); focus_handle.focus(window); From c16f2a1a29325969c0f2e39903f89e0de2dbb4e3 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 00:47:16 +0200 Subject: [PATCH 172/202] project: Normalize `Path` env var to `PATH` in shell env on windows (#40720) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/languages/src/python.rs | 10 +++++++--- crates/project/src/environment.rs | 7 ++++++- crates/project/src/lsp_store.rs | 7 +------ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index a9300dbb5ddca610e8d946b0cec211a7d9aa28df..c255ed3f09f733321c1066520b12355f76941931 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1344,9 +1344,13 @@ impl pet_core::os_environment::Environment for EnvironmentApi<'_> { fn get_know_global_search_locations(&self) -> Vec { if self.global_search_locations.lock().is_empty() { - let mut paths = - std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default()) - .collect::>(); + let mut paths = std::env::split_paths( + &self + .get_env_var("PATH".to_string()) + .or_else(|| self.get_env_var("Path".to_string())) + .unwrap_or_default(), + ) + .collect::>(); log::trace!("Env PATH: {:?}", paths); for p in self.pet_env.get_know_global_search_locations() { diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index 3d42bb217faae7645301aa0b7f9e3a857d418a7b..c888cdd11f18a4c118665eac7dbbb6037e70bba4 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -265,7 +265,7 @@ async fn load_shell_environment( (Some(fake_env), None) } else if cfg!(target_os = "windows") { let (shell, args) = shell.program_and_args(); - let envs = match shell_env::capture(shell, args, dir).await { + let mut envs = match shell_env::capture(shell, args, dir).await { Ok(envs) => envs, Err(err) => { util::log_err(&err); @@ -278,6 +278,11 @@ async fn load_shell_environment( ); } }; + if let Some(path) = envs.remove("Path") { + // windows env vars are case-insensitive, so normalize the path var + // so we can just assume `PATH` in other places + envs.insert("PATH".into(), path); + } // Note: direnv is not available on Windows, so we skip direnv processing // and just return the shell environment diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index dc082453fd74d9d6e046e99e1e75de0f7e4c544e..f33d7af8b6ede6f1ae62109f1390acee51975693 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -13271,12 +13271,7 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate { let env = self.shell_env().await; - // On Windows, PATH might be "Path" instead of "PATH" - let shell_path = env - .get("PATH") - .or_else(|| env.get("Path")) - .or_else(|| env.get("path")) - .cloned(); + let shell_path = env.get("PATH").cloned(); which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() } From a96bf504e03a1fe47f7fb045d4849e063d5db03f Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 23 Oct 2025 04:23:25 +0530 Subject: [PATCH 173/202] theme: Fix entry could appear transparent on hover with certain themes (#40944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up: https://github.com/zed-industries/zed/pull/34655 We should use an opaque fallback color for `panel.overlay_hover`. This helps when a custom theme doesn’t provide it, nor `element.hover`. For example, VSCode’s default modern dark theme doesn’t include an `element.hover` color after import. Release Notes: - Fixed an issue where the project panel’s sticky entry could appear transparent on hover with certain themes. --- crates/theme/src/default_colors.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 80ad845e989b244a5bcd5eb529720d10416ea7bc..a9cd163b8c634f6c3fd8061164b72f8b54127c81 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -85,7 +85,7 @@ impl ThemeColors { panel_indent_guide_hover: neutral().light_alpha().step_6(), panel_indent_guide_active: neutral().light_alpha().step_6(), panel_overlay_background: neutral().light().step_2(), - panel_overlay_hover: neutral().light_alpha().step_4(), + panel_overlay_hover: neutral().light().step_4(), pane_focused_border: blue().light().step_5(), pane_group_border: neutral().light().step_6(), scrollbar_thumb_background: neutral().light_alpha().step_3(), @@ -220,7 +220,7 @@ impl ThemeColors { panel_indent_guide_hover: neutral().dark_alpha().step_6(), panel_indent_guide_active: neutral().dark_alpha().step_6(), panel_overlay_background: neutral().dark().step_2(), - panel_overlay_hover: neutral().dark_alpha().step_4(), + panel_overlay_hover: neutral().dark().step_4(), pane_focused_border: blue().dark().step_5(), pane_group_border: neutral().dark().step_6(), scrollbar_thumb_background: neutral().dark_alpha().step_3(), From 044701e3a59ee2bf4b70b69449ecc26d620b673d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 01:05:43 +0200 Subject: [PATCH 174/202] rope: Implement `Rope::is_char_boundary` via chars bitmap (#40945) Slightly more efficient. No new tests as we already have tests verifiying this. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/rope/src/chunk.rs | 9 +++++++-- crates/rope/src/rope.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 515ae8c768657681de1614ba03fdce6d176b96ca..51904cd8e2217dc56947f6026fff674147cffea5 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -93,6 +93,11 @@ impl Chunk { pub fn tabs(&self) -> Bitmap { self.tabs } + + #[inline(always)] + pub fn is_char_boundary(&self, offset: usize) -> bool { + (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() + } } #[derive(Clone, Copy, Debug)] @@ -123,8 +128,8 @@ impl<'a> ChunkSlice<'a> { } #[inline(always)] - pub fn is_char_boundary(self, offset: usize) -> bool { - self.text.is_char_boundary(offset) + pub fn is_char_boundary(&self, offset: usize) -> bool { + (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() } #[inline(always)] diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index c710ed86570ef5aadd7f7a31f9c440226b073ae8..204b13cfae2c27441de1d9265cd964fa1b4215af 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -44,7 +44,7 @@ impl Rope { } let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); let chunk_offset = offset - start; - item.map(|chunk| chunk.text.is_char_boundary(chunk_offset)) + item.map(|chunk| chunk.is_char_boundary(chunk_offset)) .unwrap_or(false) } From 4fd4cbbfb7655fe39356f972eaffbaf98185b8ef Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 22 Oct 2025 16:36:18 -0700 Subject: [PATCH 175/202] gpui: Add focus-visible selector support (#40940) Release Notes: - N/A --------- Co-authored-by: Claude --- crates/gpui/examples/focus_visible.rs | 214 ++++++++++++++++++++++++++ crates/gpui/src/elements/div.rs | 20 +++ crates/gpui/src/window.rs | 17 ++ 3 files changed, 251 insertions(+) create mode 100644 crates/gpui/examples/focus_visible.rs diff --git a/crates/gpui/examples/focus_visible.rs b/crates/gpui/examples/focus_visible.rs new file mode 100644 index 0000000000000000000000000000000000000000..737317cabadb7d3358c9c0497b52d4c2ff2e1028 --- /dev/null +++ b/crates/gpui/examples/focus_visible.rs @@ -0,0 +1,214 @@ +use gpui::{ + App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, + Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size, +}; + +actions!(example, [Tab, TabPrev, Quit]); + +struct Example { + focus_handle: FocusHandle, + items: Vec<(FocusHandle, &'static str)>, + message: SharedString, +} + +impl Example { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let items = vec![ + ( + cx.focus_handle().tab_index(1).tab_stop(true), + "Button with .focus() - always shows border when focused", + ), + ( + cx.focus_handle().tab_index(2).tab_stop(true), + "Button with .focus_visible() - only shows border with keyboard", + ), + ( + cx.focus_handle().tab_index(3).tab_stop(true), + "Button with both .focus() and .focus_visible()", + ), + ]; + + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle); + + Self { + focus_handle, + items, + message: SharedString::from( + "Try clicking vs tabbing! Click shows no border, Tab shows border.", + ), + } + } + + fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context) { + window.focus_next(); + self.message = SharedString::from("Pressed Tab - focus-visible border should appear!"); + } + + fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context) { + window.focus_prev(); + self.message = + SharedString::from("Pressed Shift-Tab - focus-visible border should appear!"); + } + + fn on_quit(&mut self, _: &Quit, _window: &mut Window, cx: &mut Context) { + cx.quit(); + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn button_base(id: impl Into, label: &'static str) -> Stateful
{ + div() + .id(id) + .h_16() + .w_full() + .flex() + .justify_center() + .items_center() + .bg(gpui::rgb(0x2563eb)) + .text_color(gpui::white()) + .rounded_md() + .cursor_pointer() + .hover(|style| style.bg(gpui::rgb(0x1d4ed8))) + .child(label) + } + + div() + .id("app") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_tab)) + .on_action(cx.listener(Self::on_tab_prev)) + .on_action(cx.listener(Self::on_quit)) + .size_full() + .flex() + .flex_col() + .p_8() + .gap_6() + .bg(gpui::rgb(0xf3f4f6)) + .child( + div() + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x111827)) + .child("CSS focus-visible Demo"), + ) + .child( + div() + .p_4() + .rounded_md() + .bg(gpui::rgb(0xdbeafe)) + .text_color(gpui::rgb(0x1e3a8a)) + .child(self.message.clone()), + ) + .child( + div() + .flex() + .flex_col() + .gap_4() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x374151)) + .child("1. Regular .focus() - always visible:"), + ) + .child( + button_base("button1", self.items[0].1) + .track_focus(&self.items[0].0) + .focus(|style| { + style.border_4().border_color(gpui::rgb(0xfbbf24)) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.message = + "Clicked button 1 - focus border is visible!".into(); + cx.notify(); + })), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x374151)) + .child("2. New .focus_visible() - only keyboard:"), + ) + .child( + button_base("button2", self.items[1].1) + .track_focus(&self.items[1].0) + .focus_visible(|style| { + style.border_4().border_color(gpui::rgb(0x10b981)) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.message = + "Clicked button 2 - no border! Try Tab instead.".into(); + cx.notify(); + })), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::BOLD) + .text_color(gpui::rgb(0x374151)) + .child( + "3. Both .focus() (yellow) and .focus_visible() (green):", + ), + ) + .child( + button_base("button3", self.items[2].1) + .track_focus(&self.items[2].0) + .focus(|style| { + style.border_4().border_color(gpui::rgb(0xfbbf24)) + }) + .focus_visible(|style| { + style.border_4().border_color(gpui::rgb(0x10b981)) + }) + .on_click(cx.listener(|this, _, _, cx| { + this.message = + "Clicked button 3 - yellow border. Tab shows green!" + .into(); + cx.notify(); + })), + ), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + cx.bind_keys([ + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + KeyBinding::new("cmd-q", Quit, None), + ]); + + let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 4d4e176919784f7d7fba68f68cc00b8e7ff92922..efc931f05ffbed2a0b20f23967f20f9e0704b454 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1034,6 +1034,18 @@ pub trait InteractiveElement: Sized { self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default()))); self } + + /// Set the given styles to be applied when this element is focused via keyboard navigation. + /// This is similar to CSS's `:focus-visible` pseudo-class - it only applies when the element + /// is focused AND the user is navigating via keyboard (not mouse clicks). + /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`]. + fn focus_visible(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_visible_style = Some(Box::new(f(StyleRefinement::default()))); + self + } } /// A trait for elements that want to use the standard GPUI interactivity features @@ -1497,6 +1509,7 @@ pub struct Interactivity { pub base_style: Box, pub(crate) focus_style: Option>, pub(crate) in_focus_style: Option>, + pub(crate) focus_visible_style: Option>, pub(crate) hover_style: Option>, pub(crate) group_hover_style: Option, pub(crate) active_style: Option>, @@ -2492,6 +2505,13 @@ impl Interactivity { { style.refine(focus_style); } + + if let Some(focus_visible_style) = self.focus_visible_style.as_ref() + && focus_handle.is_focused(window) + && window.last_input_was_keyboard() + { + style.refine(focus_visible_style); + } } if let Some(hitbox) = hitbox { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 6d74a0e11f7a7ecde003f48b084f4720bd03230e..88076f3af13b5d95e820e29a59913156eae07065 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -863,6 +863,7 @@ pub struct Window { hovered: Rc>, pub(crate) needs_present: Rc>, pub(crate) last_input_timestamp: Rc>, + last_input_was_keyboard: bool, pub(crate) refreshing: bool, pub(crate) activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) focus: Option, @@ -1246,6 +1247,7 @@ impl Window { hovered, needs_present, last_input_timestamp, + last_input_was_keyboard: false, refreshing: false, activation_observers: SubscriberSet::new(), focus: None, @@ -1899,6 +1901,12 @@ impl Window { self.modifiers } + /// Returns true if the last input event was keyboard-based (key press, tab navigation, etc.) + /// This is used for focus-visible styling to show focus indicators only for keyboard navigation. + pub fn last_input_was_keyboard(&self) -> bool { + self.last_input_was_keyboard + } + /// The current state of the keyboard's capslock pub fn capslock(&self) -> Capslock { self.capslock @@ -3580,6 +3588,15 @@ impl Window { #[profiling::function] pub fn dispatch_event(&mut self, event: PlatformInput, cx: &mut App) -> DispatchEventResult { self.last_input_timestamp.set(Instant::now()); + + // Track whether this input was keyboard-based for focus-visible styling + self.last_input_was_keyboard = matches!( + event, + PlatformInput::KeyDown(_) + | PlatformInput::KeyUp(_) + | PlatformInput::ModifiersChanged(_) + ); + // Handlers may set this to false by calling `stop_propagation`. cx.propagate_event = true; // Handlers may set this to true by calling `prevent_default`. From 6b8f8592ea3d6a85a0430a621a370bc6ec8a822c Mon Sep 17 00:00:00 2001 From: "Moo, Kachon" Date: Thu, 23 Oct 2025 06:54:24 +0700 Subject: [PATCH 176/202] agent_ui: Remove ellipses from some menu entries (#40858) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image Release Notes: This PR fixes inconsistent use of trailing ellipsis (…) in the MCP Server menu. Previously, some menu items (like Add Custom Server…) used hardcoded ellipses even though they didn’t trigger additional dialogs or steps. This change removes unnecessary ellipses to align with standard UI/UX conventions used across Zed’s menus. --------- Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> --- crates/agent_ui/src/agent_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 02403b0e8d48ed2bee58c79e15d27d28ae2b49d3..19f56b26b5b9621b92c307690baefd332da183b0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1663,7 +1663,7 @@ impl AgentPanel { .separator(); menu = menu - .action("Rules…", Box::new(OpenRulesLibrary::default())) + .action("Rules", Box::new(OpenRulesLibrary::default())) .action("Settings", Box::new(OpenSettings)) .separator() .action(full_screen_label, Box::new(ToggleZoom)); From bada88c5b30b91cbba5fdf3e6ab6e4fd389d65d6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 22 Oct 2025 20:57:01 -0300 Subject: [PATCH 177/202] Make the rules library window more consistent with the settings UI (#40948) Now that we have two surface areas that open as separate windows, it's important they're consistent with one another. This PR make the settings UI and rules library windows more similar by having them use the same minimum window size and similar styles for their navbar given they have fundamentally the same design (nav on the left and content on the right). Release Notes: - N/A --- crates/gpui/src/window.rs | 7 ++ crates/rules_library/src/rules_library.rs | 124 +++++++++++----------- crates/settings_ui/src/settings_ui.rs | 10 +- 3 files changed, 74 insertions(+), 67 deletions(-) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 88076f3af13b5d95e820e29a59913156eae07065..aa01e34bbc192cf63da94a9c2e4399ff40f7c9ff 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -60,6 +60,13 @@ pub use prompts::*; pub(crate) const DEFAULT_WINDOW_SIZE: Size = size(px(1536.), px(864.)); +/// A 6:5 aspect ratio minimum window size to be used for functional, +/// additional-to-main-Zed windows, like the settings and rules library windows. +pub const DEFAULT_ADDITIONAL_WINDOW_SIZE: Size = Size { + width: Pixels(900.), + height: Pixels(750.), +}; + /// Represents the two different phases when dispatching events. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] pub enum DispatchPhase { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 1d3eb8b55690c2344a40820e3c5df472bcfc1e05..3de1786f4c747d162d554d34b06424ec6102fcd4 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -3,9 +3,9 @@ use collections::{HashMap, HashSet}; use editor::{CompletionProvider, SelectionEffects}; use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; use gpui::{ - Action, App, Bounds, Entity, EventEmitter, Focusable, PromptLevel, Subscription, Task, - TextStyle, TitlebarOptions, WindowBounds, WindowHandle, WindowOptions, actions, point, size, - transparent_black, + Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, + PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, + WindowOptions, actions, point, size, transparent_black, }; use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; use language_model::{ @@ -129,13 +129,13 @@ pub fn open_rules_library( titlebar: Some(TitlebarOptions { title: Some("Rules Library".into()), appears_transparent: true, - traffic_light_position: Some(point(px(9.0), px(9.0))), + traffic_light_position: Some(point(px(12.0), px(12.0))), }), app_id: Some(app_id.to_owned()), window_bounds: Some(WindowBounds::Windowed(bounds)), window_background: cx.theme().window_background_appearance(), window_decorations: Some(window_decorations), - window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio + window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE), kind: gpui::WindowKind::Floating, ..Default::default() }, @@ -369,10 +369,9 @@ impl PickerDelegate for RulePickerDelegate { .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child( - h_flex() - .h_5() - .line_height(relative(1.)) - .child(Label::new(rule.title.clone().unwrap_or("Untitled".into()))), + Label::new(rule.title.clone().unwrap_or("Untitled".into())) + .truncate() + .mr_10(), ) .end_slot::(default.then(|| { IconButton::new("toggle-default-rule", IconName::Paperclip) @@ -453,13 +452,15 @@ impl PickerDelegate for RulePickerDelegate { cx: &mut Context>, ) -> Div { h_flex() - .bg(cx.theme().colors().editor_background) - .rounded_sm() - .overflow_hidden() - .flex_none() .py_1() - .px_2() + .px_1p5() .mx_1() + .gap_1p5() + .rounded_sm() + .bg(cx.theme().colors().editor_background) + .border_1() + .border_color(cx.theme().colors().border) + .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted)) .child(editor.clone()) } } @@ -1096,11 +1097,11 @@ impl RulesLibrary { v_flex() .id("rule-list") .capture_action(cx.listener(Self::focus_active_rule)) - .bg(cx.theme().colors().panel_background) + .px_1p5() .h_full() - .px_1() - .w_1_3() + .w_64() .overflow_x_hidden() + .bg(cx.theme().colors().panel_background) .child( h_flex() .p(DynamicSpacing::Base04.rems(cx)) @@ -1121,16 +1122,55 @@ impl RulesLibrary { .child(div().flex_grow().child(self.picker.clone())) } + fn render_active_rule_editor( + &self, + editor: &Entity, + cx: &mut Context, + ) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + + div() + .w_full() + .on_action(cx.listener(Self::move_down_from_title)) + .pl_1() + .border_1() + .border_color(transparent_black()) + .rounded_sm() + .group_hover("active-editor-header", |this| { + this.border_color(cx.theme().colors().border_variant) + }) + .child(EditorElement::new( + &editor, + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.theme().players().local(), + text: TextStyle { + color: cx.theme().colors().editor_foreground, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features.clone(), + font_size: HeadlineSize::Large.rems().into(), + font_weight: settings.ui_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }, + scrollbar_width: Pixels::ZERO, + syntax: cx.theme().syntax().clone(), + status: cx.theme().status().clone(), + inlay_hints_style: editor::make_inlay_hints_style(cx), + edit_prediction_styles: editor::make_suggestion_styles(cx), + ..EditorStyle::default() + }, + )) + } + fn render_active_rule(&mut self, cx: &mut Context) -> gpui::Stateful
{ div() - .w_2_3() - .h_full() .id("rule-editor") + .h_full() + .flex_grow() .border_l_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background) - .flex_none() - .min_w_64() .children(self.active_rule_id.and_then(|prompt_id| { let rule_metadata = self.store.read(cx).metadata(prompt_id)?; let rule_editor = &self.rule_editors[&prompt_id]; @@ -1138,7 +1178,6 @@ impl RulesLibrary { let model = LanguageModelRegistry::read_global(cx) .default_model() .map(|default| default.model); - let settings = ThemeSettings::get_global(cx); Some( v_flex() @@ -1158,46 +1197,7 @@ impl RulesLibrary { .gap_2() .justify_between() .child( - div() - .w_full() - .on_action(cx.listener(Self::move_down_from_title)) - .pl_1() - .border_1() - .border_color(transparent_black()) - .rounded_sm() - .group_hover("active-editor-header", |this| { - this.border_color(cx.theme().colors().border_variant) - }) - .child(EditorElement::new( - &rule_editor.title_editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.theme().players().local(), - text: TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.ui_font.family.clone(), - font_features: settings - .ui_font - .features - .clone(), - font_size: HeadlineSize::Large.rems().into(), - font_weight: settings.ui_font.weight, - line_height: relative( - settings.buffer_line_height.value(), - ), - ..Default::default() - }, - scrollbar_width: Pixels::ZERO, - syntax: cx.theme().syntax().clone(), - status: cx.theme().status().clone(), - inlay_hints_style: editor::make_inlay_hints_style( - cx, - ), - edit_prediction_styles: - editor::make_suggestion_styles(cx), - ..EditorStyle::default() - }, - )), + self.render_active_rule_editor(&rule_editor.title_editor, cx), ) .child( h_flex() diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index d4c94b2b094fece6730b877f8a127b7451545ce2..e78f4458c464dec0fab69c81bc1dd51ee3f128f7 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -6,10 +6,10 @@ use editor::{Editor, EditorEvent}; use feature_flags::FeatureFlag; use fuzzy::StringMatchCandidate; use gpui::{ - Action, App, Div, Entity, FocusHandle, Focusable, Global, ListState, ReadGlobal as _, - ScrollHandle, Stateful, Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, - WindowBounds, WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, size, - uniform_list, + Action, App, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, Focusable, Global, + ListState, ReadGlobal as _, ScrollHandle, Stateful, Subscription, Task, TitlebarOptions, + UniformListScrollHandle, Window, WindowBounds, WindowHandle, WindowOptions, actions, div, list, + point, prelude::*, px, uniform_list, }; use heck::ToTitleCase as _; use project::WorktreeId; @@ -575,7 +575,7 @@ pub fn open_settings_editor( cx.defer(move |cx| { let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into(); - let default_bounds = size(px(900.), px(750.)); // 4:3 Aspect Ratio + let default_bounds = DEFAULT_ADDITIONAL_WINDOW_SIZE; let default_rem_size = 16.0; let scale_factor = current_rem_size / default_rem_size; let scaled_bounds: gpui::Size = default_bounds.map(|axis| axis * scale_factor); From f9e0642a72e6bc680ba5ff5c3a6eb5e1bd5f6752 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:23:51 -0300 Subject: [PATCH 178/202] settings_ui: Adjust warning banner design (#40952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just tidying this up a bit. Really have to fix this Banner component at some point 😅 Having to add some spacing hacks to make it perfect here that are not ideal and should be baked into the component. Release Notes: - N/A --- crates/settings_ui/src/settings_ui.rs | 28 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index e78f4458c464dec0fab69c81bc1dd51ee3f128f7..de987da4b00fa5bc2a164f213a73e03523a46947 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -2575,17 +2575,23 @@ impl SettingsWindow { Banner::new() .severity(Severity::Warning) .child( - Label::new("Your Settings File is in an Invalid State. Setting Values May Be Incorrect, and Changes May Be Lost") - .size(LabelSize::Large), + v_flex() + .my_0p5() + .gap_0p5() + .child(Label::new("Your settings file is in an invalid state.")) + .child( + Label::new(error).size(LabelSize::Small).color(Color::Muted), + ), ) - .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)) .action_slot( - Button::new("fix-in-json", "Fix in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::OutlinedGhost) - .on_click(cx.listener(|this, _, _, cx| { - this.open_current_settings_file(cx); - })), + div().pr_1().child( + Button::new("fix-in-json", "Fix in settings.json") + .tab_index(0_isize) + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .on_click(cx.listener(|this, _, _, cx| { + this.open_current_settings_file(cx); + })), + ), ), ) .into_any_element() @@ -2652,8 +2658,6 @@ impl SettingsWindow { } window.focus_prev(); })) - .child(warning_banner) - .child(page_header) .when(sub_page_stack().is_empty(), |this| { this.vertical_scrollbar_for(self.list_state.clone(), window, cx) }) @@ -2665,6 +2669,8 @@ impl SettingsWindow { .pt_6() .px_8() .bg(cx.theme().colors().editor_background) + .child(warning_banner) + .child(page_header) .child( div() .size_full() From bf63ff2b9157a05985536bd4890cd9e3587f24e7 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 22 Oct 2025 22:44:25 -0400 Subject: [PATCH 179/202] Fix path for vscode-html-language-server when found on PATH (#40832) Don't prepend the worktree root when using an absolute path from `Worktree::which`, since that does the wrong thing when running in wasmtime given two Windows absolute paths. Also don't pass this path to `node`, since when npm installed it's a sh/cmd wrapper not a JS file. Part of #39153, also needs a fix on the vscode-langservers-extracted side (missing shebang for the vscode-html-language-server script). Release Notes: - Fixed Zed failing to run the HTML language server in some cases. --- extensions/html/src/html.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs index 27fd2d1e2226a764cdcc3de29d607f3d3db8fd5d..337689ebddd427769ab985ad82512f76b601e67c 100644 --- a/extensions/html/src/html.rs +++ b/extensions/html/src/html.rs @@ -68,22 +68,24 @@ impl zed::Extension for HtmlExtension { worktree: &zed::Worktree, ) -> Result { let server_path = if let Some(path) = worktree.which(BINARY_NAME) { - path + return Ok(zed::Command { + command: path, + args: vec!["--stdio".to_string()], + env: Default::default(), + }); } else { - self.server_script_path(language_server_id)? + let server_path = self.server_script_path(language_server_id)?; + env::current_dir() + .unwrap() + .join(&server_path) + .to_string_lossy() + .to_string() }; self.cached_binary_path = Some(server_path.clone()); Ok(zed::Command { command: zed::node_binary_path()?, - args: vec![ - env::current_dir() - .unwrap() - .join(&server_path) - .to_string_lossy() - .to_string(), - "--stdio".to_string(), - ], + args: vec![server_path, "--stdio".to_string()], env: Default::default(), }) } From 18daa9a839cfcc8e8d69fedbc9c9853c235e2972 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Wed, 22 Oct 2025 23:57:33 -0400 Subject: [PATCH 180/202] Simplify environment loading code (#40531) This is a refactoring PR to simplify our environment loading code by: - Getting rid of `EnvironmentErrorMessage` in favor of using `anyhow::Result` everywhere, with a separate `mpsc` channel to communicate statuses that will be shown in the activity indicator - Inlining some functions that were only called once to reduce indirection - Removing the separate `direnv` module Release Notes: - N/A --- .../src/activity_indicator.rs | 9 +- crates/project/src/direnv.rs | 82 ----- crates/project/src/environment.rs | 286 ++++++++---------- crates/project/src/project.rs | 14 +- crates/remote_server/src/headless_project.rs | 2 +- crates/util/src/util.rs | 7 +- 6 files changed, 148 insertions(+), 252 deletions(-) delete mode 100644 crates/project/src/direnv.rs diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 58e737dfcf26a75ead5a2894a9a1f4723ab0d331..84d1291dad6d235e8d90d21bfcaf78a7e2ec042d 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -11,8 +11,7 @@ use language::{ LanguageServerStatusUpdate, ServerHealth, }; use project::{ - EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project, - ProjectEnvironmentEvent, + LanguageServerProgress, LspStoreEvent, Project, ProjectEnvironmentEvent, git_store::{GitStoreEvent, Repository}, }; use smallvec::SmallVec; @@ -327,20 +326,20 @@ impl ActivityIndicator { .flatten() } - fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a EnvironmentErrorMessage> { + fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> { self.project.read(cx).peek_environment_error(cx) } fn content_to_render(&mut self, cx: &mut Context) -> Option { // Show if any direnv calls failed - if let Some(error) = self.pending_environment_error(cx) { + if let Some(message) = self.pending_environment_error(cx) { return Some(Content { icon: Some( Icon::new(IconName::Warning) .size(IconSize::Small) .into_any_element(), ), - message: error.0.clone(), + message: message.clone(), on_click: Some(Arc::new(move |this, window, cx| { this.project.update(cx, |project, cx| { project.pop_environment_error(cx); diff --git a/crates/project/src/direnv.rs b/crates/project/src/direnv.rs deleted file mode 100644 index 75c381dda96eb4f014310f9233c7557177f6eec9..0000000000000000000000000000000000000000 --- a/crates/project/src/direnv.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::environment::EnvironmentErrorMessage; -use std::process::ExitStatus; - -use {collections::HashMap, std::path::Path, util::ResultExt}; - -#[derive(Clone)] -pub enum DirenvError { - NotFound, - FailedRun, - NonZeroExit(ExitStatus, Vec), - InvalidJson, -} - -impl From for Option { - fn from(value: DirenvError) -> Self { - match value { - DirenvError::NotFound => None, - DirenvError::FailedRun | DirenvError::NonZeroExit(_, _) => { - Some(EnvironmentErrorMessage(String::from( - "Failed to run direnv. See logs for more info", - ))) - } - DirenvError::InvalidJson => Some(EnvironmentErrorMessage(String::from( - "Direnv returned invalid json. See logs for more info", - ))), - } - } -} - -pub async fn load_direnv_environment( - env: &HashMap, - dir: &Path, -) -> Result>, DirenvError> { - let Ok(direnv_path) = which::which("direnv") else { - return Err(DirenvError::NotFound); - }; - - let args = &["export", "json"]; - let Some(direnv_output) = smol::process::Command::new(&direnv_path) - .args(args) - .envs(env) - .env("TERM", "dumb") - .current_dir(dir) - .output() - .await - .log_err() - else { - return Err(DirenvError::FailedRun); - }; - - if !direnv_output.status.success() { - log::error!( - "Loading direnv environment failed ({}), stderr: {}", - direnv_output.status, - String::from_utf8_lossy(&direnv_output.stderr) - ); - return Err(DirenvError::NonZeroExit( - direnv_output.status, - direnv_output.stderr, - )); - } - - let output = String::from_utf8_lossy(&direnv_output.stdout); - if output.is_empty() { - // direnv outputs nothing when it has no changes to apply to environment variables - return Ok(HashMap::default()); - } - - match serde_json::from_str(&output) { - Ok(env) => Ok(env), - Err(err) => { - log::error!( - "json parse error {}, while parsing output of `{} {}`:\n{}", - err, - direnv_path.display(), - args.join(" "), - output - ); - Err(DirenvError::InvalidJson) - } - } -} diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index c888cdd11f18a4c118665eac7dbbb6037e70bba4..519000f6a6a489f3f7e9677cd79a60b4112af609 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -1,4 +1,5 @@ -use futures::{FutureExt, future::Shared}; +use anyhow::{Context as _, bail}; +use futures::{FutureExt, StreamExt as _, channel::mpsc, future::Shared}; use language::Buffer; use remote::RemoteClient; use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID}; @@ -20,7 +21,9 @@ pub struct ProjectEnvironment { cli_environment: Option>, local_environments: HashMap<(Shell, Arc), Shared>>>>, remote_environments: HashMap<(Shell, Arc), Shared>>>>, - environment_error_messages: VecDeque, + environment_error_messages: VecDeque, + environment_error_messages_tx: mpsc::UnboundedSender, + _tasks: Vec>, } pub enum ProjectEnvironmentEvent { @@ -30,12 +33,24 @@ pub enum ProjectEnvironmentEvent { impl EventEmitter for ProjectEnvironment {} impl ProjectEnvironment { - pub fn new(cli_environment: Option>) -> Self { + pub fn new(cli_environment: Option>, cx: &mut Context) -> Self { + let (tx, mut rx) = mpsc::unbounded(); + let task = cx.spawn(async move |this, cx| { + while let Some(message) = rx.next().await { + this.update(cx, |this, cx| { + this.environment_error_messages.push_back(message); + cx.emit(ProjectEnvironmentEvent::ErrorsUpdated); + }) + .ok(); + } + }); Self { cli_environment, local_environments: Default::default(), remote_environments: Default::default(), environment_error_messages: Default::default(), + environment_error_messages_tx: tx, + _tasks: vec![task], } } @@ -128,7 +143,37 @@ impl ProjectEnvironment { self.local_environments .entry((shell.clone(), abs_path.clone())) .or_insert_with(|| { - get_local_directory_environment_impl(shell, abs_path.clone(), cx).shared() + let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone(); + let shell = shell.clone(); + let tx = self.environment_error_messages_tx.clone(); + cx.spawn(async move |_, cx| { + let mut shell_env = cx + .background_spawn(load_directory_shell_environment( + shell, + abs_path.clone(), + load_direnv, + tx, + )) + .await + .log_err(); + + if let Some(shell_env) = shell_env.as_mut() { + let path = shell_env + .get("PATH") + .map(|path| path.as_str()) + .unwrap_or_default(); + log::info!( + "using project environment variables shell launched in {:?}. PATH={:?}", + abs_path, + path + ); + + set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell); + } + + shell_env + }) + .shared() }) .clone() } @@ -165,11 +210,11 @@ impl ProjectEnvironment { .clone() } - pub fn peek_environment_error(&self) -> Option<&EnvironmentErrorMessage> { + pub fn peek_environment_error(&self) -> Option<&String> { self.environment_error_messages.front() } - pub fn pop_environment_error(&mut self) -> Option { + pub fn pop_environment_error(&mut self) -> Option { self.environment_error_messages.pop_front() } } @@ -194,125 +239,72 @@ impl From for String { } } -#[derive(Debug)] -pub struct EnvironmentErrorMessage(pub String); - -impl std::fmt::Display for EnvironmentErrorMessage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl EnvironmentErrorMessage { - #[allow(dead_code)] - fn from_str(s: &str) -> Self { - Self(String::from(s)) - } -} - async fn load_directory_shell_environment( - shell: &Shell, - abs_path: &Path, - load_direnv: &DirenvSettings, -) -> ( - Option>, - Option, -) { - match smol::fs::metadata(abs_path).await { - Ok(meta) => { - let dir = if meta.is_dir() { - abs_path - } else if let Some(parent) = abs_path.parent() { - parent - } else { - return ( - None, - Some(EnvironmentErrorMessage(format!( - "Failed to load shell environment in {}: not a directory", - abs_path.display() - ))), - ); - }; - - load_shell_environment(shell, dir, load_direnv).await - } - Err(err) => ( - None, - Some(EnvironmentErrorMessage(format!( - "Failed to load shell environment in {}: {}", - abs_path.display(), - err - ))), - ), - } -} - -async fn load_shell_environment( - shell: &Shell, - dir: &Path, - load_direnv: &DirenvSettings, -) -> ( - Option>, - Option, -) { - use crate::direnv::load_direnv_environment; - use util::shell_env; - - if cfg!(any(test, feature = "test-support")) { - let fake_env = [("ZED_FAKE_TEST_ENV".into(), "true".into())] - .into_iter() - .collect(); - (Some(fake_env), None) - } else if cfg!(target_os = "windows") { + shell: Shell, + abs_path: Arc, + load_direnv: DirenvSettings, + tx: mpsc::UnboundedSender, +) -> anyhow::Result> { + let meta = smol::fs::metadata(&abs_path).await.with_context(|| { + tx.unbounded_send(format!("Failed to open {}", abs_path.display())) + .ok(); + format!("stat {abs_path:?}") + })?; + + let dir = if meta.is_dir() { + abs_path.clone() + } else { + abs_path + .parent() + .with_context(|| { + tx.unbounded_send(format!("Failed to open {}", abs_path.display())) + .ok(); + format!("getting parent of {abs_path:?}") + })? + .into() + }; + + if cfg!(target_os = "windows") { + // Note: direnv is not available on Windows, so we skip direnv processing + // and just return the shell environment let (shell, args) = shell.program_and_args(); - let mut envs = match shell_env::capture(shell, args, dir).await { - Ok(envs) => envs, - Err(err) => { - util::log_err(&err); - return ( - None, - Some(EnvironmentErrorMessage(format!( - "Failed to load environment variables: {}", - err - ))), - ); - } - }; + let mut envs = util::shell_env::capture(shell.clone(), args, abs_path) + .await + .with_context(|| { + tx.unbounded_send("Failed to load environment variables".into()) + .ok(); + format!("capturing shell environment with {shell:?}") + })?; if let Some(path) = envs.remove("Path") { // windows env vars are case-insensitive, so normalize the path var // so we can just assume `PATH` in other places envs.insert("PATH".into(), path); } - - // Note: direnv is not available on Windows, so we skip direnv processing - // and just return the shell environment - (Some(envs), None) + Ok(envs) } else { - let dir_ = dir.to_owned(); let (shell, args) = shell.program_and_args(); - let mut envs = match shell_env::capture(shell, args, &dir_).await { - Ok(envs) => envs, - Err(err) => { - util::log_err(&err); - return ( - None, - Some(EnvironmentErrorMessage::from_str( - "Failed to load environment variables. See log for details", - )), - ); - } - }; + let mut envs = util::shell_env::capture(shell.clone(), args, abs_path) + .await + .with_context(|| { + tx.unbounded_send("Failed to load environment variables".into()) + .ok(); + format!("capturing shell environment with {shell:?}") + })?; // If the user selects `Direct` for direnv, it would set an environment // variable that later uses to know that it should not run the hook. // We would include in `.envs` call so it is okay to run the hook // even if direnv direct mode is enabled. - let (direnv_environment, direnv_error) = match load_direnv { - DirenvSettings::ShellHook => (None, None), - DirenvSettings::Direct => match load_direnv_environment(&envs, dir).await { - Ok(env) => (Some(env), None), - Err(err) => (None, err.into()), - }, + let direnv_environment = match load_direnv { + DirenvSettings::ShellHook => None, + DirenvSettings::Direct => load_direnv_environment(&envs, &dir) + .await + .with_context(|| { + tx.unbounded_send("Failed to load direnv environment".into()) + .ok(); + "load direnv environment" + }) + .log_err(), }; if let Some(direnv_environment) = direnv_environment { for (key, value) in direnv_environment { @@ -324,51 +316,39 @@ async fn load_shell_environment( } } - (Some(envs), direnv_error) + Ok(envs) } } -fn get_local_directory_environment_impl( - shell: &Shell, - abs_path: Arc, - cx: &Context, -) -> Task>> { - let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone(); - - let shell = shell.clone(); - cx.spawn(async move |this, cx| { - let (mut shell_env, error_message) = cx - .background_spawn({ - let abs_path = abs_path.clone(); - async move { - load_directory_shell_environment(&shell, &abs_path, &load_direnv).await - } - }) - .await; - - if let Some(shell_env) = shell_env.as_mut() { - let path = shell_env - .get("PATH") - .map(|path| path.as_str()) - .unwrap_or_default(); - log::info!( - "using project environment variables shell launched in {:?}. PATH={:?}", - abs_path, - path - ); - - set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell); - } +async fn load_direnv_environment( + env: &HashMap, + dir: &Path, +) -> anyhow::Result>> { + let direnv_path = which::which("direnv").context("finding direnv binary")?; + + let args = &["export", "json"]; + let direnv_output = smol::process::Command::new(&direnv_path) + .args(args) + .envs(env) + .env("TERM", "dumb") + .current_dir(dir) + .output() + .await + .context("running direnv")?; + + if !direnv_output.status.success() { + bail!( + "Loading direnv environment failed ({}), stderr: {}", + direnv_output.status, + String::from_utf8_lossy(&direnv_output.stderr) + ); + } - if let Some(error) = error_message { - this.update(cx, |this, cx| { - log::error!("{error}"); - this.environment_error_messages.push_back(error); - cx.emit(ProjectEnvironmentEvent::ErrorsUpdated) - }) - .log_err(); - } + let output = String::from_utf8_lossy(&direnv_output.stdout); + if output.is_empty() { + // direnv outputs nothing when it has no changes to apply to environment variables + return Ok(HashMap::default()); + } - shell_env - }) + serde_json::from_str(&output).context("parsing direnv json") } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f9a3f20fa77c41d1ec6405ea0ee7b245fe4e0845..3a141da70aeaab466dc580ec69bae16fc48de0ed 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -23,11 +23,10 @@ pub mod worktree_store; #[cfg(test)] mod project_tests; -mod direnv; mod environment; use buffer_diff::BufferDiff; use context_server_store::ContextServerStore; -pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent}; +pub use environment::ProjectEnvironmentEvent; use git::repository::get_git_committer; use git_store::{Repository, RepositoryId}; pub mod search_history; @@ -1071,7 +1070,7 @@ impl Project { let context_server_store = cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx)); - let environment = cx.new(|_| ProjectEnvironment::new(env)); + let environment = cx.new(|cx| ProjectEnvironment::new(env, cx)); let manifest_tree = ManifestTree::new(worktree_store.clone(), cx); let toolchain_store = cx.new(|cx| { ToolchainStore::local( @@ -1306,7 +1305,7 @@ impl Project { cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); - let environment = cx.new(|_| ProjectEnvironment::new(None)); + let environment = cx.new(|cx| ProjectEnvironment::new(None, cx)); let lsp_store = cx.new(|cx| { LspStore::new_remote( @@ -1519,7 +1518,7 @@ impl Project { ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) })?; - let environment = cx.new(|_| ProjectEnvironment::new(None))?; + let environment = cx.new(|cx| ProjectEnvironment::new(None, cx))?; let breakpoint_store = cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?; @@ -1951,10 +1950,7 @@ impl Project { } #[inline] - pub fn peek_environment_error<'a>( - &'a self, - cx: &'a App, - ) -> Option<&'a EnvironmentErrorMessage> { + pub fn peek_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> { self.environment.read(cx).peek_environment_error() } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 534eae6f44986afa42b6d202e4f34691935b3b33..83000c8bac3b409a0dad07490a5f028e482f0662 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -94,7 +94,7 @@ impl HeadlessProject { store }); - let environment = cx.new(|_| ProjectEnvironment::new(None)); + let environment = cx.new(|cx| ProjectEnvironment::new(None, cx)); let manifest_tree = ManifestTree::new(worktree_store.clone(), cx); let toolchain_store = cx.new(|cx| { ToolchainStore::local( diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 3f1205f96531a897ac67492c84bc2f7e949ab02a..f725167724f82b8c4479eca53a5e8f48927b4f8b 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -353,7 +353,10 @@ pub async fn load_login_shell_environment() -> Result<()> { // into shell's `cd` command (and hooks) to manipulate env. // We do this so that we get the env a user would have when spawning a shell // in home directory. - for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()).await? { + for (name, value) in shell_env::capture(get_system_shell(), &[], paths::home_dir()) + .await + .with_context(|| format!("capturing environment with {:?}", get_system_shell()))? + { unsafe { env::set_var(&name, &value) }; } @@ -627,7 +630,7 @@ where } pub fn log_err(error: &E) { - log_error_with_caller(*Location::caller(), error, log::Level::Warn); + log_error_with_caller(*Location::caller(), error, log::Level::Error); } pub trait TryFutureExt { From 8f4646d6c357409cfc286104088b4d21f2bea851 Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 23 Oct 2025 06:44:42 +0200 Subject: [PATCH 181/202] Use ShellKind::try_quote whenever we need to quote shell args (#40912) Using `shlex` unconditionally is dangerous as it assumes the underlying shell is POSIX which is not the case for PowerShell, CMD, or Nushell. Therefore, whenever we want to quote the args we should utilise our helper `util::shell::ShellKind::try_quote` which takes into account which shell is being used to actually exec/spawn the invocation. Release Notes: - N/A --------- Co-authored-by: Lukas Wirth --- Cargo.lock | 4 - crates/askpass/src/askpass.rs | 10 +- crates/dap_adapters/Cargo.toml | 1 - crates/dap_adapters/src/javascript.rs | 4 +- crates/debugger_ui/Cargo.toml | 1 - crates/debugger_ui/src/new_process_modal.rs | 14 +- crates/project/Cargo.toml | 1 - crates/project/src/environment.rs | 4 +- .../project/src/lsp_store/lsp_ext_command.rs | 3 +- crates/project/src/terminals.rs | 25 ++- crates/remote/Cargo.toml | 1 - crates/remote/src/transport/ssh.rs | 87 ++++---- crates/remote/src/transport/wsl.rs | 24 ++- crates/remote_server/src/headless_project.rs | 2 +- crates/task/src/task.rs | 103 +++------ crates/terminal/src/terminal.rs | 14 +- crates/terminal/src/terminal_settings.rs | 2 +- crates/util/src/paths.rs | 34 ++- crates/util/src/shell.rs | 201 ++++++++++++++---- crates/{task => util}/src/shell_builder.rs | 7 +- crates/util/src/shell_env.rs | 2 +- crates/util/src/util.rs | 5 +- 22 files changed, 317 insertions(+), 232 deletions(-) rename crates/{task => util}/src/shell_builder.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 0cc10bde430f4e527053e21d69c89c66f4d25241..48db1977efa9772c1d253e9382ef788664056b7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4531,7 +4531,6 @@ dependencies = [ "paths", "serde", "serde_json", - "shlex", "smol", "task", "util", @@ -4757,7 +4756,6 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", - "shlex", "sysinfo 0.37.2", "task", "tasks_ui", @@ -12925,7 +12923,6 @@ dependencies = [ "settings", "sha2", "shellexpand 2.1.2", - "shlex", "smallvec", "smol", "snippet", @@ -13838,7 +13835,6 @@ dependencies = [ "serde", "serde_json", "settings", - "shlex", "smol", "tempfile", "thiserror 2.0.17", diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index dfe8a96ee6f19510df06948f94af48d621515747..9b5d5848270b13c29e84f5601a60511f8956988c 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -20,7 +20,7 @@ use futures::{ }; use gpui::{AsyncApp, BackgroundExecutor, Task}; use smol::fs; -use util::{ResultExt as _, debug_panic, maybe, paths::PathExt}; +use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind}; /// Path to the program used for askpass /// @@ -199,9 +199,15 @@ impl PasswordProxy { let current_exec = std::env::current_exe().context("Failed to determine current zed executable path.")?; + // TODO: Inferred from the use of powershell.exe in askpass_helper_script + let shell_kind = if cfg!(windows) { + ShellKind::PowerShell + } else { + ShellKind::Posix + }; let askpass_program = ASKPASS_PROGRAM .get_or_init(|| current_exec) - .try_shell_safe() + .try_shell_safe(shell_kind) .context("Failed to shell-escape Askpass program path.")? .to_string(); // Create an askpass script that communicates back to this process. diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 1593f51cf326b06f6c865d8bca8a8b4712511ff1..253674c0f3da16574b4303faf679abeb310756d8 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -35,7 +35,6 @@ log.workspace = true paths.workspace = true serde.workspace = true serde_json.workspace = true -shlex.workspace = true smol.workspace = true task.workspace = true util.workspace = true diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 8c90bfc7c054f147336f9c6330d5f1d4a847d588..68f5ca7e7976640c5b3e44ec5e2e2b880a6c2407 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -6,7 +6,7 @@ use gpui::AsyncApp; use serde_json::Value; use std::{path::PathBuf, sync::OnceLock}; use task::DebugRequest; -use util::{ResultExt, maybe}; +use util::{ResultExt, maybe, shell::ShellKind}; use crate::*; @@ -67,7 +67,7 @@ impl JsDebugAdapter { .get("type") .filter(|value| value == &"node-terminal")?; let command = configuration.get("command")?.as_str()?.to_owned(); - let mut args = shlex::split(&command)?.into_iter(); + let mut args = ShellKind::Posix.split(&command)?.into_iter(); let program = args.next()?; configuration.insert("runtimeExecutable".to_owned(), program.into()); configuration.insert( diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 28866f0d273ce990b51615157412c9b120220d7b..c1a0657c0ed93508acb330a98dc6d1c1ee91c570 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -60,7 +60,6 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true -shlex.workspace = true sysinfo.workspace = true task.workspace = true tasks_ui.workspace = true diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index b56c0a5d3b46c4a6b6b43bbf843178c85f5c8d9f..cf3c779abad2073892a45acb4616298ef85af043 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -32,7 +32,7 @@ use ui::{ SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, relative, rems, v_flex, }; -use util::{ResultExt, rel_path::RelPath}; +use util::{ResultExt, rel_path::RelPath, shell::ShellKind}; use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; @@ -837,7 +837,11 @@ impl ConfigureMode { }; } let command = self.program.read(cx).text(cx); - let mut args = shlex::split(&command).into_iter().flatten().peekable(); + let mut args = ShellKind::Posix + .split(&command) + .into_iter() + .flatten() + .peekable(); let mut env = FxHashMap::default(); while args.peek().is_some_and(|arg| arg.contains('=')) { let arg = args.next().unwrap(); @@ -1263,7 +1267,11 @@ impl PickerDelegate for DebugDelegate { }) .unwrap_or_default(); - let mut args = shlex::split(&text).into_iter().flatten().peekable(); + let mut args = ShellKind::Posix + .split(&text) + .into_iter() + .flatten() + .peekable(); let mut env = HashMap::default(); while args.peek().is_some_and(|arg| arg.contains('=')) { let arg = args.next().unwrap(); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 0297611d101ad883c3d68d7dff9e0a92f00c5b71..d9285a8c24ec5130dd8ce8abf5bbd77c830e0f3f 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -72,7 +72,6 @@ serde_json.workspace = true settings.workspace = true sha2.workspace = true shellexpand.workspace = true -shlex.workspace = true smallvec.workspace = true smol.workspace = true snippet.workspace = true diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index 519000f6a6a489f3f7e9677cd79a60b4112af609..da2933d317ecaa17f5d4cb199f132712f5f28ac3 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -4,7 +4,7 @@ use language::Buffer; use remote::RemoteClient; use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID}; use std::{collections::VecDeque, path::Path, sync::Arc}; -use task::Shell; +use task::{Shell, shell_to_proto}; use util::ResultExt; use worktree::Worktree; @@ -198,7 +198,7 @@ impl ProjectEnvironment { .proto_client() .request(proto::GetDirectoryEnvironment { project_id: REMOTE_SERVER_PROJECT_ID, - shell: Some(shell.clone().to_proto()), + shell: Some(shell_to_proto(shell.clone())), directory: abs_path.to_string_lossy().to_string(), }); cx.spawn(async move |_, _| { diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index c79e3df178290fa614e08a8abd85a527a696b003..5066143244da890a63ead6650cb61fdb71d3964a 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -657,6 +657,7 @@ impl LspCommand for GetLspRunnables { ); task_template.args.extend(cargo.cargo_args); if !cargo.executable_args.is_empty() { + let shell_kind = task_template.shell.shell_kind(cfg!(windows)); task_template.args.push("--".to_string()); task_template.args.extend( cargo @@ -682,7 +683,7 @@ impl LspCommand for GetLspRunnables { // That bit is not auto-expanded when using single quotes. // Escape extra cargo args unconditionally as those are unlikely to contain `~`. .flat_map(|extra_arg| { - shlex::try_quote(&extra_arg).ok().map(|s| s.to_string()) + shell_kind.try_quote(&extra_arg).map(|s| s.to_string()) }), ); } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 4a0a1790b49449fd82b8aeff58f6c11c8e63261b..cf25735e4bf490847e20c92b19e791f8bef56b9b 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -168,20 +168,19 @@ impl Project { match remote_client { Some(remote_client) => match activation_script.clone() { activation_script if !activation_script.is_empty() => { - let activation_script = activation_script.join("; "); + let separator = shell_kind.sequential_commands_separator(); + let activation_script = + activation_script.join(&format!("{separator} ")); let to_run = format_to_run(); - let args = vec![ - "-c".to_owned(), - format!("{activation_script}; {to_run}"), - ]; + let shell = remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell); + let arg = format!("{activation_script}{separator} {to_run}"); + let args = shell_kind.args_for_shell(false, arg); + create_remote_shell( - Some(( - &remote_client - .read(cx) - .shell() - .unwrap_or_else(get_default_system_shell), - &args, - )), + Some((&shell, &args)), env, path, remote_client, @@ -562,7 +561,7 @@ fn create_remote_shell( Shell::WithArguments { program: command.program, args: command.args, - title_override: Some(format!("{} — Terminal", host).into()), + title_override: Some(format!("{} — Terminal", host)), }, command.env, )) diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index 02560484922fd5b02b348a74493c0af5ca4f78d1..d1a91af9a5decc88b4c70c69001ba6dad18e4b8b 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -34,7 +34,6 @@ rpc = { workspace = true, features = ["gpui"] } serde.workspace = true serde_json.workspace = true settings.workspace = true -shlex.workspace = true smol.workspace = true tempfile.workspace = true thiserror.workspace = true diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index a1337c2d65c74b882e19dd832359e297a13b9236..745391e17ee90b183e517a8ce4c4ab7006493758 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -203,17 +203,6 @@ impl AsMut for MasterProcess { } } -macro_rules! shell_script { - ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ - format!( - $fmt, - $( - $name = shlex::try_quote($arg).unwrap() - ),+ - ) - }}; -} - #[async_trait(?Send)] impl RemoteConnection for SshRemoteConnection { async fn kill(&self) -> Result<()> { @@ -738,21 +727,24 @@ impl SshRemoteConnection { delegate.set_status(Some("Extracting remote development server"), cx); let server_mode = 0o755; + let shell_kind = ShellKind::Posix; let orig_tmp_path = tmp_path.display(self.path_style()); + let server_mode = format!("{:o}", server_mode); + let server_mode = shell_kind + .try_quote(&server_mode) + .context("shell quoting")?; + let dst_path = dst_path.display(self.path_style()); + let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?; let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - shell_script!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.display(self.path_style()), + format!( + "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}" ) } else { - shell_script!( - "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", - server_mode = &format!("{:o}", server_mode), - dst_path = &dst_path.display(self.path_style()) - ) + format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}") }; - self.socket.run_command("sh", &["-c", &script]).await?; + let script = shell_kind.try_quote(&script).context("shell quoting")?; + let args = shell_kind.args_for_shell(false, script.to_string()); + self.socket.run_command("sh", &args).await?; Ok(()) } @@ -886,8 +878,12 @@ impl SshSocket { // into a machine. You must use `cd` to get back to $HOME. // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" fn ssh_command(&self, program: &str, args: &[impl AsRef]) -> process::Command { + let shell_kind = ShellKind::Posix; let mut command = util::command::new_smol_command("ssh"); - let mut to_run = shlex::try_quote(program).unwrap().into_owned(); + let mut to_run = shell_kind + .try_quote(program) + .expect("shell quoting") + .into_owned(); for arg in args { // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? debug_assert!( @@ -895,9 +891,10 @@ impl SshSocket { "multiline arguments do not work in all shells" ); to_run.push(' '); - to_run.push_str(&shlex::try_quote(arg.as_ref()).unwrap()); + to_run.push_str(&shell_kind.try_quote(arg.as_ref()).expect("shell quoting")); } - let to_run = format!("cd; {to_run}"); + let separator = shell_kind.sequential_commands_separator(); + let to_run = format!("cd{separator} {to_run}"); self.ssh_options(&mut command, true) .arg(self.connection_options.ssh_url()) .arg("-T") @@ -906,7 +903,7 @@ impl SshSocket { command } - async fn run_command(&self, program: &str, args: &[&str]) -> Result { + async fn run_command(&self, program: &str, args: &[impl AsRef]) -> Result { let output = self.ssh_command(program, args).output().await?; anyhow::ensure!( output.status.success(), @@ -1080,7 +1077,10 @@ impl SshConnectionOptions { "-w", ]; - let mut tokens = shlex::split(input).context("invalid input")?.into_iter(); + let mut tokens = ShellKind::Posix + .split(input) + .context("invalid input")? + .into_iter(); 'outer: while let Some(arg) = tokens.next() { if ALLOWED_OPTS.contains(&(&arg as &str)) { @@ -1243,6 +1243,7 @@ fn build_command( ) -> Result { use std::fmt::Write as _; + let shell_kind = ShellKind::new(ssh_shell, false); let mut exec = String::new(); if let Some(working_dir) = working_dir { let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string(); @@ -1252,29 +1253,41 @@ fn build_command( const TILDE_PREFIX: &'static str = "~/"; if working_dir.starts_with(TILDE_PREFIX) { let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - write!(exec, "cd \"$HOME/{working_dir}\" && ",).unwrap(); + write!(exec, "cd \"$HOME/{working_dir}\" && ",)?; } else { - write!(exec, "cd \"{working_dir}\" && ",).unwrap(); + write!(exec, "cd \"{working_dir}\" && ",)?; } } else { - write!(exec, "cd && ").unwrap(); + write!(exec, "cd && ")?; }; - write!(exec, "exec env ").unwrap(); + write!(exec, "exec env ")?; for (k, v) in input_env.iter() { - if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { - write!(exec, "{}={} ", k, v).unwrap(); - } + write!( + exec, + "{}={} ", + k, + shell_kind.try_quote(v).context("shell quoting")? + )?; } if let Some(input_program) = input_program { - write!(exec, "{}", shlex::try_quote(&input_program).unwrap()).unwrap(); + write!( + exec, + "{}", + shell_kind + .try_quote(&input_program) + .context("shell quoting")? + )?; for arg in input_args { - let arg = shlex::try_quote(&arg)?; - write!(exec, " {}", &arg).unwrap(); + write!( + exec, + " {}", + shell_kind.try_quote(&arg).context("shell quoting")? + )?; } } else { - write!(exec, "{ssh_shell} -l").unwrap(); + write!(exec, "{ssh_shell} -l")?; }; let mut args = Vec::new(); diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index 4eca7c4d5295e4baf8b2812763a02c32701959f7..4ccadee73f99804675c51c9eb6007419b2cacfa7 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -2,7 +2,7 @@ use crate::{ RemoteClientDelegate, RemotePlatform, remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, }; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; use async_trait::async_trait; use collections::HashMap; use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; @@ -441,6 +441,7 @@ impl RemoteConnection for WslRemoteConnection { bail!("WSL shares the network interface with the host system"); } + let shell = ShellKind::new(&self.shell, false); let working_dir = working_dir .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string()) .unwrap_or("~".to_string()); @@ -448,19 +449,26 @@ impl RemoteConnection for WslRemoteConnection { let mut exec = String::from("exec env "); for (k, v) in env.iter() { - if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { - write!(exec, "{}={} ", k, v).unwrap(); - } + write!( + exec, + "{}={} ", + k, + shell.try_quote(&v).context("shell quoting")? + )?; } if let Some(program) = program { - write!(exec, "{}", shlex::try_quote(&program)?).unwrap(); + write!( + exec, + "{}", + shell.try_quote(&program).context("shell quoting")? + )?; for arg in args { - let arg = shlex::try_quote(&arg)?; - write!(exec, " {}", &arg).unwrap(); + let arg = shell.try_quote(&arg).context("shell quoting")?; + write!(exec, " {}", &arg)?; } } else { - write!(&mut exec, "{} -l", self.shell).unwrap(); + write!(&mut exec, "{} -l", self.shell)?; } let wsl_args = if let Some(user) = &self.connection_options.user { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 83000c8bac3b409a0dad07490a5f028e482f0662..588ee836084e350010cfe552d3f294d5f3ae0bcf 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -774,7 +774,7 @@ impl HeadlessProject { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let shell = task::Shell::from_proto(envelope.payload.shell.context("missing shell")?)?; + let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?; let directory = PathBuf::from(envelope.payload.directory); let environment = this .update(&mut cx, |this, cx| { diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index bfb84ced944cda758c7c453f561ca4ec13220c07..280bf5ccdb91271d7ff738654d507573c9d667d4 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -3,7 +3,6 @@ mod adapter_schema; mod debug_format; mod serde_helpers; -mod shell_builder; pub mod static_source; mod task_template; mod vscode_debug_format; @@ -12,23 +11,22 @@ mod vscode_format; use anyhow::Context as _; use collections::{HashMap, HashSet, hash_map}; use gpui::SharedString; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::path::PathBuf; use std::str::FromStr; -use util::get_system_shell; pub use adapter_schema::{AdapterSchema, AdapterSchemas}; pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; -pub use shell_builder::{ShellBuilder, ShellKind}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, }; +pub use util::shell::{Shell, ShellKind}; +pub use util::shell_builder::ShellBuilder; pub use vscode_debug_format::VsCodeDebugTaskFile; pub use vscode_format::VsCodeTaskFile; pub use zed_actions::RevealTarget; @@ -318,81 +316,32 @@ pub struct TaskContext { #[derive(Clone, Debug)] pub struct RunnableTag(pub SharedString); -/// Shell configuration to open the terminal with. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - /// Use the system's default terminal configuration in /etc/passwd - #[default] - System, - /// Use a specific program with no arguments. - Program(String), - /// Use a specific program with arguments. - WithArguments { - /// The program to run. - program: String, - /// The arguments to pass to the program. - args: Vec, - /// An optional string to override the title of the terminal tab - title_override: Option, - }, +pub fn shell_from_proto(proto: proto::Shell) -> anyhow::Result { + let shell_type = proto.shell_type.context("invalid shell type")?; + let shell = match shell_type { + proto::shell::ShellType::System(_) => Shell::System, + proto::shell::ShellType::Program(program) => Shell::Program(program), + proto::shell::ShellType::WithArguments(program) => Shell::WithArguments { + program: program.program, + args: program.args, + title_override: None, + }, + }; + Ok(shell) } -impl Shell { - pub fn program(&self) -> String { - match self { - Shell::Program(program) => program.clone(), - Shell::WithArguments { program, .. } => program.clone(), - Shell::System => get_system_shell(), - } - } - - pub fn program_and_args(&self) -> (String, &[String]) { - match self { - Shell::Program(program) => (program.clone(), &[]), - Shell::WithArguments { program, args, .. } => (program.clone(), args), - Shell::System => (get_system_shell(), &[]), - } - } - - pub fn shell_kind(&self, is_windows: bool) -> ShellKind { - match self { - Shell::Program(program) => ShellKind::new(program, is_windows), - Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), - Shell::System => ShellKind::system(), - } - } - - pub fn from_proto(proto: proto::Shell) -> anyhow::Result { - let shell_type = proto.shell_type.context("invalid shell type")?; - let shell = match shell_type { - proto::shell::ShellType::System(_) => Self::System, - proto::shell::ShellType::Program(program) => Self::Program(program), - proto::shell::ShellType::WithArguments(program) => Self::WithArguments { - program: program.program, - args: program.args, - title_override: None, - }, - }; - Ok(shell) - } - - pub fn to_proto(self) -> proto::Shell { - let shell_type = match self { - Shell::System => proto::shell::ShellType::System(proto::System {}), - Shell::Program(program) => proto::shell::ShellType::Program(program), - Shell::WithArguments { - program, - args, - title_override: _, - } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { - program, - args, - }), - }; - proto::Shell { - shell_type: Some(shell_type), - } +pub fn shell_to_proto(shell: Shell) -> proto::Shell { + let shell_type = match shell { + Shell::System => proto::shell::ShellType::System(proto::System {}), + Shell::Program(program) => proto::shell::ShellType::Program(program), + Shell::WithArguments { + program, + args, + title_override: _, + } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { program, args }), + }; + proto::Shell { + shell_type: Some(shell_type), } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index fa42a94e932a81d171ffc871393a30abf965678f..68ac3d3e290953363a3246c41652149e1ed5da1f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -67,7 +67,7 @@ use thiserror::Error; use gpui::{ App, AppContext as _, Bounds, ClipboardItem, Context, EventEmitter, Hsla, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Rgba, - ScrollWheelEvent, SharedString, Size, Task, TouchPhase, Window, actions, black, px, + ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px, }; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; @@ -277,7 +277,7 @@ pub struct TerminalError { pub directory: Option, pub program: Option, pub args: Option>, - pub title_override: Option, + pub title_override: Option, pub source: std::io::Error, } @@ -445,14 +445,14 @@ impl TerminalBuilder { struct ShellParams { program: String, args: Option>, - title_override: Option, + title_override: Option, } impl ShellParams { fn new( program: String, args: Option>, - title_override: Option, + title_override: Option, ) -> Self { log::debug!("Using {program} as shell"); Self { @@ -514,10 +514,8 @@ impl TerminalBuilder { working_directory: working_directory.clone(), drain_on_exit: true, env: env.clone().into_iter().collect(), - // We do not want to escape arguments if we are using CMD as our shell. - // If we do we end up with too many quotes/escaped quotes for CMD to handle. #[cfg(windows)] - escape_args: shell_kind != util::shell::ShellKind::Cmd, + escape_args: shell_kind.tty_escape_args(), } }; @@ -824,7 +822,7 @@ pub struct Terminal { pub last_content: TerminalContent, pub selection_head: Option, pub breadcrumb_text: String, - title_override: Option, + title_override: Option, scroll_px: Pixels, next_link_id: usize, selection_phase: SelectionPhase, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 9bb5ffb517b15225eed711a6d4e31e2977626d0a..b8576a1de308d8bf3bd098907018b94cb73eefa0 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -66,7 +66,7 @@ fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell { } => Shell::WithArguments { program, args, - title_override, + title_override: title_override.map(Into::into), }, } } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 0743601839cc31e0e3a4c9d6c936aab7edce5837..20187bf7376861ebd03e02f7fb006428c1c51ec4 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -15,7 +15,7 @@ use std::{ sync::LazyLock, }; -use crate::rel_path::RelPath; +use crate::{rel_path::RelPath, shell::ShellKind}; static HOME_DIR: OnceLock = OnceLock::new(); @@ -84,9 +84,7 @@ pub trait PathExt { fn multiple_extensions(&self) -> Option; /// Try to make a shell-safe representation of the path. - /// - /// For Unix, the path is escaped to be safe for POSIX shells - fn try_shell_safe(&self) -> anyhow::Result; + fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result; } impl> PathExt for T { @@ -164,24 +162,16 @@ impl> PathExt for T { Some(parts.into_iter().join(".")) } - fn try_shell_safe(&self) -> anyhow::Result { - #[cfg(target_os = "windows")] - { - Ok(self.as_ref().to_string_lossy().to_string()) - } - - #[cfg(not(target_os = "windows"))] - { - let path_str = self - .as_ref() - .to_str() - .with_context(|| "Path contains invalid UTF-8")?; - - // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible - // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other - // errors are introduced in the future :( - Ok(shlex::try_quote(path_str)?.into_owned()) - } + fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result { + let path_str = self + .as_ref() + .to_str() + .with_context(|| "Path contains invalid UTF-8")?; + shell_kind + .try_quote(path_str) + .as_deref() + .map(ToOwned::to_owned) + .context("Failed to quote path") } } diff --git a/crates/util/src/shell.rs b/crates/util/src/shell.rs index 22e07acf25b46161138a297e6de701f74b483861..d81946b8ad207596cfdaf1ec714de94a9b3f71d6 100644 --- a/crates/util/src/shell.rs +++ b/crates/util/src/shell.rs @@ -1,6 +1,53 @@ +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, fmt, path::Path, sync::LazyLock}; +/// Shell configuration to open the terminal with. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + /// Use the system's default terminal configuration in /etc/passwd + #[default] + System, + /// Use a specific program with no arguments. + Program(String), + /// Use a specific program with arguments. + WithArguments { + /// The program to run. + program: String, + /// The arguments to pass to the program. + args: Vec, + /// An optional string to override the title of the terminal tab + title_override: Option, + }, +} + +impl Shell { + pub fn program(&self) -> String { + match self { + Shell::Program(program) => program.clone(), + Shell::WithArguments { program, .. } => program.clone(), + Shell::System => get_system_shell(), + } + } + + pub fn program_and_args(&self) -> (String, &[String]) { + match self { + Shell::Program(program) => (program.clone(), &[]), + Shell::WithArguments { program, args, .. } => (program.clone(), args), + Shell::System => (get_system_shell(), &[]), + } + } + + pub fn shell_kind(&self, is_windows: bool) -> ShellKind { + match self { + Shell::Program(program) => ShellKind::new(program, is_windows), + Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), + Shell::System => ShellKind::system(), + } + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ShellKind { #[default] @@ -185,32 +232,20 @@ impl ShellKind { .unwrap_or_else(|| program.as_os_str()) .to_string_lossy(); - if program == "powershell" || program == "pwsh" { - ShellKind::PowerShell - } else if program == "cmd" { - ShellKind::Cmd - } else if program == "nu" { - ShellKind::Nushell - } else if program == "fish" { - ShellKind::Fish - } else if program == "csh" { - ShellKind::Csh - } else if program == "tcsh" { - ShellKind::Tcsh - } else if program == "rc" { - ShellKind::Rc - } else if program == "xonsh" { - ShellKind::Xonsh - } else if program == "sh" || program == "bash" { - ShellKind::Posix - } else { - if is_windows { - ShellKind::PowerShell - } else { - // Some other shell detected, the user might install and use a - // unix-like shell. - ShellKind::Posix - } + match &*program { + "powershell" | "pwsh" => ShellKind::PowerShell, + "cmd" => ShellKind::Cmd, + "nu" => ShellKind::Nushell, + "fish" => ShellKind::Fish, + "csh" => ShellKind::Csh, + "tcsh" => ShellKind::Tcsh, + "rc" => ShellKind::Rc, + "xonsh" => ShellKind::Xonsh, + "sh" | "bash" => ShellKind::Posix, + _ if is_windows => ShellKind::PowerShell, + // Some other shell detected, the user might install and use a + // unix-like shell. + _ => ShellKind::Posix, } } @@ -363,44 +398,132 @@ impl ShellKind { match self { ShellKind::PowerShell => Some('&'), ShellKind::Nushell => Some('^'), - _ => None, + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::Cmd + | ShellKind::Xonsh => None, } } pub const fn sequential_commands_separator(&self) -> char { match self { ShellKind::Cmd => '&', - _ => ';', + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Nushell + | ShellKind::Xonsh => ';', } } pub fn try_quote<'a>(&self, arg: &'a str) -> Option> { + // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible + // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other + // errors are introduced in the future :( shlex::try_quote(arg).ok().map(|arg| match self { - // If we are running in PowerShell, we want to take extra care when escaping strings. - // In particular, we want to escape strings with a backtick (`) rather than a backslash (\). - // TODO double escaping backslashes is not necessary in PowerShell and probably CMD - ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"")), - _ => arg, + ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"").replace("\\\\", "\\")), + ShellKind::Cmd => Cow::Owned(arg.replace("\\\\", "\\")), + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::Nushell + | ShellKind::Xonsh => arg, }) } + pub fn split(&self, input: &str) -> Option> { + shlex::split(input) + } + pub const fn activate_keyword(&self) -> &'static str { match self { ShellKind::Cmd => "", ShellKind::Nushell => "overlay use", ShellKind::PowerShell => ".", - ShellKind::Fish => "source", - ShellKind::Csh => "source", - ShellKind::Tcsh => "source", - ShellKind::Posix | ShellKind::Rc => "source", - ShellKind::Xonsh => "source", + ShellKind::Fish + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Posix + | ShellKind::Rc + | ShellKind::Xonsh => "source", } } pub const fn clear_screen_command(&self) -> &'static str { match self { ShellKind::Cmd => "cls", - _ => "clear", + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Nushell + | ShellKind::Xonsh => "clear", + } + } + + #[cfg(windows)] + /// We do not want to escape arguments if we are using CMD as our shell. + /// If we do we end up with too many quotes/escaped quotes for CMD to handle. + pub const fn tty_escape_args(&self) -> bool { + match self { + ShellKind::Cmd => false, + ShellKind::Posix + | ShellKind::Csh + | ShellKind::Tcsh + | ShellKind::Rc + | ShellKind::Fish + | ShellKind::PowerShell + | ShellKind::Nushell + | ShellKind::Xonsh => true, } } } + +#[cfg(test)] +mod tests { + use super::*; + + // Examples + // WSL + // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello" + // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello" + // wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53 + // PowerShell from Nushell + // nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\"" + // PowerShell from CMD + // cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\" + + #[test] + fn test_try_quote_powershell() { + let shell_kind = ShellKind::PowerShell; + assert_eq!( + shell_kind + .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") + .unwrap() + .into_owned(), + "\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest `\"test_foo.py::test_foo`\"\"".to_string() + ); + } + + #[test] + fn test_try_quote_cmd() { + let shell_kind = ShellKind::Cmd; + assert_eq!( + shell_kind + .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") + .unwrap() + .into_owned(), + "\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"".to_string() + ); + } +} diff --git a/crates/task/src/shell_builder.rs b/crates/util/src/shell_builder.rs similarity index 98% rename from crates/task/src/shell_builder.rs rename to crates/util/src/shell_builder.rs index a6504f4eb765a8a144343691b82cdca9a6802cbd..7e52b67b35f6f3d21ea5e3ad5a0632cd46344125 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/util/src/shell_builder.rs @@ -1,8 +1,5 @@ -use util::shell::get_system_shell; - -use crate::Shell; - -pub use util::shell::ShellKind; +use crate::shell::get_system_shell; +use crate::shell::{Shell, ShellKind}; /// ShellBuilder is used to turn a user-requested task into a /// program that can be executed by the shell. diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs index a82bea154ec5cb16153b70499eaf7e34c0464995..b3c9e3bef390b945314ba79fcc34ff2669a349a6 100644 --- a/crates/util/src/shell_env.rs +++ b/crates/util/src/shell_env.rs @@ -35,8 +35,8 @@ async fn capture_unix( use std::os::unix::process::CommandExt; use std::process::Stdio; - let zed_path = super::get_shell_safe_zed_path()?; let shell_kind = ShellKind::new(shell_path, false); + let zed_path = super::get_shell_safe_zed_path(shell_kind)?; let mut command_string = String::new(); let mut command = std::process::Command::new(shell_path); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index f725167724f82b8c4479eca53a5e8f48927b4f8b..3a78ef3d41e557d33d5af77021464ee1dcadf5e4 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -9,6 +9,7 @@ pub mod rel_path; pub mod schemars; pub mod serde; pub mod shell; +pub mod shell_builder; pub mod shell_env; pub mod size; #[cfg(any(test, feature = "test-support"))] @@ -295,12 +296,12 @@ fn load_shell_from_passwd() -> Result<()> { } /// Returns a shell escaped path for the current zed executable -pub fn get_shell_safe_zed_path() -> anyhow::Result { +pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result { let zed_path = std::env::current_exe().context("Failed to determine current zed executable path.")?; zed_path - .try_shell_safe() + .try_shell_safe(shell_kind) .context("Failed to shell-escape Zed executable path.") } From 1edb1b389613abbd2b62b07aff0be7350e078829 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:49:36 -0400 Subject: [PATCH 182/202] settings ui: Update file headers when adding or removing projects (#40968) This PR gets the `SettingsWindow` struct to subscribe to all `Entity` events and any future project entities that are created. When a project emits an event that signals a worktree has been added or removed, the settings window refetches all settings files it can find. This fixes a bug where the settings ui would notice some project settings that were created or opened after the `SettingsWindow` has been initialized. I also renamed `LOCAL` file mask to `PROJECT` to be inline with the `SettingsFile` naming convention. Release Notes: - settings ui: Fix bug where project setting files wouldn't be detected if they were created or opened after while an active settings window is open --- crates/settings_ui/src/page_data.rs | 146 ++++++++++---------- crates/settings_ui/src/settings_ui.rs | 187 +++++++++++++++----------- crates/workspace/src/workspace.rs | 2 +- 3 files changed, 186 insertions(+), 149 deletions(-) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 5997d14de33bdec4b4739112a02b22ac0f12e2e7..5c75a78b9a6dae79abbc7d96089512d1a5063949 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -5,7 +5,7 @@ use strum::IntoDiscriminant as _; use ui::{IntoElement, SharedString}; use crate::{ - DynamicItem, LOCAL, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage, + DynamicItem, PROJECT, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage, SettingsPageItem, SubPageLink, USER, all_language_names, sub_page_stack, }; @@ -26,7 +26,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { items: vec![ SettingsPageItem::SectionHeader("General Settings"), SettingsPageItem::SettingItem(SettingItem { - files: LOCAL, + files: PROJECT, title: "Project Name", description: "The displayed name of this project. If left empty, the root directory name will be displayed.", field: Box::new( @@ -1022,7 +1022,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), // todo(settings_ui): This needs a custom component SettingsPageItem::SettingItem(SettingItem { @@ -1046,7 +1046,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), ], }, @@ -2117,7 +2117,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), ]); @@ -2314,7 +2314,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { items.extend(all_language_names(cx).into_iter().map(|language_name| { SettingsPageItem::SubPageLink(SubPageLink { title: language_name, - files: USER | LOCAL, + files: USER | PROJECT, render: Arc::new(|this, window, cx| { this.render_sub_page_items( language_settings_data() @@ -4432,7 +4432,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { SettingsPageItem::SectionHeader("Environment"), SettingsPageItem::DynamicItem(DynamicItem { discriminant: SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Shell", description: "What shell to use when opening a terminal.", field: Box::new(SettingField { @@ -4496,7 +4496,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { settings::ShellDiscriminants::System => vec![], settings::ShellDiscriminants::Program => vec![ SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Program", description: "The shell program to use.", field: Box::new(SettingField { @@ -4526,7 +4526,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { ], settings::ShellDiscriminants::WithArguments => vec![ SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Program", description: "The shell program to run.", field: Box::new(SettingField { @@ -4554,7 +4554,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { metadata: None, }, SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Arguments", description: "The arguments to pass to the shell program.", field: Box::new( @@ -4585,7 +4585,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { metadata: None, }, SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Title Override", description: "An optional string to override the title of the terminal tab.", field: Box::new(SettingField { @@ -4615,7 +4615,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { }), SettingsPageItem::DynamicItem(DynamicItem { discriminant: SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Working Directory", description: "What working directory to use when launching the terminal.", field: Box::new(SettingField { @@ -4672,7 +4672,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { settings::WorkingDirectoryDiscriminants::AlwaysHome => vec![], settings::WorkingDirectoryDiscriminants::Always => vec![ SettingItem { - files: USER | LOCAL, + files: USER | PROJECT, title: "Directory", description: "The directory path to use (will be shell expanded).", field: Box::new(SettingField { @@ -4721,7 +4721,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Detect Virtual Environment", @@ -4748,7 +4748,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Font"), SettingsPageItem::SettingItem(SettingItem { @@ -5813,7 +5813,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Hard Tabs", @@ -5832,7 +5832,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Auto Indent", @@ -5851,7 +5851,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Auto Indent On Paste", @@ -5870,7 +5870,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Wrapping"), SettingsPageItem::SettingItem(SettingItem { @@ -5890,7 +5890,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Wrap Guides", @@ -5909,7 +5909,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Preferred Line Length", @@ -5928,7 +5928,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Wrap Guides", @@ -5950,7 +5950,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Allow Rewrap", @@ -5969,7 +5969,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Indent Guides"), SettingsPageItem::SettingItem(SettingItem { @@ -5992,7 +5992,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Line Width", @@ -6014,7 +6014,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Active Line Width", @@ -6039,7 +6039,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Coloring", @@ -6061,7 +6061,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Background Coloring", @@ -6086,7 +6086,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Formatting"), SettingsPageItem::SettingItem(SettingItem { @@ -6109,7 +6109,7 @@ fn language_settings_data() -> Vec { }, ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Remove Trailing Whitespace On Save", @@ -6128,7 +6128,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Ensure Final Newline On Save", @@ -6147,7 +6147,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Formatter", @@ -6169,7 +6169,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Use On Type Format", @@ -6188,7 +6188,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Code Actions On Format", @@ -6210,7 +6210,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Autoclose"), SettingsPageItem::SettingItem(SettingItem { @@ -6230,7 +6230,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Use Auto Surround", @@ -6249,7 +6249,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Always Treat Brackets As Autoclosed", @@ -6268,7 +6268,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Jsx Tag Auto Close", @@ -6288,7 +6288,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Edit Predictions"), SettingsPageItem::SettingItem(SettingItem { @@ -6308,7 +6308,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Edit Predictions Disabled In", @@ -6330,7 +6330,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Whitespace"), SettingsPageItem::SettingItem(SettingItem { @@ -6350,7 +6350,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Space Whitespace Indicator", @@ -6372,7 +6372,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Tab Whitespace Indicator", @@ -6394,7 +6394,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Completions"), SettingsPageItem::SettingItem(SettingItem { @@ -6414,7 +6414,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Completion Documentation", @@ -6433,7 +6433,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Words", @@ -6452,7 +6452,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Words Min Length", @@ -6474,7 +6474,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Inlay Hints"), SettingsPageItem::SettingItem(SettingItem { @@ -6494,7 +6494,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Value Hints", @@ -6516,7 +6516,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Type Hints", @@ -6535,7 +6535,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Parameter Hints", @@ -6557,7 +6557,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Other Hints", @@ -6579,7 +6579,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Show Background", @@ -6598,7 +6598,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Edit Debounce Ms", @@ -6620,7 +6620,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Scroll Debounce Ms", @@ -6642,7 +6642,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Toggle On Modifiers Press", @@ -6671,7 +6671,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), ]; if current_language().is_none() { @@ -6709,7 +6709,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Variables", @@ -6732,7 +6732,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Prefer LSP", @@ -6752,7 +6752,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Miscellaneous"), SettingsPageItem::SettingItem(SettingItem { @@ -6774,7 +6774,7 @@ fn language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Middle Click Paste", @@ -6805,7 +6805,7 @@ fn language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), ]); @@ -6886,7 +6886,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Language Servers", @@ -6908,7 +6908,7 @@ fn non_editor_language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Linked Edits", @@ -6927,7 +6927,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Go To Definition Fallback", @@ -6960,7 +6960,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Fetch Timeout (milliseconds)", @@ -6982,7 +6982,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Insert Mode", @@ -7001,7 +7001,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Debuggers"), SettingsPageItem::SettingItem(SettingItem { @@ -7024,7 +7024,7 @@ fn non_editor_language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SectionHeader("Prettier"), SettingsPageItem::SettingItem(SettingItem { @@ -7044,7 +7044,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Parser", @@ -7063,7 +7063,7 @@ fn non_editor_language_settings_data() -> Vec { }, }), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Plugins", @@ -7085,7 +7085,7 @@ fn non_editor_language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), SettingsPageItem::SettingItem(SettingItem { title: "Options", @@ -7107,7 +7107,7 @@ fn non_editor_language_settings_data() -> Vec { .unimplemented(), ), metadata: None, - files: USER | LOCAL, + files: USER | PROJECT, }), ] } diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index de987da4b00fa5bc2a164f213a73e03523a46947..804cf1ad8d12771266b1104dff7b3fc1b1cf97d4 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -12,7 +12,7 @@ use gpui::{ point, prelude::*, px, uniform_list, }; use heck::ToTitleCase as _; -use project::WorktreeId; +use project::{Project, WorktreeId}; use schemars::JsonSchema; use serde::Deserialize; use settings::{Settings, SettingsContent, SettingsStore}; @@ -33,7 +33,7 @@ use ui::{ }; use ui_input::{NumberField, NumberFieldType}; use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; -use workspace::{OpenOptions, OpenVisible, Workspace, client_side_decorations}; +use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations}; use zed_actions::{OpenSettings, OpenSettingsAt}; use crate::components::{SettingsInputField, font_picker, icon_theme_picker, theme_picker}; @@ -559,7 +559,6 @@ pub fn open_settings_editor( existing_window .update(cx, |settings_window, window, cx| { settings_window.original_window = Some(workspace_handle); - settings_window.observe_last_window_close(cx); window.activate_window(); if let Some(path) = path { open_path(path, settings_window, window, cx); @@ -643,7 +642,6 @@ pub struct SettingsWindow { title_bar: Option>, original_window: Option>, files: Vec<(SettingsUiFile, FocusHandle)>, - drop_down_file: Option, worktree_root_dirs: HashMap, current_file: SettingsUiFile, pages: Vec, @@ -1016,7 +1014,7 @@ impl std::fmt::Debug for FileMask { if self.contains(USER) { items.push("USER"); } - if self.contains(LOCAL) { + if self.contains(PROJECT) { items.push("LOCAL"); } if self.contains(SERVER) { @@ -1028,7 +1026,7 @@ impl std::fmt::Debug for FileMask { } const USER: FileMask = FileMask(1 << 0); -const LOCAL: FileMask = FileMask(1 << 2); +const PROJECT: FileMask = FileMask(1 << 2); const SERVER: FileMask = FileMask(1 << 3); impl std::ops::BitAnd for FileMask { @@ -1138,14 +1136,14 @@ impl SettingsUiFile { fn mask(&self) -> FileMask { match self { SettingsUiFile::User => USER, - SettingsUiFile::Project(_) => LOCAL, + SettingsUiFile::Project(_) => PROJECT, SettingsUiFile::Server(_) => SERVER, } } } impl SettingsWindow { - pub fn new( + fn new( original_window: Option>, window: &mut Window, cx: &mut Context, @@ -1182,6 +1180,60 @@ impl SettingsWindow { }) .detach(); + cx.on_window_closed(|cx| { + if let Some(existing_window) = cx + .windows() + .into_iter() + .find_map(|window| window.downcast::()) + && cx.windows().len() == 1 + { + cx.update_window(*existing_window, |_, window, _| { + window.remove_window(); + }) + .ok(); + } + }) + .detach(); + + if let Some(app_state) = AppState::global(cx).upgrade() { + for project in app_state + .workspace_store + .read(cx) + .workspaces() + .iter() + .filter_map(|space| { + space + .read(cx) + .ok() + .map(|workspace| workspace.project().clone()) + }) + .collect::>() + { + cx.subscribe_in(&project, window, Self::handle_project_event) + .detach(); + } + } else { + log::error!("App state doesn't exist when creating a new settings window"); + } + + let this_weak = cx.weak_entity(); + cx.observe_new::({ + move |_, window, cx| { + let project = cx.entity(); + let Some(window) = window else { + return; + }; + + this_weak + .update(cx, |_, cx| { + cx.subscribe_in(&project, window, Self::handle_project_event) + .detach(); + }) + .ok(); + } + }) + .detach(); + let title_bar = if !cfg!(target_os = "macos") { Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx))) } else { @@ -1198,7 +1250,7 @@ impl SettingsWindow { worktree_root_dirs: HashMap::default(), files: vec![], - drop_down_file: None, + current_file: current_file, pages: vec![], navbar_entries: vec![], @@ -1232,8 +1284,6 @@ impl SettingsWindow { list_state, }; - this.observe_last_window_close(cx); - this.fetch_files(window, cx); this.build_ui(window, cx); this.build_search_index(); @@ -1245,21 +1295,21 @@ impl SettingsWindow { this } - fn observe_last_window_close(&mut self, cx: &mut App) { - cx.on_window_closed(|cx| { - if let Some(existing_window) = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()) - && cx.windows().len() == 1 - { - cx.update_window(*existing_window, |_, window, _| { - window.remove_window(); - }) - .ok(); + fn handle_project_event( + &mut self, + _: &Entity, + event: &project::Event, + window: &mut Window, + cx: &mut Context, + ) { + match event { + project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { + cx.defer_in(window, |this, window, cx| { + this.fetch_files(window, cx); + }); } - }) - .detach(); + _ => {} + } } fn toggle_navbar_entry(&mut self, nav_entry_index: usize) { @@ -1702,7 +1752,7 @@ impl SettingsWindow { .iter() .any(|(file, _)| file == &self.current_file); if !current_file_still_exists { - self.change_file(0, false, window, cx); + self.change_file(0, window, cx); } } @@ -1732,21 +1782,12 @@ impl SettingsWindow { self.open_navbar_entry_page(first_navbar_entry_index); } - fn change_file( - &mut self, - ix: usize, - drop_down_file: bool, - window: &mut Window, - cx: &mut Context, - ) { + fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { if ix >= self.files.len() { self.current_file = SettingsUiFile::User; self.build_ui(window, cx); return; } - if drop_down_file { - self.drop_down_file = Some(ix); - } if self.files[ix].0 == self.current_file { return; @@ -1770,7 +1811,7 @@ impl SettingsWindow { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - const OVERFLOW_LIMIT: usize = 1; + static OVERFLOW_LIMIT: usize = 1; let file_button = |ix, file: &SettingsUiFile, focus_handle, cx: &mut Context| { @@ -1785,7 +1826,7 @@ impl SettingsWindow { .on_click(cx.listener({ let focus_handle = focus_handle.clone(); move |this, _: &gpui::ClickEvent, window, cx| { - this.change_file(ix, false, window, cx); + this.change_file(ix, window, cx); focus_handle.focus(window); } })) @@ -1810,25 +1851,24 @@ impl SettingsWindow { ), ) .when(self.files.len() > OVERFLOW_LIMIT, |div| { - div.children( - self.files - .iter() - .enumerate() - .skip(OVERFLOW_LIMIT) - .find(|(_, (file, _))| file == &self.current_file) - .map(|(ix, (file, focus_handle))| { - file_button(ix, file, focus_handle, cx) - }) - .or_else(|| { - let ix = self.drop_down_file.unwrap_or(OVERFLOW_LIMIT); - self.files.get(ix).map(|(file, focus_handle)| { - file_button(ix, file, focus_handle, cx) - }) - }), - ) - .when( - self.files.len() > OVERFLOW_LIMIT + 1, - |div| { + let selected_file_ix = self + .files + .iter() + .enumerate() + .skip(OVERFLOW_LIMIT) + .find_map(|(ix, (file, _))| { + if file == &self.current_file { + Some(ix) + } else { + None + } + }) + .unwrap_or(OVERFLOW_LIMIT); + + let (file, focus_handle) = &self.files[selected_file_ix]; + + div.child(file_button(selected_file_ix, file, focus_handle, cx)) + .when(self.files.len() > OVERFLOW_LIMIT + 1, |div| { div.child( DropdownMenu::new( "more-files", @@ -1840,18 +1880,19 @@ impl SettingsWindow { .enumerate() .skip(OVERFLOW_LIMIT + 1) { - let (display_name, focus_handle) = if self - .drop_down_file - .is_some_and(|drop_down_ix| drop_down_ix == ix) - { - ix = OVERFLOW_LIMIT; - ( - self.display_name(&self.files[ix].0), - self.files[ix].1.clone(), - ) - } else { - (self.display_name(&file), focus_handle.clone()) - }; + let (display_name, focus_handle) = + if selected_file_ix == ix { + ix = OVERFLOW_LIMIT; + ( + self.display_name(&self.files[ix].0), + self.files[ix].1.clone(), + ) + } else { + ( + self.display_name(&file), + focus_handle.clone(), + ) + }; menu = menu.entry( display_name @@ -1861,9 +1902,7 @@ impl SettingsWindow { let this = this.clone(); move |window, cx| { this.update(cx, |this, cx| { - this.change_file( - ix, true, window, cx, - ); + this.change_file(ix, window, cx); }); focus_handle.focus(window); } @@ -1884,8 +1923,7 @@ impl SettingsWindow { }) .tab_index(0), ) - }, - ) + }) }), ) .child( @@ -3399,7 +3437,6 @@ pub mod test { worktree_root_dirs: HashMap::default(), files: Vec::default(), current_file: crate::SettingsUiFile::User, - drop_down_file: None, pages, search_bar: cx.new(|cx| Editor::single_line(window, cx)), navbar_entry: selected_idx.expect("Must have a selected navbar entry"), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c0e59060bd9ca963343761b77f5b25b18dd8b302..d4547266f43d21b47b5f7efa248602c9c866f391 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1458,7 +1458,7 @@ impl Workspace { }), cx.on_release(move |this, cx| { this.app_state.workspace_store.update(cx, move |store, _| { - store.workspaces.remove(&window_handle.clone()); + store.workspaces.remove(&window_handle); }) }), ]; From 05c2cc02542cdfa55787923f6411e49d9c4943c3 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 23 Oct 2025 02:23:12 -0400 Subject: [PATCH 183/202] settings_ui: Enable editing project settings for worktrees without setting file (#40971) I made three significant changes in this PR. 1. `SettingsWindow::fetch_files` now creates `SettingsUiFile::Project(..)` for any worktree that contains no project settings. 2. `update_settings_file` now creates an empty settings file if a worktree doesn't contain one. 3. `open_current_settings_file` also creates a settings file if the current one doesn't exist. Release Notes: - settings ui: Enable editing project settings for worktrees that don't have a project setting file. --- crates/settings_ui/src/settings_ui.rs | 178 +++++++++++++++++++------- 1 file changed, 130 insertions(+), 48 deletions(-) diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 804cf1ad8d12771266b1104dff7b3fc1b1cf97d4..c08e00ffbe483846106e1f2de2b86de5c1bb5eb9 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -1745,7 +1745,41 @@ impl SettingsWindow { .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true)); ui_files.push((settings_ui_file, focus_handle)); } + ui_files.reverse(); + + let mut missing_worktrees = Vec::new(); + + for worktree in all_projects(cx) + .flat_map(|project| project.read(cx).worktrees(cx)) + .filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id())) + { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let Some(directory_name) = worktree.root_dir().and_then(|file| { + file.file_name() + .map(|os_string| os_string.to_string_lossy().to_string()) + }) else { + continue; + }; + + missing_worktrees.push((worktree_id, directory_name.clone())); + let path = RelPath::empty().to_owned().into_arc(); + + let settings_ui_file = SettingsUiFile::Project((worktree_id, path)); + + let focus_handle = prev_files + .iter() + .find_map(|(prev_file, handle)| { + (prev_file == &settings_ui_file).then(|| handle.clone()) + }) + .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true)); + + ui_files.push((settings_ui_file, focus_handle)); + } + + self.worktree_root_dirs.extend(missing_worktrees); + self.files = ui_files; let current_file_still_exists = self .files @@ -2718,6 +2752,9 @@ impl SettingsWindow { ); } + /// This function will create a new settings file if one doesn't exist + /// if the current file is a project settings with a valid worktree id + /// We do this because the settings ui allows initializing project settings fn open_current_settings_file(&mut self, cx: &mut Context) { match &self.current_file { SettingsUiFile::User => { @@ -2762,58 +2799,83 @@ impl SettingsWindow { .ok(); } SettingsUiFile::Project((worktree_id, path)) => { - let mut corresponding_workspace: Option> = None; let settings_path = path.join(paths::local_settings_file_relative_path()); let Some(app_state) = workspace::AppState::global(cx).upgrade() else { return; }; - for workspace in app_state.workspace_store.read(cx).workspaces() { - let contains_settings_file = workspace - .read_with(cx, |workspace, cx| { - workspace.project().read(cx).contains_local_settings_file( - *worktree_id, - settings_path.as_ref(), - cx, - ) - }) - .ok(); - if Some(true) == contains_settings_file { - corresponding_workspace = Some(*workspace); - - break; - } - } - let Some(corresponding_workspace) = corresponding_workspace else { + let Some((worktree, corresponding_workspace)) = app_state + .workspace_store + .read(cx) + .workspaces() + .iter() + .find_map(|workspace| { + workspace + .read_with(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .worktree_for_id(*worktree_id, cx) + }) + .ok() + .flatten() + .zip(Some(*workspace)) + }) + else { log::error!( - "No corresponding workspace found for settings file {}", - settings_path.as_std_path().display() + "No corresponding workspace contains worktree id: {}", + worktree_id ); return; }; + let create_task = if worktree.read(cx).entry_for_path(&settings_path).is_some() { + None + } else { + Some(worktree.update(cx, |tree, cx| { + tree.create_entry( + settings_path.clone(), + false, + Some("{\n\n}".as_bytes().to_vec()), + cx, + ) + })) + }; + + let worktree_id = *worktree_id; + // TODO: move zed::open_local_file() APIs to this crate, and // re-implement the "initial_contents" behavior corresponding_workspace - .update(cx, |workspace, window, cx| { - let open_task = workspace.open_path( - (*worktree_id, settings_path.clone()), - None, - true, - window, - cx, - ); - + .update(cx, |_, window, cx| { cx.spawn_in(window, async move |workspace, cx| { - if open_task.await.log_err().is_some() { - workspace - .update_in(cx, |_, window, cx| { - window.activate_window(); - cx.notify(); - }) - .ok(); - } + if let Some(create_task) = create_task { + create_task.await.ok()?; + }; + + workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path( + (worktree_id, settings_path.clone()), + None, + true, + window, + cx, + ) + }) + .ok()? + .await + .log_err()?; + + workspace + .update_in(cx, |_, window, cx| { + window.activate_window(); + cx.notify(); + }) + .ok(); + + Some(()) }) .detach(); }) @@ -3033,20 +3095,40 @@ fn update_settings_file( match file { SettingsUiFile::Project((worktree_id, rel_path)) => { let rel_path = rel_path.join(paths::local_settings_file_relative_path()); - let project = all_projects(cx).find(|project| { - project.read_with(cx, |project, cx| { - project.contains_local_settings_file(worktree_id, &rel_path, cx) - }) - }); - let Some(project) = project else { - anyhow::bail!( - "Could not find worktree containing settings file: {}", - &rel_path.display(PathStyle::local()) - ); + let Some((worktree, project)) = all_projects(cx).find_map(|project| { + project + .read(cx) + .worktree_for_id(worktree_id, cx) + .zip(Some(project)) + }) else { + anyhow::bail!("Could not find project with worktree id: {}", worktree_id); }; + project.update(cx, |project, cx| { - project.update_local_settings_file(worktree_id, rel_path, cx, update); + let task = if project.contains_local_settings_file(worktree_id, &rel_path, cx) { + None + } else { + Some(worktree.update(cx, |worktree, cx| { + worktree.create_entry(rel_path.clone(), false, None, cx) + })) + }; + + cx.spawn(async move |project, cx| { + if let Some(task) = task + && task.await.is_err() + { + return; + }; + + project + .update(cx, |project, cx| { + project.update_local_settings_file(worktree_id, rel_path, cx, update); + }) + .ok(); + }) + .detach(); }); + return Ok(()); } SettingsUiFile::User => { From 5a05986479d39fc179b38bb4f7d69f5cce7ce422 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 23 Oct 2025 03:37:37 -0400 Subject: [PATCH 184/202] debugger: Fix debug scenario picker not showing language subtitles (#40977) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Before Screenshot 2025-10-23 at 2 58 44 AM ### After Screenshot 2025-10-23 at 3 08 59 AM I also changed the debug picker to use a material list to cover the edge case where there isn't a subtitle for an entry Release Notes: - debugger: Fix debug scenario picker not showing language subtitles --- crates/debugger_ui/src/new_process_modal.rs | 6 ++++-- crates/picker/src/picker.rs | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index cf3c779abad2073892a45acb4616298ef85af043..e12c768e12b1e098e150027c89d05695c59c51f6 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -96,7 +96,9 @@ impl NewProcessModal { let debug_picker = cx.new(|cx| { let delegate = DebugDelegate::new(debug_panel.downgrade(), task_store.clone()); - Picker::uniform_list(delegate, window, cx).modal(false) + Picker::list(delegate, window, cx) + .modal(false) + .list_measure_all() }); let configure_mode = ConfigureMode::new(window, cx); @@ -1050,7 +1052,7 @@ impl DebugDelegate { Some(TaskSourceKind::Lsp { language_name, .. }) => { Some(format!("LSP: {language_name}")) } - Some(TaskSourceKind::Language { .. }) => None, + Some(TaskSourceKind::Language { name }) => Some(format!("Lang: {name}")), _ => context.clone().and_then(|ctx| { ctx.task_context .task_variables diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 247fcbdd875ffc2e52d90d9b1309f874c508e588..90423bcace0ad405e0c88703efe09f39a8763778 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -352,6 +352,16 @@ impl Picker { self } + pub fn list_measure_all(mut self) -> Self { + match self.element_container { + ElementContainer::List(state) => { + self.element_container = ElementContainer::List(state.measure_all()); + } + _ => {} + } + self + } + pub fn focus(&self, window: &mut Window, cx: &mut App) { self.focus_handle(cx).focus(window); } From 278032c6b815aea8ce749af5e1580ddd13043a5c Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 09:41:05 +0200 Subject: [PATCH 185/202] extension_host: Run extensions on the tokio threadpool (#40936) Fixes ZED-12D `wasmtime_wasi` might call into tokio futures (to sleep for example) which requires access to the tokio runtime. So we are required to run these extensions in the tokio thread pool Release Notes: - Fixed extensions causing zed to occasionally panic --- Cargo.lock | 1 + crates/extension_host/Cargo.toml | 1 + .../extension_compilation_benchmark.rs | 3 +- .../src/extension_store_test.rs | 1 + crates/extension_host/src/wasm_host.rs | 42 ++++++++++++------- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 48db1977efa9772c1d253e9382ef788664056b7a..eb5df527243bf130f7bd11735f767ed51807bc6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5883,6 +5883,7 @@ dependencies = [ "fs", "futures 0.3.31", "gpui", + "gpui_tokio", "http_client", "language", "language_extension", diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml index 42dcdd3a6f3fd7da3d40bc4cc5437ffdfcd688c5..16cbd9ac0c0ef938322f2b57789c7542549a570a 100644 --- a/crates/extension_host/Cargo.toml +++ b/crates/extension_host/Cargo.toml @@ -27,6 +27,7 @@ extension.workspace = true fs.workspace = true futures.workspace = true gpui.workspace = true +gpui_tokio.workspace = true http_client.workspace = true language.workspace = true log.workspace = true diff --git a/crates/extension_host/benches/extension_compilation_benchmark.rs b/crates/extension_host/benches/extension_compilation_benchmark.rs index 6f0897af6edbb38acef305ff03b76569a741aca5..309e089758eab8bed1139e2d813bc99b1febb594 100644 --- a/crates/extension_host/benches/extension_compilation_benchmark.rs +++ b/crates/extension_host/benches/extension_compilation_benchmark.rs @@ -19,6 +19,7 @@ use util::test::TempTree; fn extension_benchmarks(c: &mut Criterion) { let cx = init(); + cx.update(gpui_tokio::init); let mut group = c.benchmark_group("load"); @@ -37,7 +38,7 @@ fn extension_benchmarks(c: &mut Criterion) { |wasm_bytes| { let _extension = cx .executor() - .block(wasm_host.load_extension(wasm_bytes, &manifest, cx.executor())) + .block(wasm_host.load_extension(wasm_bytes, &manifest, &cx.to_async())) .unwrap(); }, BatchSize::SmallInput, diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index 855077bcf87c58fb8e751d6477921d7e8bba8ad9..509edc6845c6e99745a4b94944cf5f2b68ff9b93 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -868,5 +868,6 @@ fn init_test(cx: &mut TestAppContext) { Project::init_settings(cx); ExtensionSettings::register(cx); language::init(cx); + gpui_tokio::init(cx); }); } diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index f77258e8957fa1be7579b931de82fd633a0f6ae4..22d11732a743d56e651ce71279fe4d276f269640 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -591,11 +591,12 @@ impl WasmHost { self: &Arc, wasm_bytes: Vec, manifest: &Arc, - executor: BackgroundExecutor, + cx: &AsyncApp, ) -> Task> { let this = self.clone(); let manifest = manifest.clone(); - executor.clone().spawn(async move { + let executor = cx.background_executor().clone(); + let load_extension_task = async move { let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?; let component = Component::from_binary(&this.engine, &wasm_bytes) @@ -632,20 +633,29 @@ impl WasmHost { .context("failed to initialize wasm extension")?; let (tx, mut rx) = mpsc::unbounded::(); - executor - .spawn(async move { - while let Some(call) = rx.next().await { - (call)(&mut extension, &mut store).await; - } - }) - .detach(); + let extension_task = async move { + while let Some(call) = rx.next().await { + (call)(&mut extension, &mut store).await; + } + }; - Ok(WasmExtension { - manifest: manifest.clone(), - work_dir: this.work_dir.join(manifest.id.as_ref()).into(), - tx, - zed_api_version, - }) + anyhow::Ok(( + extension_task, + WasmExtension { + manifest: manifest.clone(), + work_dir: this.work_dir.join(manifest.id.as_ref()).into(), + tx, + zed_api_version, + }, + )) + }; + cx.spawn(async move |cx| { + let (extension_task, extension) = load_extension_task.await?; + // we need to run run the task in an extension context as wasmtime_wasi may + // call into tokio, accessing its runtime handle + gpui_tokio::Tokio::spawn(cx, extension_task)?.detach(); + + Ok(extension) }) } @@ -747,7 +757,7 @@ impl WasmExtension { .context("failed to read wasm")?; wasm_host - .load_extension(wasm_bytes, manifest, cx.background_executor().clone()) + .load_extension(wasm_bytes, manifest, cx) .await .with_context(|| format!("failed to load wasm extension {}", manifest.id)) } From c529a066bf2ce40842b972532c36acb8b8d9f084 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 09:58:33 +0200 Subject: [PATCH 186/202] gpui: Arc `GlobalElementId` (#40979) This shrinks it from roughly a ~kilobyte to 8 byte, removing a bunch of memmoves emitted by the compiler. Also `Arc`'s it instead of boxing as we do clone it a couple times here and there, making that also a fair bit cheaper Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/display_map/block_map.rs | 18 +++++++++--------- crates/gpui/src/element.rs | 14 +++++++------- crates/gpui/src/inspector.rs | 2 +- crates/gpui/src/window.rs | 13 ++++++------- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 8bfcd2f063c663c7ab7bbbc9cefaf50cc0f53192..4535e161392fd53e80ceb80fc736799ffafc84a7 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1186,18 +1186,14 @@ impl BlockMapWriter<'_> { self.0.sync(wrap_snapshot, edits); } - pub fn remove_intersecting_replace_blocks( + pub fn remove_intersecting_replace_blocks( &mut self, - ranges: impl IntoIterator>, + ranges: impl IntoIterator>, inclusive: bool, - ) where - T: ToOffset, - { + ) { let wrap_snapshot = self.0.wrap_snapshot.borrow(); let mut blocks_to_remove = HashSet::default(); for range in ranges { - let range = range.start.to_offset(wrap_snapshot.buffer_snapshot()) - ..range.end.to_offset(wrap_snapshot.buffer_snapshot()); for block in self.blocks_intersecting_buffer_range(range, inclusive) { if matches!(block.placement, BlockPlacement::Replace(_)) { blocks_to_remove.insert(block.id); @@ -3570,8 +3566,12 @@ mod tests { let mut writer = block_map.write(wraps_snapshot.clone(), Default::default()); writer.remove_intersecting_replace_blocks( - [buffer_snapshot.anchor_after(Point::new(1, 0)) - ..buffer_snapshot.anchor_after(Point::new(1, 0))], + [buffer_snapshot + .anchor_after(Point::new(1, 0)) + .to_offset(&buffer_snapshot) + ..buffer_snapshot + .anchor_after(Point::new(1, 0)) + .to_offset(&buffer_snapshot)], false, ); let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 5fa2f9ead8274452ac04795bf68dffc571f5dc31..2c695486c5d09103f69fb211076aec6629a29f1b 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -37,11 +37,11 @@ use crate::{ util::FluentBuilder, }; use derive_more::{Deref, DerefMut}; -pub(crate) use smallvec::SmallVec; use std::{ any::{Any, type_name}, fmt::{self, Debug, Display}, mem, panic, + sync::Arc, }; /// Implemented by types that participate in laying out and painting the contents of a window. @@ -272,8 +272,8 @@ impl IntoElement for Component { } /// A globally unique identifier for an element, used to track state across frames. -#[derive(Deref, DerefMut, Default, Debug, Eq, PartialEq, Hash)] -pub struct GlobalElementId(pub(crate) SmallVec<[ElementId; 32]>); +#[derive(Deref, DerefMut, Clone, Default, Debug, Eq, PartialEq, Hash)] +pub struct GlobalElementId(pub(crate) Arc<[ElementId]>); impl Display for GlobalElementId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -353,7 +353,7 @@ impl Drawable { ElementDrawPhase::Start => { let global_id = self.element.id().map(|element_id| { window.element_id_stack.push(element_id); - GlobalElementId(window.element_id_stack.clone()) + GlobalElementId(Arc::from(&*window.element_id_stack)) }); let inspector_id; @@ -361,7 +361,7 @@ impl Drawable { { inspector_id = self.element.source_location().map(|source| { let path = crate::InspectorElementPath { - global_id: GlobalElementId(window.element_id_stack.clone()), + global_id: GlobalElementId(Arc::from(&*window.element_id_stack)), source_location: source, }; window.build_inspector_element_id(path) @@ -412,7 +412,7 @@ impl Drawable { } => { if let Some(element_id) = self.element.id() { window.element_id_stack.push(element_id); - debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack); + debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack); } let bounds = window.layout_bounds(layout_id); @@ -461,7 +461,7 @@ impl Drawable { } => { if let Some(element_id) = self.element.id() { window.element_id_stack.push(element_id); - debug_assert_eq!(global_id.as_ref().unwrap().0, window.element_id_stack); + debug_assert_eq!(&*global_id.as_ref().unwrap().0, &*window.element_id_stack); } window.next_frame.dispatch_tree.set_active_node(node_id); diff --git a/crates/gpui/src/inspector.rs b/crates/gpui/src/inspector.rs index 9f86576a599845bb9e09760e8001333b9dea745d..ad3ba6a4b693ef3270d570dc98b4e03f7927d388 100644 --- a/crates/gpui/src/inspector.rs +++ b/crates/gpui/src/inspector.rs @@ -39,7 +39,7 @@ mod conditional { impl Clone for InspectorElementPath { fn clone(&self) -> Self { Self { - global_id: crate::GlobalElementId(self.global_id.0.clone()), + global_id: self.global_id.clone(), source_location: self.source_location, } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index aa01e34bbc192cf63da94a9c2e4399ff40f7c9ff..eccad833b4f78563ecfaf8e40e77b62334581cb4 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1848,7 +1848,8 @@ impl Window { f: impl FnOnce(&GlobalElementId, &mut Self) -> R, ) -> R { self.element_id_stack.push(element_id); - let global_id = GlobalElementId(self.element_id_stack.clone()); + let global_id = GlobalElementId(Arc::from(&*self.element_id_stack)); + let result = f(&global_id, self); self.element_id_stack.pop(); result @@ -2260,7 +2261,7 @@ impl Window { self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index ..range.end.accessed_element_states_index] .iter() - .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + .map(|(id, type_id)| (id.clone(), *type_id)), ); self.text_system .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index); @@ -2328,7 +2329,7 @@ impl Window { self.rendered_frame.accessed_element_states[range.start.accessed_element_states_index ..range.end.accessed_element_states_index] .iter() - .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), + .map(|(id, type_id)| (id.clone(), *type_id)), ); self.next_frame.tab_stops.replay( &self.rendered_frame.tab_stops.insertion_history @@ -2650,10 +2651,8 @@ impl Window { { self.invalidator.debug_assert_paint_or_prepaint(); - let key = (GlobalElementId(global_id.0.clone()), TypeId::of::()); - self.next_frame - .accessed_element_states - .push((GlobalElementId(key.0.clone()), TypeId::of::())); + let key = (global_id.clone(), TypeId::of::()); + self.next_frame.accessed_element_states.push(key.clone()); if let Some(any) = self .next_frame From 16f7bd0a2e41b3556b6accefcde9d98e0b71c8e0 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 10:46:50 +0200 Subject: [PATCH 187/202] editor: Translate utf16 to utf8 offsets in `copy_highlight_json` (#40981) Fixes ZED-2FM Release Notes: - Fixed panic in copy highlight json action --- crates/editor/src/editor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b9075e47e4681809228ee827db5805a7b402f921..c13458e3816448057030391b0673a8f731803aa8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -21351,7 +21351,10 @@ impl Editor { if selection.range.is_empty() { None } else { - Some(selection.range) + Some( + snapshot.offset_utf16_to_offset(OffsetUtf16(selection.range.start)) + ..snapshot.offset_utf16_to_offset(OffsetUtf16(selection.range.end)), + ) } }) .unwrap_or_else(|| 0..snapshot.len()); From 3bb4c94ed440a4e093bd72823e8e6a9ff7401e43 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 23 Oct 2025 12:25:36 +0300 Subject: [PATCH 188/202] Revert "Round the scroll offset in editor to fix jumping text (#40401)" (#40982) This reverts commit 3da4cddce205d71dcad760c6af66a8985b16ffbe. The scrolling is ~30% less for the same gesture, and I'm not using anything lodpi: https://github.com/user-attachments/assets/b19521fc-9e29-4bfd-9660-dc1e4c8ae846 Release Notes: - N/A --- crates/editor/src/element.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index efb0bf9a1e5c8a7704eb776e050bc36b4539a99e..7d8e3239373272c2cff204ebb5c2442c7ad3d3da 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7232,16 +7232,9 @@ impl EditorElement { * ScrollPixelOffset::from(max_glyph_advance) - ScrollPixelOffset::from(delta.x * scroll_sensitivity)) / ScrollPixelOffset::from(max_glyph_advance); - - let scale_factor = window.scale_factor(); - let y = (current_scroll_position.y - * ScrollPixelOffset::from(line_height) - * ScrollPixelOffset::from(scale_factor) + let y = (current_scroll_position.y * ScrollPixelOffset::from(line_height) - ScrollPixelOffset::from(delta.y * scroll_sensitivity)) - .round() - / ScrollPixelOffset::from(line_height) - / ScrollPixelOffset::from(scale_factor); - + / ScrollPixelOffset::from(line_height); let mut scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll(); From 8c1b4cb1cd7132493fbad21a5e2665fdb180af2d Mon Sep 17 00:00:00 2001 From: Abderrahmane TAHRI JOUTI <302837+atahrijouti@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:55:26 +0200 Subject: [PATCH 189/202] Re-order Helix keymaps and add alt-o/i/p/n (#40527) Release Notes: - helix: Re-ordered `helix_normal || helix_select` keybindings to follow the same order as the keymap on the helix-editor [documentation](https://docs.helix-editor.com/keymap.html). - helix: Added `alt-o` & `alt-i` to Select larger and smaller syntax node respectively - helix: Added `alt-p` & `alt-n` to Select Next Syntax Node and Previous Syntax Node respectively --- The new main helix normal & select context looks like follows ```jsonc { "context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu", "bindings": { // Movement "h": "vim::WrappingLeft", "left": "vim::WrappingLeft", "l": "vim::WrappingRight", "right": "vim::WrappingRight", "t": ["vim::PushFindForward", { "before": true, "multiline": true }], "f": ["vim::PushFindForward", { "before": false, "multiline": true }], "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }], "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }], "alt-.": "vim::RepeatFind", // Changes "shift-r": "editor::Paste", "`": "vim::ConvertToLowerCase", "alt-`": "vim::ConvertToUpperCase", "insert": "vim::InsertBefore", "shift-u": "editor::Redo", "ctrl-r": "vim::Redo", "y": "vim::HelixYank", "p": "vim::HelixPaste", "shift-p": ["vim::HelixPaste", { "before": true }], ">": "vim::Indent", "<": "vim::Outdent", "=": "vim::AutoIndent", "d": "vim::HelixDelete", "c": "vim::HelixSubstitute", "alt-c": "vim::HelixSubstituteNoYank", // Selection manipulation "s": "vim::HelixSelectRegex", "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }], ";": "vim::HelixCollapseSelection", "alt-;": "vim::OtherEnd", ",": "vim::HelixKeepNewestSelection", "shift-c": "vim::HelixDuplicateBelow", "alt-shift-c": "vim::HelixDuplicateAbove", "%": "editor::SelectAll", "x": "vim::HelixSelectLine", "shift-x": "editor::SelectLine", "ctrl-c": "editor::ToggleComments", "alt-o": "editor::SelectLargerSyntaxNode", "alt-i": "editor::SelectSmallerSyntaxNode", "alt-p": "editor::SelectPreviousSyntaxNode", "alt-n": "editor::SelectNextSyntaxNode", // Goto mode "g e": "vim::EndOfDocument", "g h": "vim::StartOfLine", "g l": "vim::EndOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", "g r": "editor::FindAllReferences", // zed specific "g n": "pane::ActivateNextItem", "shift-l": "pane::ActivateNextItem", "g p": "pane::ActivatePreviousItem", "shift-h": "pane::ActivatePreviousItem", "g .": "vim::HelixGotoLastModification", // go to last modification // Window mode "space w h": "workspace::ActivatePaneLeft", "space w l": "workspace::ActivatePaneRight", "space w k": "workspace::ActivatePaneUp", "space w j": "workspace::ActivatePaneDown", "space w q": "pane::CloseActiveItem", "space w s": "pane::SplitRight", "space w r": "pane::SplitRight", "space w v": "pane::SplitDown", "space w d": "pane::SplitDown", // Space mode "space f": "file_finder::Toggle", "space k": "editor::Hover", "space s": "outline::Toggle", "space shift-s": "project_symbols::Toggle", "space d": "editor::GoToDiagnostic", "space r": "editor::Rename", "space a": "editor::ToggleCodeActions", "space h": "editor::SelectAllMatches", "space c": "editor::ToggleComments", "space p": "editor::Paste", "space y": "editor::Copy", // Other ":": "command_palette::Toggle", "m": "vim::PushHelixMatch", "]": ["vim::PushHelixNext", { "around": true }], "[": ["vim::PushHelixPrevious", { "around": true }], "g q": "vim::PushRewrap", "g w": "vim::PushRewrap", // "tab": "pane::ActivateNextItem", // "shift-tab": "pane::ActivatePrevItem", } } ``` --- assets/keymaps/vim.json | 103 ++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index bd8e07d1a1e36cfce97580422ea7b1ebd275e35c..1d61628025765bb48b2002f8541bb425e025c142 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -422,56 +422,66 @@ { "context": "(vim_mode == helix_normal || vim_mode == helix_select) && !menu", "bindings": { - ";": "vim::HelixCollapseSelection", - ":": "command_palette::Toggle", - "m": "vim::PushHelixMatch", - "s": "vim::HelixSelectRegex", - "]": ["vim::PushHelixNext", { "around": true }], - "[": ["vim::PushHelixPrevious", { "around": true }], - "left": "vim::WrappingLeft", - "right": "vim::WrappingRight", + // Movement "h": "vim::WrappingLeft", + "left": "vim::WrappingLeft", "l": "vim::WrappingRight", - "y": "vim::HelixYank", - "p": "vim::HelixPaste", - "shift-p": ["vim::HelixPaste", { "before": true }], - "alt-;": "vim::OtherEnd", - "ctrl-r": "vim::Redo", - "f": ["vim::PushFindForward", { "before": false, "multiline": true }], + "right": "vim::WrappingRight", "t": ["vim::PushFindForward", { "before": true, "multiline": true }], - "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }], + "f": ["vim::PushFindForward", { "before": false, "multiline": true }], "shift-t": ["vim::PushFindBackward", { "after": true, "multiline": true }], - ">": "vim::Indent", - "<": "vim::Outdent", - "=": "vim::AutoIndent", + "shift-f": ["vim::PushFindBackward", { "after": false, "multiline": true }], + "alt-.": "vim::RepeatFind", + + // Changes + "shift-r": "editor::Paste", "`": "vim::ConvertToLowerCase", "alt-`": "vim::ConvertToUpperCase", - "g q": "vim::PushRewrap", - "g w": "vim::PushRewrap", "insert": "vim::InsertBefore", - "alt-.": "vim::RepeatFind", + "shift-u": "editor::Redo", + "ctrl-r": "vim::Redo", + "y": "vim::HelixYank", + "p": "vim::HelixPaste", + "shift-p": ["vim::HelixPaste", { "before": true }], + ">": "vim::Indent", + "<": "vim::Outdent", + "=": "vim::AutoIndent", + "d": "vim::HelixDelete", + "c": "vim::HelixSubstitute", + "alt-c": "vim::HelixSubstituteNoYank", + + // Selection manipulation + "s": "vim::HelixSelectRegex", "alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }], + ";": "vim::HelixCollapseSelection", + "alt-;": "vim::OtherEnd", + ",": "vim::HelixKeepNewestSelection", + "shift-c": "vim::HelixDuplicateBelow", + "alt-shift-c": "vim::HelixDuplicateAbove", + "%": "editor::SelectAll", + "x": "vim::HelixSelectLine", + "shift-x": "editor::SelectLine", + "ctrl-c": "editor::ToggleComments", + "alt-o": "editor::SelectLargerSyntaxNode", + "alt-i": "editor::SelectSmallerSyntaxNode", + "alt-p": "editor::SelectPreviousSyntaxNode", + "alt-n": "editor::SelectNextSyntaxNode", + // Goto mode - "g n": "pane::ActivateNextItem", - "g p": "pane::ActivatePreviousItem", - // "tab": "pane::ActivateNextItem", - // "shift-tab": "pane::ActivatePrevItem", - "shift-h": "pane::ActivatePreviousItem", - "shift-l": "pane::ActivateNextItem", - "g l": "vim::EndOfLine", + "g e": "vim::EndOfDocument", "g h": "vim::StartOfLine", + "g l": "vim::EndOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" - "g e": "vim::EndOfDocument", - "g .": "vim::HelixGotoLastModification", // go to last modification - "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", - - "shift-r": "editor::Paste", - "x": "vim::HelixSelectLine", - "shift-x": "editor::SelectLine", - "%": "editor::SelectAll", + "g r": "editor::FindAllReferences", // zed specific + "g n": "pane::ActivateNextItem", + "shift-l": "pane::ActivateNextItem", + "g p": "pane::ActivatePreviousItem", + "shift-h": "pane::ActivatePreviousItem", + "g .": "vim::HelixGotoLastModification", // go to last modification + // Window mode "space w h": "workspace::ActivatePaneLeft", "space w l": "workspace::ActivatePaneRight", @@ -482,6 +492,7 @@ "space w r": "pane::SplitRight", "space w v": "pane::SplitDown", "space w d": "pane::SplitDown", + // Space mode "space f": "file_finder::Toggle", "space k": "editor::Hover", @@ -492,16 +503,18 @@ "space a": "editor::ToggleCodeActions", "space h": "editor::SelectAllMatches", "space c": "editor::ToggleComments", - "space y": "editor::Copy", "space p": "editor::Paste", - "shift-u": "editor::Redo", - "ctrl-c": "editor::ToggleComments", - "d": "vim::HelixDelete", - "c": "vim::HelixSubstitute", - "alt-c": "vim::HelixSubstituteNoYank", - "shift-c": "vim::HelixDuplicateBelow", - "alt-shift-c": "vim::HelixDuplicateAbove", - ",": "vim::HelixKeepNewestSelection" + "space y": "editor::Copy", + + // Other + ":": "command_palette::Toggle", + "m": "vim::PushHelixMatch", + "]": ["vim::PushHelixNext", { "around": true }], + "[": ["vim::PushHelixPrevious", { "around": true }], + "g q": "vim::PushRewrap", + "g w": "vim::PushRewrap", + // "tab": "pane::ActivateNextItem", + // "shift-tab": "pane::ActivatePrevItem", } }, { From 93ef1947b5e1da6d074f7495982ddd71ccb34d01 Mon Sep 17 00:00:00 2001 From: paneutral <97750807+paneutral@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:15:12 +0200 Subject: [PATCH 190/202] vim: Fix cursor movement after entering Helix normal mode (#40528) Closes #40009 Release Notes: - `vim::NormalBefore` now enters `helix_normal` correctly. --- crates/vim/src/insert.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 98d542dbc4d3651b5307959fba01bf7320983cc9..d5323f31dce38dad29831cbbfe551b6f30760ed2 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -50,17 +50,23 @@ impl Vim { if count <= 1 || Vim::globals(cx).dot_replaying { self.create_mark("^".into(), window, cx); + if HelixModeSetting::get_global(cx).0 { + self.update_editor(cx, |_, editor, cx| { + editor.dismiss_menus_and_popups(false, window, cx); + }); + self.switch_mode(Mode::HelixNormal, false, window, cx); + return; + } + self.update_editor(cx, |_, editor, cx| { editor.dismiss_menus_and_popups(false, window, cx); - if !HelixModeSetting::get_global(cx).0 { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) - }); + editor.change_selections(Default::default(), window, cx, |s| { + s.move_cursors_with(|map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) }); - } + }); }); self.switch_mode(Mode::Normal, false, window, cx); From 9a6397fb177cec19b848e2800f9cdc018dbfe984 Mon Sep 17 00:00:00 2001 From: Viraj Bhartiya Date: Thu, 23 Oct 2025 16:51:32 +0530 Subject: [PATCH 191/202] Add keybindings for menu navigation in vim.json (#40877) - Added "ctrl-p" for selecting the previous menu item - Added "ctrl-n" for selecting the next menu item Closes #40619 Release Notes: - Ctrl+P now moves to the previous result; Ctrl+N moves to the next. --- assets/keymaps/vim.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1d61628025765bb48b2002f8541bb425e025c142..da7491a0070cc74d8329d9bae65d445896b77386 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -983,7 +983,9 @@ "bindings": { "ctrl-h": "editor::Backspace", "ctrl-u": "editor::DeleteToBeginningOfLine", - "ctrl-w": "editor::DeleteToPreviousWordStart" + "ctrl-w": "editor::DeleteToPreviousWordStart", + "ctrl-p": "menu::SelectPrevious", + "ctrl-n": "menu::SelectNext" } }, { From 4f0a44896a1aa2a66163d467d627ce14194f7122 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 23 Oct 2025 15:51:07 +0300 Subject: [PATCH 192/202] Fix anchor-related panic when gathering applicable inlay chunks (#41002) Before, inlay chunks were retrieved from the cache based on actualized anchor ranges, but using an old buffer snapshot. Now, update all chunks and snapshot to the actual before returning the applicable ones. Follow-up of https://github.com/zed-industries/zed/pull/40183 Release Notes: - N/A --- crates/editor/src/editor.rs | 15 +++++++-------- crates/editor/src/inlays/inlay_hints.rs | 6 +++++- crates/editor/src/proposed_changes_editor.rs | 6 +++--- crates/project/src/lsp_store.rs | 19 ++++++++----------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c13458e3816448057030391b0673a8f731803aa8..ba6f71822b3cf6adb748f8dab12f503b7c1ca850 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22603,9 +22603,9 @@ pub trait SemanticsProvider { fn applicable_inlay_chunks( &self, - buffer_id: BufferId, + buffer: &Entity, ranges: &[Range], - cx: &App, + cx: &mut App, ) -> Vec>; fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App); @@ -23112,14 +23112,13 @@ impl SemanticsProvider for Entity { fn applicable_inlay_chunks( &self, - buffer_id: BufferId, + buffer: &Entity, ranges: &[Range], - cx: &App, + cx: &mut App, ) -> Vec> { - self.read(cx) - .lsp_store() - .read(cx) - .applicable_inlay_chunks(buffer_id, ranges) + self.read(cx).lsp_store().update(cx, |lsp_store, cx| { + lsp_store.applicable_inlay_chunks(buffer, ranges, cx) + }) } fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App) { diff --git a/crates/editor/src/inlays/inlay_hints.rs b/crates/editor/src/inlays/inlay_hints.rs index 07faf8446749085ed24795451c241c0a5747335f..9a9be1d1591e5b9f5303a0706ce0ded5afba3f83 100644 --- a/crates/editor/src/inlays/inlay_hints.rs +++ b/crates/editor/src/inlays/inlay_hints.rs @@ -330,6 +330,7 @@ impl Editor { } }; + let multi_buffer = self.buffer().clone(); let Some(inlay_hints) = self.inlay_hints.as_mut() else { return; }; @@ -365,6 +366,9 @@ impl Editor { let all_affected_buffers = Arc::new(Mutex::new(all_affected_buffers)); for (buffer_id, visible_excerpts) in buffers_to_query { + let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) else { + continue; + }; let fetched_tasks = inlay_hints.hint_chunk_fetched.entry(buffer_id).or_default(); if visible_excerpts .buffer_version @@ -376,7 +380,7 @@ impl Editor { } let applicable_chunks = - semantics_provider.applicable_inlay_chunks(buffer_id, &visible_excerpts.ranges, cx); + semantics_provider.applicable_inlay_chunks(&buffer, &visible_excerpts.ranges, cx); match inlay_hints .hint_refresh_tasks diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index a8a03d3e5b3f7f72d58f3a12d7b265832f1b2e10..9f5a17bfccfbc88bf4e00fa8d30030958dfe7e6c 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -436,11 +436,11 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { fn applicable_inlay_chunks( &self, - buffer_id: BufferId, + buffer: &Entity, ranges: &[Range], - cx: &App, + cx: &mut App, ) -> Vec> { - self.0.applicable_inlay_chunks(buffer_id, ranges, cx) + self.0.applicable_inlay_chunks(buffer, ranges, cx) } fn invalidate_inlay_hints(&self, for_buffers: &HashSet, cx: &mut App) { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index f33d7af8b6ede6f1ae62109f1390acee51975693..2350c110eee4c8e3f2e838f04ee9fc04292209da 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6499,19 +6499,16 @@ impl LspStore { } pub fn applicable_inlay_chunks( - &self, - buffer_id: BufferId, + &mut self, + buffer: &Entity, ranges: &[Range], + cx: &mut Context, ) -> Vec> { - self.lsp_data - .get(&buffer_id) - .map(|data| { - data.inlay_hints - .applicable_chunks(ranges) - .map(|chunk| chunk.start..chunk.end) - .collect() - }) - .unwrap_or_default() + self.latest_lsp_data(buffer, cx) + .inlay_hints + .applicable_chunks(ranges) + .map(|chunk| chunk.start..chunk.end) + .collect() } pub fn invalidate_inlay_hints<'a>( From 738e2481096c5702fd314c7b46529cbf6760acf4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 15:26:27 +0200 Subject: [PATCH 193/202] gpui: Small perf optimizations (#40767) Some random findings based on profiling Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/bounds_tree.rs | 5 +- crates/gpui/src/key_dispatch.rs | 18 ++--- crates/gpui/src/taffy.rs | 7 +- crates/gpui/src/window.rs | 29 ++++----- crates/project/src/environment.rs | 2 +- crates/rope/src/rope.rs | 105 +++++++++++++++--------------- 6 files changed, 78 insertions(+), 88 deletions(-) diff --git a/crates/gpui/src/bounds_tree.rs b/crates/gpui/src/bounds_tree.rs index a96bfe55b9ff431a96da7bf42692288264eb184c..d621609bf7334801059513e03dfd11b4036ea816 100644 --- a/crates/gpui/src/bounds_tree.rs +++ b/crates/gpui/src/bounds_tree.rs @@ -34,15 +34,14 @@ where pub fn insert(&mut self, new_bounds: Bounds) -> u32 { // If the tree is empty, make the root the new leaf. - if self.root.is_none() { + let Some(mut index) = self.root else { let new_node = self.push_leaf(new_bounds, 1); self.root = Some(new_node); return 1; - } + }; // Search for the best place to add the new leaf based on heuristics. let mut max_intersecting_ordering = 0; - let mut index = self.root.unwrap(); while let Node::Internal { left, right, diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 03ee31fdad5bdfc48e10dbf74a2557ea7ee0036e..f0c857abd6f3c353105b4272b51ca519f1906078 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -572,18 +572,14 @@ impl DispatchTree { focus_path } - pub fn view_path(&self, view_id: EntityId) -> SmallVec<[EntityId; 8]> { - let mut view_path: SmallVec<[EntityId; 8]> = SmallVec::new(); + pub fn view_path_reversed(&self, view_id: EntityId) -> impl Iterator { let mut current_node_id = self.view_node_ids.get(&view_id).copied(); - while let Some(node_id) = current_node_id { - let node = self.node(node_id); - if let Some(view_id) = node.view_id { - view_path.push(view_id); - } - current_node_id = node.parent; - } - view_path.reverse(); // Reverse the path so it goes from the root to the view node. - view_path + + std::iter::successors( + current_node_id.map(|node_id| self.node(node_id)), + |node_id| Some(self.node(node_id.parent?)), + ) + .filter_map(|node| node.view_id) } pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 29b4ce644f7b6e4221b72ac8dacc43b558b3eb8e..11cb0872861321c3c06c3f8a5bf79fdd30eb2275 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -3,7 +3,6 @@ use crate::{ point, size, }; use collections::{FxHashMap, FxHashSet}; -use smallvec::SmallVec; use stacksafe::{StackSafe, stacksafe}; use std::{fmt::Debug, ops::Range}; use taffy::{ @@ -31,6 +30,7 @@ pub struct TaffyLayoutEngine { taffy: TaffyTree, absolute_layout_bounds: FxHashMap>, computed_layouts: FxHashSet, + layout_bounds_scratch_space: Vec, } const EXPECT_MESSAGE: &str = "we should avoid taffy layout errors by construction if possible"; @@ -43,6 +43,7 @@ impl TaffyLayoutEngine { taffy, absolute_layout_bounds: FxHashMap::default(), computed_layouts: FxHashSet::default(), + layout_bounds_scratch_space: Vec::new(), } } @@ -168,7 +169,7 @@ impl TaffyLayoutEngine { // if !self.computed_layouts.insert(id) { - let mut stack = SmallVec::<[LayoutId; 64]>::new(); + let mut stack = &mut self.layout_bounds_scratch_space; stack.push(id); while let Some(id) = stack.pop() { self.absolute_layout_bounds.remove(&id); @@ -177,7 +178,7 @@ impl TaffyLayoutEngine { .children(id.into()) .expect(EXPECT_MESSAGE) .into_iter() - .map(Into::into), + .map(LayoutId::from), ); } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index eccad833b4f78563ecfaf8e40e77b62334581cb4..e7b1e563034f3a025648e12f57d3ad73e83eb2e5 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1316,9 +1316,7 @@ impl Window { for view_id in self .rendered_frame .dispatch_tree - .view_path(view_id) - .into_iter() - .rev() + .view_path_reversed(view_id) { if !self.dirty_views.insert(view_id) { break; @@ -2277,19 +2275,14 @@ impl Window { } self.next_frame.deferred_draws.extend( - self.rendered_frame.deferred_draws - [range.start.deferred_draws_index..range.end.deferred_draws_index] - .iter() - .map(|deferred_draw| DeferredDraw { - current_view: deferred_draw.current_view, - parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), - element_id_stack: deferred_draw.element_id_stack.clone(), - text_style_stack: deferred_draw.text_style_stack.clone(), - priority: deferred_draw.priority, - element: None, - absolute_offset: deferred_draw.absolute_offset, - prepaint_range: deferred_draw.prepaint_range.clone(), - paint_range: deferred_draw.paint_range.clone(), + self.rendered_frame + .deferred_draws + .drain(range.start.deferred_draws_index..range.end.deferred_draws_index) + .map(|mut deferred_draw| { + deferred_draw.parent_node = + reused_subtree.refresh_node_id(deferred_draw.parent_node); + deferred_draw.element = None; + deferred_draw }), ); } @@ -4902,7 +4895,7 @@ pub enum ElementId { /// A code location. CodeLocation(core::panic::Location<'static>), /// A labeled child of an element. - NamedChild(Box, SharedString), + NamedChild(Arc, SharedString), } impl ElementId { @@ -5016,7 +5009,7 @@ impl From<(&'static str, u32)> for ElementId { impl> From<(ElementId, T)> for ElementId { fn from((id, name): (ElementId, T)) -> Self { - ElementId::NamedChild(Box::new(id), name.into()) + ElementId::NamedChild(Arc::new(id), name.into()) } } diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index da2933d317ecaa17f5d4cb199f132712f5f28ac3..4f669545668834a6d93e62a18e5bb4944e01e2d9 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -162,7 +162,7 @@ impl ProjectEnvironment { .get("PATH") .map(|path| path.as_str()) .unwrap_or_default(); - log::info!( + log::debug!( "using project environment variables shell launched in {:?}. PATH={:?}", abs_path, path diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 204b13cfae2c27441de1d9265cd964fa1b4215af..23eda84481ced1228cd54741f48009e012edc0e5 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -389,11 +389,12 @@ impl Rope { if offset >= self.summary().len_utf16 { return self.summary().len; } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&offset, Bias::Left); - let overshoot = offset - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(Default::default(), |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &offset, Bias::Left); + let overshoot = offset - start.0; + start.1 + + item.map_or(Default::default(), |chunk| { chunk.as_slice().offset_utf16_to_offset(overshoot) }) } @@ -402,11 +403,12 @@ impl Rope { if offset >= self.summary().len { return self.summary().lines; } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&offset, Bias::Left); - let overshoot = offset - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(Point::zero(), |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &offset, Bias::Left); + let overshoot = offset - start.0; + start.1 + + item.map_or(Point::zero(), |chunk| { chunk.as_slice().offset_to_point(overshoot) }) } @@ -415,11 +417,12 @@ impl Rope { if offset >= self.summary().len { return self.summary().lines_utf16(); } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&offset, Bias::Left); - let overshoot = offset - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(PointUtf16::zero(), |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &offset, Bias::Left); + let overshoot = offset - start.0; + start.1 + + item.map_or(PointUtf16::zero(), |chunk| { chunk.as_slice().offset_to_point_utf16(overshoot) }) } @@ -428,11 +431,12 @@ impl Rope { if point >= self.summary().lines { return self.summary().lines_utf16(); } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(PointUtf16::zero(), |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &point, Bias::Left); + let overshoot = point - start.0; + start.1 + + item.map_or(PointUtf16::zero(), |chunk| { chunk.as_slice().point_to_point_utf16(overshoot) }) } @@ -441,13 +445,11 @@ impl Rope { if point >= self.summary().lines { return self.summary().len; } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor - .item() - .map_or(0, |chunk| chunk.as_slice().point_to_offset(overshoot)) + let (start, _, item) = + self.chunks + .find::, _>((), &point, Bias::Left); + let overshoot = point - start.0; + start.1 + item.map_or(0, |chunk| chunk.as_slice().point_to_offset(overshoot)) } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { @@ -462,11 +464,12 @@ impl Rope { if point >= self.summary().lines_utf16() { return self.summary().len; } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(0, |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &point, Bias::Left); + let overshoot = point - start.0; + start.1 + + item.map_or(0, |chunk| { chunk.as_slice().point_utf16_to_offset(overshoot, clip) }) } @@ -475,11 +478,12 @@ impl Rope { if point.0 >= self.summary().lines_utf16() { return self.summary().lines; } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point.0, Bias::Left); - let overshoot = Unclipped(point.0 - cursor.start().0); - cursor.start().1 - + cursor.item().map_or(Point::zero(), |chunk| { + let (start, _, item) = + self.chunks + .find::, _>((), &point.0, Bias::Left); + let overshoot = Unclipped(point.0 - start.0); + start.1 + + item.map_or(Point::zero(), |chunk| { chunk.as_slice().unclipped_point_utf16_to_point(overshoot) }) } @@ -492,33 +496,30 @@ impl Rope { } pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - let mut cursor = self.chunks.cursor::(()); - cursor.seek(&offset, Bias::Right); - if let Some(chunk) = cursor.item() { - let overshoot = offset - cursor.start(); - *cursor.start() + chunk.as_slice().clip_offset_utf16(overshoot, bias) + let (start, _, item) = self.chunks.find::((), &offset, Bias::Right); + if let Some(chunk) = item { + let overshoot = offset - start; + start + chunk.as_slice().clip_offset_utf16(overshoot, bias) } else { self.summary().len_utf16 } } pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - let mut cursor = self.chunks.cursor::(()); - cursor.seek(&point, Bias::Right); - if let Some(chunk) = cursor.item() { - let overshoot = point - cursor.start(); - *cursor.start() + chunk.as_slice().clip_point(overshoot, bias) + let (start, _, item) = self.chunks.find::((), &point, Bias::Right); + if let Some(chunk) = item { + let overshoot = point - start; + start + chunk.as_slice().clip_point(overshoot, bias) } else { self.summary().lines } } pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - let mut cursor = self.chunks.cursor::(()); - cursor.seek(&point.0, Bias::Right); - if let Some(chunk) = cursor.item() { - let overshoot = Unclipped(point.0 - cursor.start()); - *cursor.start() + chunk.as_slice().clip_point_utf16(overshoot, bias) + let (start, _, item) = self.chunks.find::((), &point.0, Bias::Right); + if let Some(chunk) = item { + let overshoot = Unclipped(point.0 - start); + start + chunk.as_slice().clip_point_utf16(overshoot, bias) } else { self.summary().lines_utf16() } From 023ac1b64997e659455b29ead86ca9fc7b9083be Mon Sep 17 00:00:00 2001 From: Jakub Konka Date: Thu, 23 Oct 2025 18:06:47 +0200 Subject: [PATCH 194/202] Revert "Use ShellKind::try_quote whenever we need to quote shell args" (#41022) Reverts zed-industries/zed#40912 Closes https://github.com/zed-industries/zed/issues/41010 --- Cargo.lock | 4 + crates/askpass/src/askpass.rs | 10 +- crates/dap_adapters/Cargo.toml | 1 + crates/dap_adapters/src/javascript.rs | 4 +- crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/new_process_modal.rs | 14 +- crates/project/Cargo.toml | 1 + crates/project/src/environment.rs | 4 +- .../project/src/lsp_store/lsp_ext_command.rs | 3 +- crates/project/src/terminals.rs | 25 +-- crates/remote/Cargo.toml | 1 + crates/remote/src/transport/ssh.rs | 87 ++++---- crates/remote/src/transport/wsl.rs | 24 +-- crates/remote_server/src/headless_project.rs | 2 +- crates/{util => task}/src/shell_builder.rs | 7 +- crates/task/src/task.rs | 103 ++++++--- crates/terminal/src/terminal.rs | 14 +- crates/terminal/src/terminal_settings.rs | 2 +- crates/util/src/paths.rs | 34 +-- crates/util/src/shell.rs | 201 ++++-------------- crates/util/src/shell_env.rs | 2 +- crates/util/src/util.rs | 5 +- 22 files changed, 232 insertions(+), 317 deletions(-) rename crates/{util => task}/src/shell_builder.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index eb5df527243bf130f7bd11735f767ed51807bc6b..0b24221bb6594478b70e50be0c03e2456b97e402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4531,6 +4531,7 @@ dependencies = [ "paths", "serde", "serde_json", + "shlex", "smol", "task", "util", @@ -4756,6 +4757,7 @@ dependencies = [ "serde_json", "serde_json_lenient", "settings", + "shlex", "sysinfo 0.37.2", "task", "tasks_ui", @@ -12924,6 +12926,7 @@ dependencies = [ "settings", "sha2", "shellexpand 2.1.2", + "shlex", "smallvec", "smol", "snippet", @@ -13836,6 +13839,7 @@ dependencies = [ "serde", "serde_json", "settings", + "shlex", "smol", "tempfile", "thiserror 2.0.17", diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index 9b5d5848270b13c29e84f5601a60511f8956988c..dfe8a96ee6f19510df06948f94af48d621515747 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -20,7 +20,7 @@ use futures::{ }; use gpui::{AsyncApp, BackgroundExecutor, Task}; use smol::fs; -use util::{ResultExt as _, debug_panic, maybe, paths::PathExt, shell::ShellKind}; +use util::{ResultExt as _, debug_panic, maybe, paths::PathExt}; /// Path to the program used for askpass /// @@ -199,15 +199,9 @@ impl PasswordProxy { let current_exec = std::env::current_exe().context("Failed to determine current zed executable path.")?; - // TODO: Inferred from the use of powershell.exe in askpass_helper_script - let shell_kind = if cfg!(windows) { - ShellKind::PowerShell - } else { - ShellKind::Posix - }; let askpass_program = ASKPASS_PROGRAM .get_or_init(|| current_exec) - .try_shell_safe(shell_kind) + .try_shell_safe() .context("Failed to shell-escape Askpass program path.")? .to_string(); // Create an askpass script that communicates back to this process. diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 253674c0f3da16574b4303faf679abeb310756d8..1593f51cf326b06f6c865d8bca8a8b4712511ff1 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -35,6 +35,7 @@ log.workspace = true paths.workspace = true serde.workspace = true serde_json.workspace = true +shlex.workspace = true smol.workspace = true task.workspace = true util.workspace = true diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 68f5ca7e7976640c5b3e44ec5e2e2b880a6c2407..8c90bfc7c054f147336f9c6330d5f1d4a847d588 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -6,7 +6,7 @@ use gpui::AsyncApp; use serde_json::Value; use std::{path::PathBuf, sync::OnceLock}; use task::DebugRequest; -use util::{ResultExt, maybe, shell::ShellKind}; +use util::{ResultExt, maybe}; use crate::*; @@ -67,7 +67,7 @@ impl JsDebugAdapter { .get("type") .filter(|value| value == &"node-terminal")?; let command = configuration.get("command")?.as_str()?.to_owned(); - let mut args = ShellKind::Posix.split(&command)?.into_iter(); + let mut args = shlex::split(&command)?.into_iter(); let program = args.next()?; configuration.insert("runtimeExecutable".to_owned(), program.into()); configuration.insert( diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index c1a0657c0ed93508acb330a98dc6d1c1ee91c570..28866f0d273ce990b51615157412c9b120220d7b 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -60,6 +60,7 @@ serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true settings.workspace = true +shlex.workspace = true sysinfo.workspace = true task.workspace = true tasks_ui.workspace = true diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index e12c768e12b1e098e150027c89d05695c59c51f6..c7cfedf5a2d03fa6d89d28a412eefba750a2e5be 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -32,7 +32,7 @@ use ui::{ SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, relative, rems, v_flex, }; -use util::{ResultExt, rel_path::RelPath, shell::ShellKind}; +use util::{ResultExt, rel_path::RelPath}; use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; @@ -839,11 +839,7 @@ impl ConfigureMode { }; } let command = self.program.read(cx).text(cx); - let mut args = ShellKind::Posix - .split(&command) - .into_iter() - .flatten() - .peekable(); + let mut args = shlex::split(&command).into_iter().flatten().peekable(); let mut env = FxHashMap::default(); while args.peek().is_some_and(|arg| arg.contains('=')) { let arg = args.next().unwrap(); @@ -1269,11 +1265,7 @@ impl PickerDelegate for DebugDelegate { }) .unwrap_or_default(); - let mut args = ShellKind::Posix - .split(&text) - .into_iter() - .flatten() - .peekable(); + let mut args = shlex::split(&text).into_iter().flatten().peekable(); let mut env = HashMap::default(); while args.peek().is_some_and(|arg| arg.contains('=')) { let arg = args.next().unwrap(); diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index d9285a8c24ec5130dd8ce8abf5bbd77c830e0f3f..0297611d101ad883c3d68d7dff9e0a92f00c5b71 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -72,6 +72,7 @@ serde_json.workspace = true settings.workspace = true sha2.workspace = true shellexpand.workspace = true +shlex.workspace = true smallvec.workspace = true smol.workspace = true snippet.workspace = true diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index 4f669545668834a6d93e62a18e5bb4944e01e2d9..4dd1e94b9fce6bcf99e49402b8e5d05ece041f40 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -4,7 +4,7 @@ use language::Buffer; use remote::RemoteClient; use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID}; use std::{collections::VecDeque, path::Path, sync::Arc}; -use task::{Shell, shell_to_proto}; +use task::Shell; use util::ResultExt; use worktree::Worktree; @@ -198,7 +198,7 @@ impl ProjectEnvironment { .proto_client() .request(proto::GetDirectoryEnvironment { project_id: REMOTE_SERVER_PROJECT_ID, - shell: Some(shell_to_proto(shell.clone())), + shell: Some(shell.clone().to_proto()), directory: abs_path.to_string_lossy().to_string(), }); cx.spawn(async move |_, _| { diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index 5066143244da890a63ead6650cb61fdb71d3964a..c79e3df178290fa614e08a8abd85a527a696b003 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -657,7 +657,6 @@ impl LspCommand for GetLspRunnables { ); task_template.args.extend(cargo.cargo_args); if !cargo.executable_args.is_empty() { - let shell_kind = task_template.shell.shell_kind(cfg!(windows)); task_template.args.push("--".to_string()); task_template.args.extend( cargo @@ -683,7 +682,7 @@ impl LspCommand for GetLspRunnables { // That bit is not auto-expanded when using single quotes. // Escape extra cargo args unconditionally as those are unlikely to contain `~`. .flat_map(|extra_arg| { - shell_kind.try_quote(&extra_arg).map(|s| s.to_string()) + shlex::try_quote(&extra_arg).ok().map(|s| s.to_string()) }), ); } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index cf25735e4bf490847e20c92b19e791f8bef56b9b..4a0a1790b49449fd82b8aeff58f6c11c8e63261b 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -168,19 +168,20 @@ impl Project { match remote_client { Some(remote_client) => match activation_script.clone() { activation_script if !activation_script.is_empty() => { - let separator = shell_kind.sequential_commands_separator(); - let activation_script = - activation_script.join(&format!("{separator} ")); + let activation_script = activation_script.join("; "); let to_run = format_to_run(); - let shell = remote_client - .read(cx) - .shell() - .unwrap_or_else(get_default_system_shell); - let arg = format!("{activation_script}{separator} {to_run}"); - let args = shell_kind.args_for_shell(false, arg); - + let args = vec![ + "-c".to_owned(), + format!("{activation_script}; {to_run}"), + ]; create_remote_shell( - Some((&shell, &args)), + Some(( + &remote_client + .read(cx) + .shell() + .unwrap_or_else(get_default_system_shell), + &args, + )), env, path, remote_client, @@ -561,7 +562,7 @@ fn create_remote_shell( Shell::WithArguments { program: command.program, args: command.args, - title_override: Some(format!("{} — Terminal", host)), + title_override: Some(format!("{} — Terminal", host).into()), }, command.env, )) diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml index d1a91af9a5decc88b4c70c69001ba6dad18e4b8b..02560484922fd5b02b348a74493c0af5ca4f78d1 100644 --- a/crates/remote/Cargo.toml +++ b/crates/remote/Cargo.toml @@ -34,6 +34,7 @@ rpc = { workspace = true, features = ["gpui"] } serde.workspace = true serde_json.workspace = true settings.workspace = true +shlex.workspace = true smol.workspace = true tempfile.workspace = true thiserror.workspace = true diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 745391e17ee90b183e517a8ce4c4ab7006493758..a1337c2d65c74b882e19dd832359e297a13b9236 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -203,6 +203,17 @@ impl AsMut for MasterProcess { } } +macro_rules! shell_script { + ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ + format!( + $fmt, + $( + $name = shlex::try_quote($arg).unwrap() + ),+ + ) + }}; +} + #[async_trait(?Send)] impl RemoteConnection for SshRemoteConnection { async fn kill(&self) -> Result<()> { @@ -727,24 +738,21 @@ impl SshRemoteConnection { delegate.set_status(Some("Extracting remote development server"), cx); let server_mode = 0o755; - let shell_kind = ShellKind::Posix; let orig_tmp_path = tmp_path.display(self.path_style()); - let server_mode = format!("{:o}", server_mode); - let server_mode = shell_kind - .try_quote(&server_mode) - .context("shell quoting")?; - let dst_path = dst_path.display(self.path_style()); - let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?; let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - format!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}" + shell_script!( + "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", + server_mode = &format!("{:o}", server_mode), + dst_path = &dst_path.display(self.path_style()), ) } else { - format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}") + shell_script!( + "chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}", + server_mode = &format!("{:o}", server_mode), + dst_path = &dst_path.display(self.path_style()) + ) }; - let script = shell_kind.try_quote(&script).context("shell quoting")?; - let args = shell_kind.args_for_shell(false, script.to_string()); - self.socket.run_command("sh", &args).await?; + self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } @@ -878,12 +886,8 @@ impl SshSocket { // into a machine. You must use `cd` to get back to $HOME. // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" fn ssh_command(&self, program: &str, args: &[impl AsRef]) -> process::Command { - let shell_kind = ShellKind::Posix; let mut command = util::command::new_smol_command("ssh"); - let mut to_run = shell_kind - .try_quote(program) - .expect("shell quoting") - .into_owned(); + let mut to_run = shlex::try_quote(program).unwrap().into_owned(); for arg in args { // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? debug_assert!( @@ -891,10 +895,9 @@ impl SshSocket { "multiline arguments do not work in all shells" ); to_run.push(' '); - to_run.push_str(&shell_kind.try_quote(arg.as_ref()).expect("shell quoting")); + to_run.push_str(&shlex::try_quote(arg.as_ref()).unwrap()); } - let separator = shell_kind.sequential_commands_separator(); - let to_run = format!("cd{separator} {to_run}"); + let to_run = format!("cd; {to_run}"); self.ssh_options(&mut command, true) .arg(self.connection_options.ssh_url()) .arg("-T") @@ -903,7 +906,7 @@ impl SshSocket { command } - async fn run_command(&self, program: &str, args: &[impl AsRef]) -> Result { + async fn run_command(&self, program: &str, args: &[&str]) -> Result { let output = self.ssh_command(program, args).output().await?; anyhow::ensure!( output.status.success(), @@ -1077,10 +1080,7 @@ impl SshConnectionOptions { "-w", ]; - let mut tokens = ShellKind::Posix - .split(input) - .context("invalid input")? - .into_iter(); + let mut tokens = shlex::split(input).context("invalid input")?.into_iter(); 'outer: while let Some(arg) = tokens.next() { if ALLOWED_OPTS.contains(&(&arg as &str)) { @@ -1243,7 +1243,6 @@ fn build_command( ) -> Result { use std::fmt::Write as _; - let shell_kind = ShellKind::new(ssh_shell, false); let mut exec = String::new(); if let Some(working_dir) = working_dir { let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string(); @@ -1253,41 +1252,29 @@ fn build_command( const TILDE_PREFIX: &'static str = "~/"; if working_dir.starts_with(TILDE_PREFIX) { let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - write!(exec, "cd \"$HOME/{working_dir}\" && ",)?; + write!(exec, "cd \"$HOME/{working_dir}\" && ",).unwrap(); } else { - write!(exec, "cd \"{working_dir}\" && ",)?; + write!(exec, "cd \"{working_dir}\" && ",).unwrap(); } } else { - write!(exec, "cd && ")?; + write!(exec, "cd && ").unwrap(); }; - write!(exec, "exec env ")?; + write!(exec, "exec env ").unwrap(); for (k, v) in input_env.iter() { - write!( - exec, - "{}={} ", - k, - shell_kind.try_quote(v).context("shell quoting")? - )?; + if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { + write!(exec, "{}={} ", k, v).unwrap(); + } } if let Some(input_program) = input_program { - write!( - exec, - "{}", - shell_kind - .try_quote(&input_program) - .context("shell quoting")? - )?; + write!(exec, "{}", shlex::try_quote(&input_program).unwrap()).unwrap(); for arg in input_args { - write!( - exec, - " {}", - shell_kind.try_quote(&arg).context("shell quoting")? - )?; + let arg = shlex::try_quote(&arg)?; + write!(exec, " {}", &arg).unwrap(); } } else { - write!(exec, "{ssh_shell} -l")?; + write!(exec, "{ssh_shell} -l").unwrap(); }; let mut args = Vec::new(); diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs index 4ccadee73f99804675c51c9eb6007419b2cacfa7..4eca7c4d5295e4baf8b2812763a02c32701959f7 100644 --- a/crates/remote/src/transport/wsl.rs +++ b/crates/remote/src/transport/wsl.rs @@ -2,7 +2,7 @@ use crate::{ RemoteClientDelegate, RemotePlatform, remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, }; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Result, anyhow, bail}; use async_trait::async_trait; use collections::HashMap; use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; @@ -441,7 +441,6 @@ impl RemoteConnection for WslRemoteConnection { bail!("WSL shares the network interface with the host system"); } - let shell = ShellKind::new(&self.shell, false); let working_dir = working_dir .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string()) .unwrap_or("~".to_string()); @@ -449,26 +448,19 @@ impl RemoteConnection for WslRemoteConnection { let mut exec = String::from("exec env "); for (k, v) in env.iter() { - write!( - exec, - "{}={} ", - k, - shell.try_quote(&v).context("shell quoting")? - )?; + if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) { + write!(exec, "{}={} ", k, v).unwrap(); + } } if let Some(program) = program { - write!( - exec, - "{}", - shell.try_quote(&program).context("shell quoting")? - )?; + write!(exec, "{}", shlex::try_quote(&program)?).unwrap(); for arg in args { - let arg = shell.try_quote(&arg).context("shell quoting")?; - write!(exec, " {}", &arg)?; + let arg = shlex::try_quote(&arg)?; + write!(exec, " {}", &arg).unwrap(); } } else { - write!(&mut exec, "{} -l", self.shell)?; + write!(&mut exec, "{} -l", self.shell).unwrap(); } let wsl_args = if let Some(user) = &self.connection_options.user { diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 588ee836084e350010cfe552d3f294d5f3ae0bcf..83000c8bac3b409a0dad07490a5f028e482f0662 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -774,7 +774,7 @@ impl HeadlessProject { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?; + let shell = task::Shell::from_proto(envelope.payload.shell.context("missing shell")?)?; let directory = PathBuf::from(envelope.payload.directory); let environment = this .update(&mut cx, |this, cx| { diff --git a/crates/util/src/shell_builder.rs b/crates/task/src/shell_builder.rs similarity index 98% rename from crates/util/src/shell_builder.rs rename to crates/task/src/shell_builder.rs index 7e52b67b35f6f3d21ea5e3ad5a0632cd46344125..a6504f4eb765a8a144343691b82cdca9a6802cbd 100644 --- a/crates/util/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,5 +1,8 @@ -use crate::shell::get_system_shell; -use crate::shell::{Shell, ShellKind}; +use util::shell::get_system_shell; + +use crate::Shell; + +pub use util::shell::ShellKind; /// ShellBuilder is used to turn a user-requested task into a /// program that can be executed by the shell. diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index 280bf5ccdb91271d7ff738654d507573c9d667d4..bfb84ced944cda758c7c453f561ca4ec13220c07 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -3,6 +3,7 @@ mod adapter_schema; mod debug_format; mod serde_helpers; +mod shell_builder; pub mod static_source; mod task_template; mod vscode_debug_format; @@ -11,22 +12,23 @@ mod vscode_format; use anyhow::Context as _; use collections::{HashMap, HashSet, hash_map}; use gpui::SharedString; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::path::PathBuf; use std::str::FromStr; +use util::get_system_shell; pub use adapter_schema::{AdapterSchema, AdapterSchemas}; pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; +pub use shell_builder::{ShellBuilder, ShellKind}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, }; -pub use util::shell::{Shell, ShellKind}; -pub use util::shell_builder::ShellBuilder; pub use vscode_debug_format::VsCodeDebugTaskFile; pub use vscode_format::VsCodeTaskFile; pub use zed_actions::RevealTarget; @@ -316,32 +318,81 @@ pub struct TaskContext { #[derive(Clone, Debug)] pub struct RunnableTag(pub SharedString); -pub fn shell_from_proto(proto: proto::Shell) -> anyhow::Result { - let shell_type = proto.shell_type.context("invalid shell type")?; - let shell = match shell_type { - proto::shell::ShellType::System(_) => Shell::System, - proto::shell::ShellType::Program(program) => Shell::Program(program), - proto::shell::ShellType::WithArguments(program) => Shell::WithArguments { - program: program.program, - args: program.args, - title_override: None, - }, - }; - Ok(shell) +/// Shell configuration to open the terminal with. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + /// Use the system's default terminal configuration in /etc/passwd + #[default] + System, + /// Use a specific program with no arguments. + Program(String), + /// Use a specific program with arguments. + WithArguments { + /// The program to run. + program: String, + /// The arguments to pass to the program. + args: Vec, + /// An optional string to override the title of the terminal tab + title_override: Option, + }, } -pub fn shell_to_proto(shell: Shell) -> proto::Shell { - let shell_type = match shell { - Shell::System => proto::shell::ShellType::System(proto::System {}), - Shell::Program(program) => proto::shell::ShellType::Program(program), - Shell::WithArguments { - program, - args, - title_override: _, - } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { program, args }), - }; - proto::Shell { - shell_type: Some(shell_type), +impl Shell { + pub fn program(&self) -> String { + match self { + Shell::Program(program) => program.clone(), + Shell::WithArguments { program, .. } => program.clone(), + Shell::System => get_system_shell(), + } + } + + pub fn program_and_args(&self) -> (String, &[String]) { + match self { + Shell::Program(program) => (program.clone(), &[]), + Shell::WithArguments { program, args, .. } => (program.clone(), args), + Shell::System => (get_system_shell(), &[]), + } + } + + pub fn shell_kind(&self, is_windows: bool) -> ShellKind { + match self { + Shell::Program(program) => ShellKind::new(program, is_windows), + Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), + Shell::System => ShellKind::system(), + } + } + + pub fn from_proto(proto: proto::Shell) -> anyhow::Result { + let shell_type = proto.shell_type.context("invalid shell type")?; + let shell = match shell_type { + proto::shell::ShellType::System(_) => Self::System, + proto::shell::ShellType::Program(program) => Self::Program(program), + proto::shell::ShellType::WithArguments(program) => Self::WithArguments { + program: program.program, + args: program.args, + title_override: None, + }, + }; + Ok(shell) + } + + pub fn to_proto(self) -> proto::Shell { + let shell_type = match self { + Shell::System => proto::shell::ShellType::System(proto::System {}), + Shell::Program(program) => proto::shell::ShellType::Program(program), + Shell::WithArguments { + program, + args, + title_override: _, + } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { + program, + args, + }), + }; + proto::Shell { + shell_type: Some(shell_type), + } } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 68ac3d3e290953363a3246c41652149e1ed5da1f..fa42a94e932a81d171ffc871393a30abf965678f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -67,7 +67,7 @@ use thiserror::Error; use gpui::{ App, AppContext as _, Bounds, ClipboardItem, Context, EventEmitter, Hsla, Keystroke, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Rgba, - ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px, + ScrollWheelEvent, SharedString, Size, Task, TouchPhase, Window, actions, black, px, }; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; @@ -277,7 +277,7 @@ pub struct TerminalError { pub directory: Option, pub program: Option, pub args: Option>, - pub title_override: Option, + pub title_override: Option, pub source: std::io::Error, } @@ -445,14 +445,14 @@ impl TerminalBuilder { struct ShellParams { program: String, args: Option>, - title_override: Option, + title_override: Option, } impl ShellParams { fn new( program: String, args: Option>, - title_override: Option, + title_override: Option, ) -> Self { log::debug!("Using {program} as shell"); Self { @@ -514,8 +514,10 @@ impl TerminalBuilder { working_directory: working_directory.clone(), drain_on_exit: true, env: env.clone().into_iter().collect(), + // We do not want to escape arguments if we are using CMD as our shell. + // If we do we end up with too many quotes/escaped quotes for CMD to handle. #[cfg(windows)] - escape_args: shell_kind.tty_escape_args(), + escape_args: shell_kind != util::shell::ShellKind::Cmd, } }; @@ -822,7 +824,7 @@ pub struct Terminal { pub last_content: TerminalContent, pub selection_head: Option, pub breadcrumb_text: String, - title_override: Option, + title_override: Option, scroll_px: Pixels, next_link_id: usize, selection_phase: SelectionPhase, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index b8576a1de308d8bf3bd098907018b94cb73eefa0..9bb5ffb517b15225eed711a6d4e31e2977626d0a 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -66,7 +66,7 @@ fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell { } => Shell::WithArguments { program, args, - title_override: title_override.map(Into::into), + title_override, }, } } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 20187bf7376861ebd03e02f7fb006428c1c51ec4..0743601839cc31e0e3a4c9d6c936aab7edce5837 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -15,7 +15,7 @@ use std::{ sync::LazyLock, }; -use crate::{rel_path::RelPath, shell::ShellKind}; +use crate::rel_path::RelPath; static HOME_DIR: OnceLock = OnceLock::new(); @@ -84,7 +84,9 @@ pub trait PathExt { fn multiple_extensions(&self) -> Option; /// Try to make a shell-safe representation of the path. - fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result; + /// + /// For Unix, the path is escaped to be safe for POSIX shells + fn try_shell_safe(&self) -> anyhow::Result; } impl> PathExt for T { @@ -162,16 +164,24 @@ impl> PathExt for T { Some(parts.into_iter().join(".")) } - fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result { - let path_str = self - .as_ref() - .to_str() - .with_context(|| "Path contains invalid UTF-8")?; - shell_kind - .try_quote(path_str) - .as_deref() - .map(ToOwned::to_owned) - .context("Failed to quote path") + fn try_shell_safe(&self) -> anyhow::Result { + #[cfg(target_os = "windows")] + { + Ok(self.as_ref().to_string_lossy().to_string()) + } + + #[cfg(not(target_os = "windows"))] + { + let path_str = self + .as_ref() + .to_str() + .with_context(|| "Path contains invalid UTF-8")?; + + // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible + // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other + // errors are introduced in the future :( + Ok(shlex::try_quote(path_str)?.into_owned()) + } } } diff --git a/crates/util/src/shell.rs b/crates/util/src/shell.rs index d81946b8ad207596cfdaf1ec714de94a9b3f71d6..22e07acf25b46161138a297e6de701f74b483861 100644 --- a/crates/util/src/shell.rs +++ b/crates/util/src/shell.rs @@ -1,53 +1,6 @@ -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{borrow::Cow, fmt, path::Path, sync::LazyLock}; -/// Shell configuration to open the terminal with. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - /// Use the system's default terminal configuration in /etc/passwd - #[default] - System, - /// Use a specific program with no arguments. - Program(String), - /// Use a specific program with arguments. - WithArguments { - /// The program to run. - program: String, - /// The arguments to pass to the program. - args: Vec, - /// An optional string to override the title of the terminal tab - title_override: Option, - }, -} - -impl Shell { - pub fn program(&self) -> String { - match self { - Shell::Program(program) => program.clone(), - Shell::WithArguments { program, .. } => program.clone(), - Shell::System => get_system_shell(), - } - } - - pub fn program_and_args(&self) -> (String, &[String]) { - match self { - Shell::Program(program) => (program.clone(), &[]), - Shell::WithArguments { program, args, .. } => (program.clone(), args), - Shell::System => (get_system_shell(), &[]), - } - } - - pub fn shell_kind(&self, is_windows: bool) -> ShellKind { - match self { - Shell::Program(program) => ShellKind::new(program, is_windows), - Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), - Shell::System => ShellKind::system(), - } - } -} - #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ShellKind { #[default] @@ -232,20 +185,32 @@ impl ShellKind { .unwrap_or_else(|| program.as_os_str()) .to_string_lossy(); - match &*program { - "powershell" | "pwsh" => ShellKind::PowerShell, - "cmd" => ShellKind::Cmd, - "nu" => ShellKind::Nushell, - "fish" => ShellKind::Fish, - "csh" => ShellKind::Csh, - "tcsh" => ShellKind::Tcsh, - "rc" => ShellKind::Rc, - "xonsh" => ShellKind::Xonsh, - "sh" | "bash" => ShellKind::Posix, - _ if is_windows => ShellKind::PowerShell, - // Some other shell detected, the user might install and use a - // unix-like shell. - _ => ShellKind::Posix, + if program == "powershell" || program == "pwsh" { + ShellKind::PowerShell + } else if program == "cmd" { + ShellKind::Cmd + } else if program == "nu" { + ShellKind::Nushell + } else if program == "fish" { + ShellKind::Fish + } else if program == "csh" { + ShellKind::Csh + } else if program == "tcsh" { + ShellKind::Tcsh + } else if program == "rc" { + ShellKind::Rc + } else if program == "xonsh" { + ShellKind::Xonsh + } else if program == "sh" || program == "bash" { + ShellKind::Posix + } else { + if is_windows { + ShellKind::PowerShell + } else { + // Some other shell detected, the user might install and use a + // unix-like shell. + ShellKind::Posix + } } } @@ -398,132 +363,44 @@ impl ShellKind { match self { ShellKind::PowerShell => Some('&'), ShellKind::Nushell => Some('^'), - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Cmd - | ShellKind::Xonsh => None, + _ => None, } } pub const fn sequential_commands_separator(&self) -> char { match self { ShellKind::Cmd => '&', - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Nushell - | ShellKind::Xonsh => ';', + _ => ';', } } pub fn try_quote<'a>(&self, arg: &'a str) -> Option> { - // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible - // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other - // errors are introduced in the future :( shlex::try_quote(arg).ok().map(|arg| match self { - ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"").replace("\\\\", "\\")), - ShellKind::Cmd => Cow::Owned(arg.replace("\\\\", "\\")), - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Nushell - | ShellKind::Xonsh => arg, + // If we are running in PowerShell, we want to take extra care when escaping strings. + // In particular, we want to escape strings with a backtick (`) rather than a backslash (\). + // TODO double escaping backslashes is not necessary in PowerShell and probably CMD + ShellKind::PowerShell => Cow::Owned(arg.replace("\\\"", "`\"")), + _ => arg, }) } - pub fn split(&self, input: &str) -> Option> { - shlex::split(input) - } - pub const fn activate_keyword(&self) -> &'static str { match self { ShellKind::Cmd => "", ShellKind::Nushell => "overlay use", ShellKind::PowerShell => ".", - ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Posix - | ShellKind::Rc - | ShellKind::Xonsh => "source", + ShellKind::Fish => "source", + ShellKind::Csh => "source", + ShellKind::Tcsh => "source", + ShellKind::Posix | ShellKind::Rc => "source", + ShellKind::Xonsh => "source", } } pub const fn clear_screen_command(&self) -> &'static str { match self { ShellKind::Cmd => "cls", - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Nushell - | ShellKind::Xonsh => "clear", - } - } - - #[cfg(windows)] - /// We do not want to escape arguments if we are using CMD as our shell. - /// If we do we end up with too many quotes/escaped quotes for CMD to handle. - pub const fn tty_escape_args(&self) -> bool { - match self { - ShellKind::Cmd => false, - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Nushell - | ShellKind::Xonsh => true, + _ => "clear", } } } - -#[cfg(test)] -mod tests { - use super::*; - - // Examples - // WSL - // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello" - // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello" - // wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53 - // PowerShell from Nushell - // nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\"" - // PowerShell from CMD - // cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\" - - #[test] - fn test_try_quote_powershell() { - let shell_kind = ShellKind::PowerShell; - assert_eq!( - shell_kind - .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") - .unwrap() - .into_owned(), - "\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest `\"test_foo.py::test_foo`\"\"".to_string() - ); - } - - #[test] - fn test_try_quote_cmd() { - let shell_kind = ShellKind::Cmd; - assert_eq!( - shell_kind - .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") - .unwrap() - .into_owned(), - "\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"".to_string() - ); - } -} diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs index b3c9e3bef390b945314ba79fcc34ff2669a349a6..a82bea154ec5cb16153b70499eaf7e34c0464995 100644 --- a/crates/util/src/shell_env.rs +++ b/crates/util/src/shell_env.rs @@ -35,8 +35,8 @@ async fn capture_unix( use std::os::unix::process::CommandExt; use std::process::Stdio; + let zed_path = super::get_shell_safe_zed_path()?; let shell_kind = ShellKind::new(shell_path, false); - let zed_path = super::get_shell_safe_zed_path(shell_kind)?; let mut command_string = String::new(); let mut command = std::process::Command::new(shell_path); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 3a78ef3d41e557d33d5af77021464ee1dcadf5e4..f725167724f82b8c4479eca53a5e8f48927b4f8b 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -9,7 +9,6 @@ pub mod rel_path; pub mod schemars; pub mod serde; pub mod shell; -pub mod shell_builder; pub mod shell_env; pub mod size; #[cfg(any(test, feature = "test-support"))] @@ -296,12 +295,12 @@ fn load_shell_from_passwd() -> Result<()> { } /// Returns a shell escaped path for the current zed executable -pub fn get_shell_safe_zed_path(shell_kind: shell::ShellKind) -> anyhow::Result { +pub fn get_shell_safe_zed_path() -> anyhow::Result { let zed_path = std::env::current_exe().context("Failed to determine current zed executable path.")?; zed_path - .try_shell_safe(shell_kind) + .try_shell_safe() .context("Failed to shell-escape Zed executable path.") } From b519ab2758864410b4269692ae04124886b3a0d4 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 18:17:11 +0200 Subject: [PATCH 195/202] rope: Improve chunk slicing panic messages (#41023) We still see a bunch of panics here but the default slicing panic doesn't tell which side of the range is bad Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/rope/src/chunk.rs | 132 +++++++++++++++++++++++++++++++++++---- crates/rope/src/rope.rs | 29 +++++---- 2 files changed, 134 insertions(+), 27 deletions(-) diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index 51904cd8e2217dc56947f6026fff674147cffea5..d0be336c9faf2c5834182387307a7775ba00db38 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -98,6 +98,65 @@ impl Chunk { pub fn is_char_boundary(&self, offset: usize) -> bool { (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() } + + pub fn floor_char_boundary(&self, index: usize) -> usize { + #[inline] + pub(crate) const fn is_utf8_char_boundary(u8: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (u8 as i8) >= -0x40 + } + + if index >= self.text.len() { + self.text.len() + } else { + let mut i = index; + while i > 0 { + if is_utf8_char_boundary(self.text.as_bytes()[i]) { + break; + } + i -= 1; + } + + i + } + } + + #[track_caller] + #[inline(always)] + pub fn assert_char_boundary(&self, offset: usize) { + if self.is_char_boundary(offset) { + return; + } + panic_char_boundary(self, offset); + + #[cold] + #[inline(never)] + fn panic_char_boundary(chunk: &Chunk, offset: usize) { + if offset > chunk.text.len() { + panic!( + "byte index {} is out of bounds of `{:?}` (length: {})", + offset, + chunk.text, + chunk.text.len() + ); + } + // find the character + let char_start = chunk.floor_char_boundary(offset); + // `char_start` must be less than len and a char boundary + let ch = chunk + .text + .get(char_start..) + .unwrap() + .chars() + .next() + .unwrap(); + let char_range = char_start..char_start + ch.len_utf8(); + panic!( + "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", + offset, ch, char_range, + ); + } + } } #[derive(Clone, Copy, Debug)] @@ -167,12 +226,6 @@ impl<'a> ChunkSlice<'a> { #[inline(always)] pub fn slice(self, range: Range) -> Self { - debug_assert!( - self.is_char_boundary(range.end), - "Invalid range end {} in {:?}", - range.end, - self - ); let mask = (1 as Bitmap) .unbounded_shl(range.end as u32) .wrapping_sub(1); @@ -185,12 +238,8 @@ impl<'a> ChunkSlice<'a> { text: "", } } else { - debug_assert!( - self.is_char_boundary(range.start), - "Invalid range start {} in {:?}", - range.start, - self - ); + self.assert_char_boundary(range.start); + self.assert_char_boundary(range.end); Self { chars: (self.chars & mask) >> range.start, chars_utf16: (self.chars_utf16 & mask) >> range.start, @@ -340,6 +389,65 @@ impl<'a> ChunkSlice<'a> { } } + #[track_caller] + #[inline(always)] + pub fn assert_char_boundary(&self, offset: usize) { + if self.is_char_boundary(offset) { + return; + } + panic_char_boundary(self, offset); + + #[cold] + #[inline(never)] + fn panic_char_boundary(chunk: &ChunkSlice, offset: usize) { + if offset > chunk.text.len() { + panic!( + "byte index {} is out of bounds of `{:?}` (length: {})", + offset, + chunk.text, + chunk.text.len() + ); + } + // find the character + let char_start = chunk.floor_char_boundary(offset); + // `char_start` must be less than len and a char boundary + let ch = chunk + .text + .get(char_start..) + .unwrap() + .chars() + .next() + .unwrap(); + let char_range = char_start..char_start + ch.len_utf8(); + panic!( + "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", + offset, ch, char_range, + ); + } + } + + pub fn floor_char_boundary(&self, index: usize) -> usize { + #[inline] + pub(crate) const fn is_utf8_char_boundary(u8: u8) -> bool { + // This is bit magic equivalent to: b < 128 || b >= 192 + (u8 as i8) >= -0x40 + } + + if index >= self.text.len() { + self.text.len() + } else { + let mut i = index; + while i > 0 { + if is_utf8_char_boundary(self.text.as_bytes()[i]) { + break; + } + i -= 1; + } + + i + } + } + #[inline(always)] pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 23eda84481ced1228cd54741f48009e012edc0e5..5a43e22ea5ef43c5b31aeb63d52dcecdea72f5fe 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -51,23 +51,22 @@ impl Rope { #[track_caller] #[inline(always)] pub fn assert_char_boundary(&self, offset: usize) { - if self.is_char_boundary(offset) { + if self.chunks.is_empty() && offset == 0 { return; } - panic_char_boundary(self, offset); - - #[cold] - #[inline(never)] - fn panic_char_boundary(rope: &Rope, offset: usize) { - // find the character - let char_start = rope.floor_char_boundary(offset); - // `char_start` must be less than len and a char boundary - let ch = rope.chars_at(char_start).next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - panic!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", - offset, ch, char_range, - ); + let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); + match item { + Some(chunk) => { + let chunk_offset = offset - start; + chunk.assert_char_boundary(chunk_offset); + } + None => { + panic!( + "byte index {} is out of bounds of rope (length: {})", + offset, + self.len() + ); + } } } From a66098b4852acdda43839847168fbbf9db6faefe Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 18:23:35 +0200 Subject: [PATCH 196/202] gpui: Revert `reuse_prepaint` change of #40767 (#41025) https://github.com/zed-industries/zed/pull/40767/files/c95ae84d919462393455d858836f479dd24bd620#r2455674159 Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/gpui/src/window.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e7b1e563034f3a025648e12f57d3ad73e83eb2e5..0610ea96cb5150cfbad72b2b70b4432df9b76ca2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2275,14 +2275,19 @@ impl Window { } self.next_frame.deferred_draws.extend( - self.rendered_frame - .deferred_draws - .drain(range.start.deferred_draws_index..range.end.deferred_draws_index) - .map(|mut deferred_draw| { - deferred_draw.parent_node = - reused_subtree.refresh_node_id(deferred_draw.parent_node); - deferred_draw.element = None; - deferred_draw + self.rendered_frame.deferred_draws + [range.start.deferred_draws_index..range.end.deferred_draws_index] + .iter() + .map(|deferred_draw| DeferredDraw { + current_view: deferred_draw.current_view, + parent_node: reused_subtree.refresh_node_id(deferred_draw.parent_node), + element_id_stack: deferred_draw.element_id_stack.clone(), + text_style_stack: deferred_draw.text_style_stack.clone(), + priority: deferred_draw.priority, + element: None, + absolute_offset: deferred_draw.absolute_offset, + prepaint_range: deferred_draw.prepaint_range.clone(), + paint_range: deferred_draw.paint_range.clone(), }), ); } From 8b6f3ec647cad5107e7f5eee4641ffb2e632dd82 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 23 Oct 2025 12:46:27 -0400 Subject: [PATCH 197/202] Fix the project diff sometimes missing updates (#40662) This PR does two related things: - First, it gets rid of the undifferentiated `RepositoryEvent::Updated` in favor of three new events that have clearer definitions: `BranchChanged`, `StashEntriesChanged`, and `StatusesChanged`. An implication of this is that we no longer emit a `RepositoryEvent` unless some git state changed; previously we would emit `RepositoryUpdated` after doing a git status scan even if no statuses changed. - Second, it changes the subscription strategy of the project diff to make it update more robustly. Previously, the project diff only subscribed to the `GitStore`, so it relied on getting a `GitStoreEvent` when some buffer's diff hunks changed, even if the git status of the buffer's file didn't change (e.g. a second hunk in a file that was already modified). After this PR, it also subscribes to the individual `BufferDiff` entities for buffers that have a git status, so the `GitStore` is freed from that responsibility. This also fixes some real cases where the previous strategy was not effective in keeping the project diff up to date (captured in a test). Release Notes: - Fixed some cases where the project diff would fail to update in response to git events. --- crates/editor/src/editor.rs | 11 +-- crates/editor/src/editor_tests.rs | 26 +++--- crates/editor/src/git/blame.rs | 6 +- crates/fs/src/fake_git_repo.rs | 54 +++++------ crates/fs/src/fs.rs | 3 + crates/git_ui/src/git_panel.rs | 15 ++- crates/git_ui/src/project_diff.rs | 105 ++++++++++++++++++--- crates/git_ui/src/stash_picker.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 9 +- crates/project/src/git_store.rs | 107 +++++++++++----------- crates/project/src/project_tests.rs | 18 +--- crates/project_panel/src/project_panel.rs | 8 +- crates/title_bar/src/title_bar.rs | 9 +- 13 files changed, 214 insertions(+), 159 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba6f71822b3cf6adb748f8dab12f503b7c1ca850..f3aa166a691b8a29ec8d174b1c9503afbaafc6a4 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -156,7 +156,7 @@ use project::{ }, session::{Session, SessionEvent}, }, - git_store::{GitStoreEvent, RepositoryEvent}, + git_store::GitStoreEvent, lsp_store::{ CacheInlayHints, CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle, @@ -1978,14 +1978,7 @@ impl Editor { let git_store = project.read(cx).git_store().clone(); let project = project.clone(); project_subscriptions.push(cx.subscribe(&git_store, move |this, _, event, cx| { - if let GitStoreEvent::RepositoryUpdated( - _, - RepositoryEvent::Updated { - new_instance: true, .. - }, - _, - ) = event - { + if let GitStoreEvent::RepositoryAdded = event { this.load_diff_task = Some( update_uncommitted_diff_for_buffer( cx.entity(), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ea2ae32ba9a9d589b937e3cbc7939cd8b5bc1b2a..2a3909f069ac491adb8f5675807b647b77d23ac9 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12615,18 +12615,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { ) .await; - cx.run_until_parked(); - // Set up a buffer white some trailing whitespace and no trailing newline. - cx.set_state( - &[ - "one ", // - "twoˇ", // - "three ", // - "four", // - ] - .join("\n"), - ); - // Record which buffer changes have been sent to the language server let buffer_changes = Arc::new(Mutex::new(Vec::new())); cx.lsp @@ -12641,8 +12629,6 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { ); } }); - cx.run_until_parked(); - // Handle formatting requests to the language server. cx.lsp .set_request_handler::({ @@ -12691,6 +12677,18 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) { } }); + // Set up a buffer white some trailing whitespace and no trailing newline. + cx.set_state( + &[ + "one ", // + "twoˇ", // + "three ", // + "four", // + ] + .join("\n"), + ); + cx.run_until_parked(); + // Submit a format request. let format = cx .update_editor(|editor, window, cx| editor.format(&Format, window, cx)) diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index 836b61d56674f070abc13dbf6c67981c78818ff6..3d83e3a5cce937b92255810003a6ff951bb84d95 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -16,7 +16,7 @@ use markdown::Markdown; use multi_buffer::{MultiBuffer, RowInfo}; use project::{ Project, ProjectItem as _, - git_store::{GitStoreEvent, Repository, RepositoryEvent}, + git_store::{GitStoreEvent, Repository}, }; use smallvec::SmallVec; use std::{sync::Arc, time::Duration}; @@ -235,8 +235,8 @@ impl GitBlame { let git_store = project.read(cx).git_store().clone(); let git_store_subscription = cx.subscribe(&git_store, move |this, _, event, cx| match event { - GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _) - | GitStoreEvent::RepositoryAdded(_) + GitStoreEvent::RepositoryUpdated(_, _, _) + | GitStoreEvent::RepositoryAdded | GitStoreEvent::RepositoryRemoved(_) => { log::debug!("Status of git repositories updated. Regenerating blame data...",); this.generate(cx); diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 2c6db5b53987013d24a3a922e8f3b67adc9d43f5..409edade8a704bc79c00aab5acb3856c66d5b91e 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -11,14 +11,20 @@ use git::{ }, status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task}; +use gpui::{AsyncApp, BackgroundExecutor, SharedString, Task, TaskLabel}; use ignore::gitignore::GitignoreBuilder; use parking_lot::Mutex; use rope::Rope; use smol::future::FutureExt as _; -use std::{path::PathBuf, sync::Arc}; +use std::{ + path::PathBuf, + sync::{Arc, LazyLock}, +}; use util::{paths::PathStyle, rel_path::RelPath}; +pub static LOAD_INDEX_TEXT_TASK: LazyLock = LazyLock::new(TaskLabel::new); +pub static LOAD_HEAD_TEXT_TASK: LazyLock = LazyLock::new(TaskLabel::new); + #[derive(Clone)] pub struct FakeGitRepository { pub(crate) fs: Arc, @@ -79,33 +85,29 @@ impl GitRepository for FakeGitRepository { fn reload_index(&self) {} fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option> { - async { - self.with_state_async(false, move |state| { - state - .index_contents - .get(&path) - .context("not present in index") - .cloned() - }) - .await - .ok() - } - .boxed() + let fut = self.with_state_async(false, move |state| { + state + .index_contents + .get(&path) + .context("not present in index") + .cloned() + }); + self.executor + .spawn_labeled(*LOAD_INDEX_TEXT_TASK, async move { fut.await.ok() }) + .boxed() } fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option> { - async { - self.with_state_async(false, move |state| { - state - .head_contents - .get(&path) - .context("not present in HEAD") - .cloned() - }) - .await - .ok() - } - .boxed() + let fut = self.with_state_async(false, move |state| { + state + .head_contents + .get(&path) + .context("not present in HEAD") + .cloned() + }); + self.executor + .spawn_labeled(*LOAD_HEAD_TEXT_TASK, async move { fut.await.ok() }) + .boxed() } fn load_commit( diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 69ac5fb0d3b4aed9e63166c60ba9550186fb204f..a6c7be1c9388302f04a1a0440d33d3f6322c2077 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -58,6 +58,9 @@ use smol::io::AsyncReadExt; #[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; +#[cfg(any(test, feature = "test-support"))] +pub use fake_git_repo::{LOAD_HEAD_TEXT_TASK, LOAD_INDEX_TEXT_TASK}; + pub trait Watcher: Send + Sync { fn add(&self, path: &Path) -> Result<()>; fn remove(&self, path: &Path) -> Result<()>; diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 2bd0fea7018a99a943efe91becd7c22962b27fb4..9ff8602a18fd1a7eec5804deecee5c21921c6eee 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -425,13 +425,20 @@ impl GitPanel { } GitStoreEvent::RepositoryUpdated( _, - RepositoryEvent::Updated { full_scan, .. }, + RepositoryEvent::StatusesChanged { full_scan: true } + | RepositoryEvent::BranchChanged + | RepositoryEvent::MergeHeadsChanged, true, ) => { - this.schedule_update(*full_scan, window, cx); + this.schedule_update(true, window, cx); } - - GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => { + GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::StatusesChanged { full_scan: false }, + true, + ) + | GitStoreEvent::RepositoryAdded + | GitStoreEvent::RepositoryRemoved(_) => { this.schedule_update(false, window, cx); } GitStoreEvent::IndexWriteError(error) => { diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index b073b9dc3da17c10d9df1fa99c9bec53575818df..de16803965e0d8afb5bebcc37628d8c25ecc05e9 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::Result; use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{ Editor, EditorEvent, SelectionEffects, actions::{GoToHunk, GoToPreviousHunk}, @@ -27,7 +27,7 @@ use language::{Anchor, Buffer, Capability, OffsetRangeExt}; use multi_buffer::{MultiBuffer, PathKey}; use project::{ Project, ProjectPath, - git_store::{GitStore, GitStoreEvent, Repository}, + git_store::{GitStore, GitStoreEvent, Repository, RepositoryEvent}, }; use settings::{Settings, SettingsStore}; use std::any::{Any, TypeId}; @@ -57,12 +57,13 @@ pub struct ProjectDiff { multibuffer: Entity, editor: Entity, git_store: Entity, + buffer_diff_subscriptions: HashMap, Subscription)>, workspace: WeakEntity, focus_handle: FocusHandle, update_needed: postage::watch::Sender<()>, pending_scroll: Option, _task: Task>, - _subscription: Subscription, + _git_store_subscription: Subscription, } #[derive(Debug)] @@ -177,7 +178,11 @@ impl ProjectDiff { window, move |this, _git_store, event, _window, _cx| match event { GitStoreEvent::ActiveRepositoryChanged(_) - | GitStoreEvent::RepositoryUpdated(_, _, true) + | GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::StatusesChanged { full_scan: _ }, + true, + ) | GitStoreEvent::ConflictsUpdated => { *this.update_needed.borrow_mut() = (); } @@ -217,10 +222,11 @@ impl ProjectDiff { focus_handle, editor, multibuffer, + buffer_diff_subscriptions: Default::default(), pending_scroll: None, update_needed: send, _task: worker, - _subscription: git_store_subscription, + _git_store_subscription: git_store_subscription, } } @@ -365,6 +371,7 @@ impl ProjectDiff { self.multibuffer.update(cx, |multibuffer, cx| { multibuffer.clear(cx); }); + self.buffer_diff_subscriptions.clear(); return vec![]; }; @@ -407,6 +414,8 @@ impl ProjectDiff { }); self.multibuffer.update(cx, |multibuffer, cx| { for path in previous_paths { + self.buffer_diff_subscriptions + .remove(&path.path.clone().into()); multibuffer.remove_excerpts_for_path(path, cx); } }); @@ -419,9 +428,15 @@ impl ProjectDiff { window: &mut Window, cx: &mut Context, ) { - let path_key = diff_buffer.path_key; - let buffer = diff_buffer.buffer; - let diff = diff_buffer.diff; + let path_key = diff_buffer.path_key.clone(); + let buffer = diff_buffer.buffer.clone(); + let diff = diff_buffer.diff.clone(); + + let subscription = cx.subscribe(&diff, move |this, _, _, _| { + *this.update_needed.borrow_mut() = (); + }); + self.buffer_diff_subscriptions + .insert(path_key.path.clone().into(), (diff.clone(), subscription)); let conflict_addon = self .editor @@ -440,9 +455,10 @@ impl ProjectDiff { .unwrap_or_default(); let conflicts = conflicts.iter().map(|conflict| conflict.range.clone()); - let excerpt_ranges = merge_anchor_ranges(diff_hunk_ranges, conflicts, &snapshot) - .map(|range| range.to_point(&snapshot)) - .collect::>(); + let excerpt_ranges = + merge_anchor_ranges(diff_hunk_ranges.into_iter(), conflicts, &snapshot) + .map(|range| range.to_point(&snapshot)) + .collect::>(); let (was_empty, is_excerpt_newly_added) = self.multibuffer.update(cx, |multibuffer, cx| { let was_empty = multibuffer.is_empty(); @@ -519,8 +535,7 @@ impl ProjectDiff { self.multibuffer .read(cx) .excerpt_paths() - .map(|key| key.path()) - .cloned() + .map(|key| key.path.clone()) .collect() } } @@ -1621,8 +1636,8 @@ mod tests { cx, &" - original - + different - ˇ" + + ˇdifferent + " .unindent(), ); } @@ -1950,6 +1965,7 @@ mod tests { .unindent(), ); + // The project diff updates its excerpts when a new hunk appears in a buffer that already has a diff. let buffer = project .update(cx, |project, cx| { project.open_local_buffer(path!("/project/foo.txt"), cx) @@ -2002,4 +2018,63 @@ mod tests { .unindent(), ); } + + #[gpui::test] + async fn test_update_on_uncommit(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/project"), + json!({ + ".git": {}, + "README.md": "# My cool project\n".to_owned() + }), + ) + .await; + fs.set_head_and_index_for_repo( + Path::new(path!("/project/.git")), + &[("README.md", "# My cool project\n".to_owned())], + ); + let project = Project::test(fs.clone(), [Path::new(path!("/project"))], cx).await; + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + cx.run_until_parked(); + + let _editor = workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_path((worktree_id, rel_path("README.md")), None, true, window, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + cx.focus(&workspace); + cx.update(|window, cx| { + window.dispatch_action(project_diff::Diff.boxed_clone(), cx); + }); + cx.run_until_parked(); + let item = workspace.update(cx, |workspace, cx| { + workspace.active_item_as::(cx).unwrap() + }); + cx.focus(&item); + let editor = item.read_with(cx, |item, _| item.editor.clone()); + + fs.set_head_and_index_for_repo( + Path::new(path!("/project/.git")), + &[( + "README.md", + "# My cool project\nDetails to come.\n".to_owned(), + )], + ); + cx.run_until_parked(); + + let mut cx = EditorTestContext::for_editor_in(editor, cx).await; + + cx.assert_excerpts_with_selections("[EXCERPT]\nˇ# My cool project\nDetails to come.\n"); + } } diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index a8e725eefcafb2f3742b23adfdd75ab129052773..58f17d7a3bb087ff058878f7889d6d83bc1727a6 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -72,7 +72,7 @@ impl StashList { if let Some(repo) = repository.clone() { _subscriptions.push( cx.subscribe_in(&repo, window, |this, _, event, window, cx| { - if matches!(event, RepositoryEvent::Updated { .. }) { + if matches!(event, RepositoryEvent::StashEntriesChanged) { let stash_entries = this.picker.read_with(cx, |picker, cx| { picker .delegate diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e6e2f7f1c68f976473b5e6ee8b60ca8652aa4b1d..646d7fce825c05204c07f42619d5f9964d5cd321 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -166,8 +166,8 @@ impl MultiBufferDiffHunk { #[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)] pub struct PathKey { // Used by the derived PartialOrd & Ord - sort_prefix: Option, - path: Arc, + pub sort_prefix: Option, + pub path: Arc, } impl PathKey { @@ -190,11 +190,6 @@ impl PathKey { } } } - - #[cfg(any(test, feature = "test-support"))] - pub fn path(&self) -> &Arc { - &self.path - } } pub type MultiBufferPoint = Point; diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 326af767102721987f753012ac82649046543ee0..8612c739a01c1a927dd071c73f789ea1ef4a2542 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -301,9 +301,13 @@ pub enum RepositoryState { #[derive(Clone, Debug, PartialEq, Eq)] pub enum RepositoryEvent { - Updated { full_scan: bool, new_instance: bool }, + StatusesChanged { + // TODO could report which statuses changed here + full_scan: bool, + }, MergeHeadsChanged, - PathsChanged, + BranchChanged, + StashEntriesChanged, } #[derive(Clone, Debug)] @@ -313,7 +317,7 @@ pub struct JobsUpdated; pub enum GitStoreEvent { ActiveRepositoryChanged(Option), RepositoryUpdated(RepositoryId, RepositoryEvent, bool), - RepositoryAdded(RepositoryId), + RepositoryAdded, RepositoryRemoved(RepositoryId), IndexWriteError(anyhow::Error), JobsUpdated, @@ -1218,7 +1222,7 @@ impl GitStore { self._subscriptions .push(cx.subscribe(&repo, Self::on_jobs_updated)); self.repositories.insert(id, repo); - cx.emit(GitStoreEvent::RepositoryAdded(id)); + cx.emit(GitStoreEvent::RepositoryAdded); self.active_repo_id.get_or_insert_with(|| { cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); id @@ -1485,11 +1489,10 @@ impl GitStore { let id = RepositoryId::from_proto(update.id); let client = this.upstream_client().context("no upstream client")?; - let mut is_new = false; + let mut repo_subscription = None; let repo = this.repositories.entry(id).or_insert_with(|| { - is_new = true; let git_store = cx.weak_entity(); - cx.new(|cx| { + let repo = cx.new(|cx| { Repository::remote( id, Path::new(&update.abs_path).into(), @@ -1499,16 +1502,16 @@ impl GitStore { git_store, cx, ) - }) + }); + repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event)); + cx.emit(GitStoreEvent::RepositoryAdded); + repo }); - if is_new { - this._subscriptions - .push(cx.subscribe(repo, Self::on_repository_event)) - } + this._subscriptions.extend(repo_subscription); repo.update(cx, { let update = update.clone(); - |repo, cx| repo.apply_remote_update(update, is_new, cx) + |repo, cx| repo.apply_remote_update(update, cx) })?; this.active_repo_id.get_or_insert_with(|| { @@ -3877,18 +3880,15 @@ impl Repository { environment, .. } => { + // TODO would be nice to not have to do this manually let result = backend.stash_drop(index, environment).await; if result.is_ok() && let Ok(stash_entries) = backend.stash_entries().await { let snapshot = this.update(&mut cx, |this, cx| { this.snapshot.stash_entries = stash_entries; - let snapshot = this.snapshot.clone(); - cx.emit(RepositoryEvent::Updated { - full_scan: false, - new_instance: false, - }); - snapshot + cx.emit(RepositoryEvent::StashEntriesChanged); + this.snapshot.clone() })?; if let Some(updates_tx) = updates_tx { updates_tx @@ -4048,18 +4048,15 @@ impl Repository { cx.clone(), ) .await; + // TODO would be nice to not have to do this manually if result.is_ok() { let branches = backend.branches().await?; let branch = branches.into_iter().find(|branch| branch.is_head); log::info!("head branch after scan is {branch:?}"); let snapshot = this.update(&mut cx, |this, cx| { this.snapshot.branch = branch; - let snapshot = this.snapshot.clone(); - cx.emit(RepositoryEvent::Updated { - full_scan: false, - new_instance: false, - }); - snapshot + cx.emit(RepositoryEvent::BranchChanged); + this.snapshot.clone() })?; if let Some(updates_tx) = updates_tx { updates_tx @@ -4458,7 +4455,6 @@ impl Repository { pub(crate) fn apply_remote_update( &mut self, update: proto::UpdateRepository, - is_new: bool, cx: &mut Context, ) -> Result<()> { let conflicted_paths = TreeSet::from_ordered_entries( @@ -4467,21 +4463,30 @@ impl Repository { .into_iter() .filter_map(|path| RepoPath::from_proto(&path).log_err()), ); - self.snapshot.branch = update.branch_summary.as_ref().map(proto_to_branch); - self.snapshot.head_commit = update + let new_branch = update.branch_summary.as_ref().map(proto_to_branch); + let new_head_commit = update .head_commit_details .as_ref() .map(proto_to_commit_details); + if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit { + cx.emit(RepositoryEvent::BranchChanged) + } + self.snapshot.branch = new_branch; + self.snapshot.head_commit = new_head_commit; self.snapshot.merge.conflicted_paths = conflicted_paths; self.snapshot.merge.message = update.merge_message.map(SharedString::from); - self.snapshot.stash_entries = GitStash { + let new_stash_entries = GitStash { entries: update .stash_entries .iter() .filter_map(|entry| proto_to_stash(entry).ok()) .collect(), }; + if self.snapshot.stash_entries != new_stash_entries { + cx.emit(RepositoryEvent::StashEntriesChanged) + } + self.snapshot.stash_entries = new_stash_entries; let edits = update .removed_statuses @@ -4500,14 +4505,13 @@ impl Repository { }), ) .collect::>(); + if !edits.is_empty() { + cx.emit(RepositoryEvent::StatusesChanged { full_scan: true }); + } self.snapshot.statuses_by_path.edit(edits, ()); if update.is_last_update { self.snapshot.scan_id = update.scan_id; } - cx.emit(RepositoryEvent::Updated { - full_scan: true, - new_instance: is_new, - }); Ok(()) } @@ -4830,23 +4834,19 @@ impl Repository { .await; this.update(&mut cx, |this, cx| { - let needs_update = !changed_path_statuses.is_empty() - || this.snapshot.stash_entries != stash_entries; - this.snapshot.stash_entries = stash_entries; + if this.snapshot.stash_entries != stash_entries { + cx.emit(RepositoryEvent::StashEntriesChanged); + this.snapshot.stash_entries = stash_entries; + } + if !changed_path_statuses.is_empty() { + cx.emit(RepositoryEvent::StatusesChanged { full_scan: false }); this.snapshot .statuses_by_path .edit(changed_path_statuses, ()); this.snapshot.scan_id += 1; } - if needs_update { - cx.emit(RepositoryEvent::Updated { - full_scan: false, - new_instance: false, - }); - } - if let Some(updates_tx) = updates_tx { updates_tx .unbounded_send(DownstreamUpdate::UpdateRepository( @@ -4854,7 +4854,6 @@ impl Repository { )) .ok(); } - cx.emit(RepositoryEvent::PathsChanged); }) }, ); @@ -5117,28 +5116,24 @@ async fn compute_snapshot( MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?; log::debug!("new merge details (changed={merge_heads_changed:?}): {merge_details:?}"); - if merge_heads_changed - || branch != prev_snapshot.branch - || statuses_by_path != prev_snapshot.statuses_by_path - { - events.push(RepositoryEvent::Updated { - full_scan: true, - new_instance: false, - }); - } - - // Cache merge conflict paths so they don't change from staging/unstaging, - // until the merge heads change (at commit time, etc.). if merge_heads_changed { events.push(RepositoryEvent::MergeHeadsChanged); } + if statuses_by_path != prev_snapshot.statuses_by_path { + events.push(RepositoryEvent::StatusesChanged { full_scan: true }) + } + // Useful when branch is None in detached head state let head_commit = match backend.head_sha().await { Some(head_sha) => backend.show(head_sha).await.log_err(), None => None, }; + if branch != prev_snapshot.branch || head_commit != prev_snapshot.head_commit { + events.push(RepositoryEvent::BranchChanged); + } + // Used by edit prediction data collection let remote_origin_url = backend.remote_url("origin"); let remote_upstream_url = backend.remote_url("upstream"); diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 89a49c6fb0185d36cf2dab3f07cfc6efedd1b6d1..859fe02cfa70d035a347c62ba7cbe93f250b674a 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -8936,10 +8936,7 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { assert_eq!( repository_updates.lock().drain(..).collect::>(), vec![ - RepositoryEvent::Updated { - full_scan: true, - new_instance: false, - }, + RepositoryEvent::StatusesChanged { full_scan: true }, RepositoryEvent::MergeHeadsChanged, ], "Initial worktree scan should produce a repo update event" @@ -9000,7 +8997,6 @@ async fn test_ignored_dirs_events(cx: &mut gpui::TestAppContext) { repository_updates .lock() .iter() - .filter(|update| !matches!(update, RepositoryEvent::PathsChanged)) .cloned() .collect::>(), Vec::new(), @@ -9104,17 +9100,10 @@ async fn test_odd_events_for_ignored_dirs( }); assert_eq!( - repository_updates - .lock() - .drain(..) - .filter(|update| !matches!(update, RepositoryEvent::PathsChanged)) - .collect::>(), + repository_updates.lock().drain(..).collect::>(), vec![ - RepositoryEvent::Updated { - full_scan: true, - new_instance: false, - }, RepositoryEvent::MergeHeadsChanged, + RepositoryEvent::BranchChanged ], "Initial worktree scan should produce a repo update event" ); @@ -9142,7 +9131,6 @@ async fn test_odd_events_for_ignored_dirs( repository_updates .lock() .iter() - .filter(|update| !matches!(update, RepositoryEvent::PathsChanged)) .cloned() .collect::>(), Vec::new(), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index ff5a6b661e792ad7c9188fa99b288827efe55c48..8794b625e2b63384041264d67b7d8bf729707735 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -496,8 +496,12 @@ impl ProjectPanel { &git_store, window, |this, _, event, window, cx| match event { - GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _) - | GitStoreEvent::RepositoryAdded(_) + GitStoreEvent::RepositoryUpdated( + _, + RepositoryEvent::StatusesChanged { full_scan: _ }, + _, + ) + | GitStoreEvent::RepositoryAdded | GitStoreEvent::RepositoryRemoved(_) => { this.update_visible_entries(None, false, false, window, cx); cx.notify(); diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 3f3b009a19fa15a9e9b9c2abe09a66e90eceafb2..18a4592edb153dd204bf8df72b1d37fbc81567d5 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -30,10 +30,7 @@ use gpui::{ Subscription, WeakEntity, Window, actions, div, }; use onboarding_banner::OnboardingBanner; -use project::{ - Project, WorktreeSettings, - git_store::{GitStoreEvent, RepositoryEvent}, -}; +use project::{Project, WorktreeSettings, git_store::GitStoreEvent}; use remote::RemoteConnectionOptions; use settings::{Settings, SettingsLocation}; use std::sync::Arc; @@ -287,9 +284,7 @@ impl TitleBar { subscriptions.push( cx.subscribe(&git_store, move |_, _, event, cx| match event { GitStoreEvent::ActiveRepositoryChanged(_) - | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::Updated { .. }, _) - | GitStoreEvent::RepositoryAdded(_) - | GitStoreEvent::RepositoryRemoved(_) => { + | GitStoreEvent::RepositoryUpdated(_, _, true) => { cx.notify(); } _ => {} From 63fe1eae59fa81daeb43b38192ed3e0de4e04e15 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 23 Oct 2025 13:03:55 -0400 Subject: [PATCH 198/202] Fix overly noisy direnv error notification (#41029) Updates #40531, restoring the previous behavior which didn't surface an error when no direnv binary was found. Release Notes: - N/A --- crates/project/src/environment.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs index 4dd1e94b9fce6bcf99e49402b8e5d05ece041f40..c04f1350aa7069267f9d8067e6ec765c4514f535 100644 --- a/crates/project/src/environment.rs +++ b/crates/project/src/environment.rs @@ -324,7 +324,9 @@ async fn load_direnv_environment( env: &HashMap, dir: &Path, ) -> anyhow::Result>> { - let direnv_path = which::which("direnv").context("finding direnv binary")?; + let Some(direnv_path) = which::which("direnv").ok() else { + return Ok(HashMap::default()); + }; let args = &["export", "json"]; let direnv_output = smol::process::Command::new(&direnv_path) From 11eba64e685eb7bbafa9c804821305dcac52e916 Mon Sep 17 00:00:00 2001 From: Bennet Fenner Date: Thu, 23 Oct 2025 19:17:41 +0200 Subject: [PATCH 199/202] Rename `assistant_context` crate to `assistant_text_thread` (#41024) Previously we had `Context` and `ContextStore` in both `agent_ui` (used to store context for the inline assistant) and `assistant_context` (used for text threads) which is confusing. This PR makes it so that the `assistant_context` concepts are now called `TextThread*`, the crate was renamed to `assistant_text_thread` Release Notes: - N/A --- Cargo.lock | 94 ++-- Cargo.toml | 4 +- crates/agent/Cargo.toml | 4 +- crates/agent/src/agent.rs | 20 +- crates/agent/src/history_store.rs | 42 +- crates/agent/src/native_agent_server.rs | 7 +- crates/agent/src/tests/mod.rs | 5 +- crates/agent_ui/Cargo.toml | 4 +- crates/agent_ui/src/acp/entry_view_state.rs | 6 +- crates/agent_ui/src/acp/message_editor.rs | 28 +- crates/agent_ui/src/acp/thread_history.rs | 8 +- crates/agent_ui/src/acp/thread_view.rs | 22 +- crates/agent_ui/src/agent_panel.rs | 154 +++--- crates/agent_ui/src/agent_ui.rs | 2 +- crates/agent_ui/src/context.rs | 12 +- crates/agent_ui/src/context_store.rs | 37 +- crates/agent_ui/src/context_strip.rs | 16 +- crates/agent_ui/src/inline_assistant.rs | 8 +- crates/agent_ui/src/slash_command_picker.rs | 4 +- crates/agent_ui/src/text_thread_editor.rs | 352 +++++++------- crates/agent_ui/src/ui/context_pill.rs | 4 +- .../Cargo.toml | 4 +- .../LICENSE-GPL | 0 .../src/assistant_text_thread.rs | 15 + .../src/assistant_text_thread_tests.rs} | 447 ++++++++++-------- .../src/text_thread.rs} | 324 ++++++------- .../src/text_thread_store.rs} | 343 +++++++------- crates/collab/Cargo.toml | 2 +- crates/collab/src/tests/integration_tests.rs | 52 +- crates/collab/src/tests/test_server.rs | 2 +- crates/paths/src/paths.rs | 2 +- 31 files changed, 1032 insertions(+), 992 deletions(-) rename crates/{assistant_context => assistant_text_thread}/Cargo.toml (95%) rename crates/{assistant_context => assistant_text_thread}/LICENSE-GPL (100%) create mode 100644 crates/assistant_text_thread/src/assistant_text_thread.rs rename crates/{assistant_context/src/assistant_context_tests.rs => assistant_text_thread/src/assistant_text_thread_tests.rs} (75%) rename crates/{assistant_context/src/assistant_context.rs => assistant_text_thread/src/text_thread.rs} (92%) rename crates/{assistant_context/src/context_store.rs => assistant_text_thread/src/text_thread_store.rs} (71%) diff --git a/Cargo.lock b/Cargo.lock index 0b24221bb6594478b70e50be0c03e2456b97e402..d33d31d9fdc5ab0e7819cbaf1d15c0a149d56627 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,7 +142,7 @@ dependencies = [ "agent_servers", "agent_settings", "anyhow", - "assistant_context", + "assistant_text_thread", "chrono", "client", "clock", @@ -315,9 +315,9 @@ dependencies = [ "ai_onboarding", "anyhow", "arrayvec", - "assistant_context", "assistant_slash_command", "assistant_slash_commands", + "assistant_text_thread", "audio", "buffer_diff", "chrono", @@ -803,107 +803,107 @@ dependencies = [ ] [[package]] -name = "assistant_context" +name = "assistant_slash_command" version = "0.1.0" dependencies = [ - "agent_settings", "anyhow", - "assistant_slash_command", - "assistant_slash_commands", - "chrono", - "client", - "clock", - "cloud_llm_client", + "async-trait", "collections", - "context_server", - "fs", + "derive_more 0.99.20", + "extension", "futures 0.3.31", - "fuzzy", "gpui", - "indoc", "language", "language_model", - "log", - "open_ai", "parking_lot", - "paths", "pretty_assertions", - "project", - "prompt_store", - "proto", - "rand 0.9.2", - "regex", - "rpc", "serde", "serde_json", - "settings", - "smallvec", - "smol", - "telemetry_events", - "text", "ui", - "unindent", "util", - "uuid", "workspace", - "zed_env_vars", ] [[package]] -name = "assistant_slash_command" +name = "assistant_slash_commands" version = "0.1.0" dependencies = [ "anyhow", - "async-trait", + "assistant_slash_command", + "chrono", "collections", - "derive_more 0.99.20", - "extension", + "context_server", + "editor", + "feature_flags", + "fs", "futures 0.3.31", + "fuzzy", + "globset", "gpui", + "html_to_markdown", + "http_client", "language", - "language_model", - "parking_lot", "pretty_assertions", + "project", + "prompt_store", + "rope", "serde", "serde_json", + "settings", + "smol", + "text", "ui", "util", "workspace", + "worktree", + "zlog", ] [[package]] -name = "assistant_slash_commands" +name = "assistant_text_thread" version = "0.1.0" dependencies = [ + "agent_settings", "anyhow", "assistant_slash_command", + "assistant_slash_commands", "chrono", + "client", + "clock", + "cloud_llm_client", "collections", "context_server", - "editor", - "feature_flags", "fs", "futures 0.3.31", "fuzzy", - "globset", "gpui", - "html_to_markdown", - "http_client", + "indoc", "language", + "language_model", + "log", + "open_ai", + "parking_lot", + "paths", "pretty_assertions", "project", "prompt_store", - "rope", + "proto", + "rand 0.9.2", + "regex", + "rpc", "serde", "serde_json", "settings", + "smallvec", "smol", + "telemetry_events", "text", "ui", + "unindent", "util", + "uuid", "workspace", - "worktree", - "zlog", + "zed_env_vars", ] [[package]] @@ -3324,8 +3324,8 @@ version = "0.44.0" dependencies = [ "agent_settings", "anyhow", - "assistant_context", "assistant_slash_command", + "assistant_text_thread", "async-trait", "async-tungstenite", "audio", diff --git a/Cargo.toml b/Cargo.toml index c0c0ffc1508aaa51465db7a30cccfcfa04fd8467..e0682924bc377d40a0711630c77ad4dd000515b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = [ "crates/anthropic", "crates/askpass", "crates/assets", - "crates/assistant_context", + "crates/assistant_text_thread", "crates/assistant_slash_command", "crates/assistant_slash_commands", "crates/audio", @@ -246,7 +246,7 @@ ai_onboarding = { path = "crates/ai_onboarding" } anthropic = { path = "crates/anthropic" } askpass = { path = "crates/askpass" } assets = { path = "crates/assets" } -assistant_context = { path = "crates/assistant_context" } +assistant_text_thread = { path = "crates/assistant_text_thread" } assistant_slash_command = { path = "crates/assistant_slash_command" } assistant_slash_commands = { path = "crates/assistant_slash_commands" } audio = { path = "crates/audio" } diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 9e5b6ad66096b784bfb496b71ef1ee5cb30005cb..e0f2d9dcb97e298dd3c906e3f902974821efcdc0 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -24,7 +24,7 @@ agent-client-protocol.workspace = true agent_servers.workspace = true agent_settings.workspace = true anyhow.workspace = true -assistant_context.workspace = true +assistant_text_thread.workspace = true chrono.workspace = true client.workspace = true cloud_llm_client.workspace = true @@ -76,7 +76,7 @@ zstd.workspace = true [dev-dependencies] agent_servers = { workspace = true, "features" = ["test-support"] } -assistant_context = { workspace = true, "features" = ["test-support"] } +assistant_text_thread = { workspace = true, "features" = ["test-support"] } client = { workspace = true, "features" = ["test-support"] } clock = { workspace = true, "features" = ["test-support"] } context_server = { workspace = true, "features" = ["test-support"] } diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 65eb25e6ac9d005fc2e18901a56287e2938e5bb8..63ee0adf191cbe309229c57b950d11ca7a3680e3 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1266,8 +1266,9 @@ mod internal_tests { ) .await; let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = + cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let agent = NativeAgent::new( project.clone(), history_store, @@ -1327,8 +1328,9 @@ mod internal_tests { let fs = FakeFs::new(cx.executor()); fs.insert_tree("/", json!({ "a": {} })).await; let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = + cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let connection = NativeAgentConnection( NativeAgent::new( project.clone(), @@ -1402,8 +1404,9 @@ mod internal_tests { .await; let project = Project::test(fs.clone(), [], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = + cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); // Create the agent and connection let agent = NativeAgent::new( @@ -1474,8 +1477,9 @@ mod internal_tests { ) .await; let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = + cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let agent = NativeAgent::new( project.clone(), history_store.clone(), diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs index c342110f3ee289b6e84241517b69fe9a86efcf16..3bfbd99677feed5db53d96d2fa96316ac49abce4 100644 --- a/crates/agent/src/history_store.rs +++ b/crates/agent/src/history_store.rs @@ -2,12 +2,12 @@ use crate::{DbThread, DbThreadMetadata, ThreadsDatabase}; use acp_thread::MentionUri; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; -use assistant_context::{AssistantContext, SavedContextMetadata}; +use assistant_text_thread::{SavedTextThreadMetadata, TextThread}; use chrono::{DateTime, Utc}; use db::kvp::KEY_VALUE_STORE; use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; use itertools::Itertools; -use paths::contexts_dir; +use paths::text_threads_dir; use project::Project; use serde::{Deserialize, Serialize}; use std::{collections::VecDeque, path::Path, rc::Rc, sync::Arc, time::Duration}; @@ -50,21 +50,23 @@ pub fn load_agent_thread( #[derive(Clone, Debug)] pub enum HistoryEntry { AcpThread(DbThreadMetadata), - TextThread(SavedContextMetadata), + TextThread(SavedTextThreadMetadata), } impl HistoryEntry { pub fn updated_at(&self) -> DateTime { match self { HistoryEntry::AcpThread(thread) => thread.updated_at, - HistoryEntry::TextThread(context) => context.mtime.to_utc(), + HistoryEntry::TextThread(text_thread) => text_thread.mtime.to_utc(), } } pub fn id(&self) -> HistoryEntryId { match self { HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()), - HistoryEntry::TextThread(context) => HistoryEntryId::TextThread(context.path.clone()), + HistoryEntry::TextThread(text_thread) => { + HistoryEntryId::TextThread(text_thread.path.clone()) + } } } @@ -74,9 +76,9 @@ impl HistoryEntry { id: thread.id.clone(), name: thread.title.to_string(), }, - HistoryEntry::TextThread(context) => MentionUri::TextThread { - path: context.path.as_ref().to_owned(), - name: context.title.to_string(), + HistoryEntry::TextThread(text_thread) => MentionUri::TextThread { + path: text_thread.path.as_ref().to_owned(), + name: text_thread.title.to_string(), }, } } @@ -90,7 +92,7 @@ impl HistoryEntry { &thread.title } } - HistoryEntry::TextThread(context) => &context.title, + HistoryEntry::TextThread(text_thread) => &text_thread.title, } } } @@ -120,7 +122,7 @@ enum SerializedRecentOpen { pub struct HistoryStore { threads: Vec, entries: Vec, - text_thread_store: Entity, + text_thread_store: Entity, recently_opened_entries: VecDeque, _subscriptions: Vec, _save_recently_opened_entries_task: Task<()>, @@ -128,7 +130,7 @@ pub struct HistoryStore { impl HistoryStore { pub fn new( - text_thread_store: Entity, + text_thread_store: Entity, cx: &mut Context, ) -> Self { let subscriptions = @@ -192,16 +194,16 @@ impl HistoryStore { cx: &mut Context, ) -> Task> { self.text_thread_store - .update(cx, |store, cx| store.delete_local_context(path, cx)) + .update(cx, |store, cx| store.delete_local(path, cx)) } pub fn load_text_thread( &self, path: Arc, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { self.text_thread_store - .update(cx, |store, cx| store.open_local_context(path, cx)) + .update(cx, |store, cx| store.open_local(path, cx)) } pub fn reload(&self, cx: &mut Context) { @@ -243,7 +245,7 @@ impl HistoryStore { history_entries.extend( self.text_thread_store .read(cx) - .unordered_contexts() + .unordered_text_threads() .cloned() .map(HistoryEntry::TextThread), ); @@ -278,14 +280,14 @@ impl HistoryStore { let context_entries = self .text_thread_store .read(cx) - .unordered_contexts() - .flat_map(|context| { + .unordered_text_threads() + .flat_map(|text_thread| { self.recently_opened_entries .iter() .enumerate() .flat_map(|(index, entry)| match entry { - HistoryEntryId::TextThread(path) if &context.path == path => { - Some((index, HistoryEntry::TextThread(context.clone()))) + HistoryEntryId::TextThread(path) if &text_thread.path == path => { + Some((index, HistoryEntry::TextThread(text_thread.clone()))) } _ => None, }) @@ -347,7 +349,7 @@ impl HistoryStore { acp::SessionId(id.as_str().into()), )), SerializedRecentOpen::TextThread(file_name) => Some( - HistoryEntryId::TextThread(contexts_dir().join(file_name).into()), + HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()), ), }) .collect(); diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs index 0dde0ff98552d4292a4391d2aec4f36419228a25..b28009223b7a7f2232b440282a0d6f61907f442c 100644 --- a/crates/agent/src/native_agent_server.rs +++ b/crates/agent/src/native_agent_server.rs @@ -81,7 +81,7 @@ impl AgentServer for NativeAgentServer { mod tests { use super::*; - use assistant_context::ContextStore; + use assistant_text_thread::TextThreadStore; use gpui::AppContext; agent_servers::e2e_tests::common_e2e_tests!( @@ -116,8 +116,9 @@ mod tests { }); let history = cx.update(|cx| { - let context_store = cx.new(move |cx| ContextStore::fake(project.clone(), cx)); - cx.new(move |cx| HistoryStore::new(context_store, cx)) + let text_thread_store = + cx.new(move |cx| TextThreadStore::fake(project.clone(), cx)); + cx.new(move |cx| HistoryStore::new(text_thread_store, cx)) }); NativeAgentServer::new(fs.clone(), history) diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs index 66b006893e50b9c59701eff850adb7747f96e3b5..ddddbfc5279ca23fb95527892e929b23b8cefbf6 100644 --- a/crates/agent/src/tests/mod.rs +++ b/crates/agent/src/tests/mod.rs @@ -1834,8 +1834,9 @@ async fn test_agent_connection(cx: &mut TestAppContext) { fake_fs.insert_tree(path!("/test"), json!({})).await; let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; let cwd = Path::new("/test"); - let context_store = cx.new(|cx| assistant_context::ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = + cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); // Create agent and connection let agent = NativeAgent::new( diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index f763d6f91e45d1e8b5a035c22fbb7ab65de93dd9..724b53a017911edbd6e9dd88c410daf794889d4e 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -25,7 +25,7 @@ agent_settings.workspace = true ai_onboarding.workspace = true anyhow.workspace = true arrayvec.workspace = true -assistant_context.workspace = true +assistant_text_thread.workspace = true assistant_slash_command.workspace = true assistant_slash_commands.workspace = true audio.workspace = true @@ -102,7 +102,7 @@ zed_actions.workspace = true [dev-dependencies] acp_thread = { workspace = true, features = ["test-support"] } agent = { workspace = true, features = ["test-support"] } -assistant_context = { workspace = true, features = ["test-support"] } +assistant_text_thread = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] } db = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index 8123c4a422b9d95a2da45e75ceb4079675d845fd..4c058b984f4fa24074ea9e9d81e43c1d73d87d1f 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -402,7 +402,7 @@ mod tests { use agent::HistoryStore; use agent_client_protocol as acp; use agent_settings::AgentSettings; - use assistant_context::ContextStore; + use assistant_text_thread::TextThreadStore; use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; use editor::{EditorSettings, RowInfo}; use fs::FakeFs; @@ -466,8 +466,8 @@ mod tests { connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) }); - let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let view_state = cx.new(|_cx| { EntryViewState::new( diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 53a26d9fabdd59e93efbc615ce5be5d1c2d492fb..c24cefcf2d5fc04baffeb9f3d1a1ecaf9dd05268 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -629,12 +629,12 @@ impl MessageEditor { path: PathBuf, cx: &mut Context, ) -> Task> { - let context = self.history_store.update(cx, |store, cx| { + let text_thread_task = self.history_store.update(cx, |store, cx| { store.load_text_thread(path.as_path().into(), cx) }); cx.spawn(async move |_, cx| { - let context = context.await?; - let xml = context.update(cx, |context, cx| context.to_xml(cx))?; + let text_thread = text_thread_task.await?; + let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?; Ok(Mention::Text { content: xml, tracked_buffers: Vec::new(), @@ -1591,7 +1591,7 @@ mod tests { use acp_thread::MentionUri; use agent::{HistoryStore, outline}; use agent_client_protocol as acp; - use assistant_context::ContextStore; + use assistant_text_thread::TextThreadStore; use editor::{AnchorRangeExt as _, Editor, EditorMode}; use fs::FakeFs; use futures::StreamExt as _; @@ -1622,8 +1622,8 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let message_editor = cx.update(|window, cx| { cx.new(|cx| { @@ -1727,8 +1727,8 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); // Start with no available commands - simulating Claude which doesn't support slash commands let available_commands = Rc::new(RefCell::new(vec![])); @@ -1891,8 +1891,8 @@ mod tests { let mut cx = VisualTestContext::from_window(*window, cx); - let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); let available_commands = Rc::new(RefCell::new(vec![ acp::AvailableCommand { @@ -2131,8 +2131,8 @@ mod tests { opened_editors.push(buffer); } - let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { @@ -2658,8 +2658,8 @@ mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let context_store = cx.new(|cx| ContextStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(context_store, cx)); + let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); + let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); let message_editor = cx.update(|window, cx| { cx.new(|cx| { diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index aacae785a1f6ba727089c053588e6f0bc2ae24a2..d96c3b3219717b3ffa7310d207a323bc5fb222b0 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -324,8 +324,8 @@ impl AcpThreadHistory { HistoryEntry::AcpThread(thread) => self .history_store .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), - HistoryEntry::TextThread(context) => self.history_store.update(cx, |this, cx| { - this.delete_text_thread(context.path.clone(), cx) + HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| { + this.delete_text_thread(text_thread.path.clone(), cx) }), }; task.detach_and_log_err(cx); @@ -635,12 +635,12 @@ impl RenderOnce for AcpHistoryEntryElement { }); } } - HistoryEntry::TextThread(context) => { + HistoryEntry::TextThread(text_thread) => { if let Some(panel) = workspace.read(cx).panel::(cx) { panel.update(cx, |panel, cx| { panel .open_saved_text_thread( - context.path.clone(), + text_thread.path.clone(), window, cx, ) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index adf279c82036e8f8219c5647f016ec4fc887a046..8e5396590fe0170b536075bff210c859435a4b3c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -5414,9 +5414,11 @@ impl AcpThreadView { HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { history.delete_thread(thread.id.clone(), cx) }), - HistoryEntry::TextThread(context) => self.history_store.update(cx, |history, cx| { - history.delete_text_thread(context.path.clone(), cx) - }), + HistoryEntry::TextThread(text_thread) => { + self.history_store.update(cx, |history, cx| { + history.delete_text_thread(text_thread.path.clone(), cx) + }) + } }; task.detach_and_log_err(cx); } @@ -5735,7 +5737,7 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { pub(crate) mod tests { use acp_thread::StubAgentConnection; use agent_client_protocol::SessionId; - use assistant_context::ContextStore; + use assistant_text_thread::TextThreadStore; use editor::EditorSettings; use fs::FakeFs; use gpui::{EventEmitter, SemanticVersion, TestAppContext, VisualTestContext}; @@ -5898,10 +5900,10 @@ pub(crate) mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let context_store = - cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); + let text_thread_store = + cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); + cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_store, cx))); let thread_view = cx.update(|window, cx| { cx.new(|cx| { @@ -6170,10 +6172,10 @@ pub(crate) mod tests { let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let context_store = - cx.update(|_window, cx| cx.new(|cx| ContextStore::fake(project.clone(), cx))); + let text_thread_store = + cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(context_store, cx))); + cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_store, cx))); let connection = Rc::new(StubAgentConnection::new()); let thread_view = cx.update(|window, cx| { diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 19f56b26b5b9621b92c307690baefd332da183b0..deb202832469eaa16b3eab3bced0236dc5467c53 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -36,8 +36,8 @@ use crate::{ use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; -use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; +use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary}; use client::{UserStore, zed_urls}; use cloud_llm_client::{Plan, PlanV1, PlanV2, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; @@ -199,7 +199,7 @@ enum ActiveView { thread_view: Entity, }, TextThread { - context_editor: Entity, + text_thread_editor: Entity, title_editor: Entity, buffer_search_bar: Entity, _subscriptions: Vec, @@ -301,13 +301,13 @@ impl ActiveView { } pub fn text_thread( - context_editor: Entity, + text_thread_editor: Entity, acp_history_store: Entity, language_registry: Arc, window: &mut Window, cx: &mut App, ) -> Self { - let title = context_editor.read(cx).title(cx).to_string(); + let title = text_thread_editor.read(cx).title(cx).to_string(); let editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); @@ -323,7 +323,7 @@ impl ActiveView { let subscriptions = vec![ window.subscribe(&editor, cx, { { - let context_editor = context_editor.clone(); + let text_thread_editor = text_thread_editor.clone(); move |editor, event, window, cx| match event { EditorEvent::BufferEdited => { if suppress_first_edit { @@ -332,19 +332,19 @@ impl ActiveView { } let new_summary = editor.read(cx).text(cx); - context_editor.update(cx, |context_editor, cx| { - context_editor - .context() - .update(cx, |assistant_context, cx| { - assistant_context.set_custom_summary(new_summary, cx); + text_thread_editor.update(cx, |text_thread_editor, cx| { + text_thread_editor + .text_thread() + .update(cx, |text_thread, cx| { + text_thread.set_custom_summary(new_summary, cx); }) }) } EditorEvent::Blurred => { if editor.read(cx).text(cx).is_empty() { - let summary = context_editor + let summary = text_thread_editor .read(cx) - .context() + .text_thread() .read(cx) .summary() .or_default(); @@ -358,17 +358,17 @@ impl ActiveView { } } }), - window.subscribe(&context_editor.read(cx).context().clone(), cx, { + window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, { let editor = editor.clone(); - move |assistant_context, event, window, cx| match event { - ContextEvent::SummaryGenerated => { - let summary = assistant_context.read(cx).summary().or_default(); + move |text_thread, event, window, cx| match event { + TextThreadEvent::SummaryGenerated => { + let summary = text_thread.read(cx).summary().or_default(); editor.update(cx, |editor, cx| { editor.set_text(summary, window, cx); }) } - ContextEvent::PathChanged { old_path, new_path } => { + TextThreadEvent::PathChanged { old_path, new_path } => { acp_history_store.update(cx, |history_store, cx| { if let Some(old_path) = old_path { history_store @@ -389,11 +389,11 @@ impl ActiveView { let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx)); buffer_search_bar.update(cx, |buffer_search_bar, cx| { - buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx) + buffer_search_bar.set_active_pane_item(Some(&text_thread_editor), window, cx) }); Self::TextThread { - context_editor, + text_thread_editor, title_editor: editor, buffer_search_bar, _subscriptions: subscriptions, @@ -410,7 +410,7 @@ pub struct AgentPanel { language_registry: Arc, acp_history: Entity, history_store: Entity, - text_thread_store: Entity, + text_thread_store: Entity, prompt_store: Option>, context_server_registry: Entity, inline_assist_context_store: Entity, @@ -474,7 +474,7 @@ impl AgentPanel { let text_thread_store = workspace .update(cx, |workspace, cx| { let project = workspace.project().clone(); - assistant_context::ContextStore::new( + assistant_text_thread::TextThreadStore::new( project, prompt_builder, slash_commands, @@ -512,7 +512,7 @@ impl AgentPanel { fn new( workspace: &Workspace, - text_thread_store: Entity, + text_thread_store: Entity, prompt_store: Option>, window: &mut Window, cx: &mut Context, @@ -565,8 +565,8 @@ impl AgentPanel { DefaultView::TextThread => { let context = text_thread_store.update(cx, |store, cx| store.create(cx)); let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap(); - let context_editor = cx.new(|cx| { - let mut editor = TextThreadEditor::for_context( + let text_thread_editor = cx.new(|cx| { + let mut editor = TextThreadEditor::for_text_thread( context, fs.clone(), workspace.clone(), @@ -579,7 +579,7 @@ impl AgentPanel { editor }); ActiveView::text_thread( - context_editor, + text_thread_editor, history_store.clone(), language_registry.clone(), window, @@ -736,8 +736,8 @@ impl AgentPanel { .log_err() .flatten(); - let context_editor = cx.new(|cx| { - let mut editor = TextThreadEditor::for_context( + let text_thread_editor = cx.new(|cx| { + let mut editor = TextThreadEditor::for_text_thread( context, self.fs.clone(), self.workspace.clone(), @@ -757,7 +757,7 @@ impl AgentPanel { self.set_active_view( ActiveView::text_thread( - context_editor.clone(), + text_thread_editor.clone(), self.history_store.clone(), self.language_registry.clone(), window, @@ -766,7 +766,7 @@ impl AgentPanel { window, cx, ); - context_editor.focus_handle(cx).focus(window); + text_thread_editor.focus_handle(cx).focus(window); } fn external_thread( @@ -905,20 +905,20 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) -> Task> { - let context = self + let text_thread_task = self .history_store .update(cx, |store, cx| store.load_text_thread(path, cx)); cx.spawn_in(window, async move |this, cx| { - let context = context.await?; + let text_thread = text_thread_task.await?; this.update_in(cx, |this, window, cx| { - this.open_text_thread(context, window, cx); + this.open_text_thread(text_thread, window, cx); }) }) } pub(crate) fn open_text_thread( &mut self, - context: Entity, + text_thread: Entity, window: &mut Window, cx: &mut Context, ) { @@ -926,8 +926,8 @@ impl AgentPanel { .log_err() .flatten(); let editor = cx.new(|cx| { - TextThreadEditor::for_context( - context, + TextThreadEditor::for_text_thread( + text_thread, self.fs.clone(), self.workspace.clone(), self.project.clone(), @@ -965,8 +965,10 @@ impl AgentPanel { ActiveView::ExternalAgentThread { thread_view } => { thread_view.focus_handle(cx).focus(window); } - ActiveView::TextThread { context_editor, .. } => { - context_editor.focus_handle(cx).focus(window); + ActiveView::TextThread { + text_thread_editor, .. + } => { + text_thread_editor.focus_handle(cx).focus(window); } ActiveView::History | ActiveView::Configuration => {} } @@ -1183,9 +1185,11 @@ impl AgentPanel { } } - pub(crate) fn active_context_editor(&self) -> Option> { + pub(crate) fn active_text_thread_editor(&self) -> Option> { match &self.active_view { - ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()), + ActiveView::TextThread { + text_thread_editor, .. + } => Some(text_thread_editor.clone()), _ => None, } } @@ -1206,16 +1210,16 @@ impl AgentPanel { let new_is_special = new_is_history || new_is_config; match &new_view { - ActiveView::TextThread { context_editor, .. } => { - self.history_store.update(cx, |store, cx| { - if let Some(path) = context_editor.read(cx).context().read(cx).path() { - store.push_recently_opened_entry( - agent::HistoryEntryId::TextThread(path.clone()), - cx, - ) - } - }) - } + ActiveView::TextThread { + text_thread_editor, .. + } => self.history_store.update(cx, |store, cx| { + if let Some(path) = text_thread_editor.read(cx).text_thread().read(cx).path() { + store.push_recently_opened_entry( + agent::HistoryEntryId::TextThread(path.clone()), + cx, + ) + } + }), ActiveView::ExternalAgentThread { .. } => {} ActiveView::History | ActiveView::Configuration => {} } @@ -1372,7 +1376,9 @@ impl Focusable for AgentPanel { match &self.active_view { ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), ActiveView::History => self.acp_history.focus_handle(cx), - ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), + ActiveView::TextThread { + text_thread_editor, .. + } => text_thread_editor.focus_handle(cx), ActiveView::Configuration => { if let Some(configuration) = self.configuration.as_ref() { configuration.focus_handle(cx) @@ -1507,17 +1513,17 @@ impl AgentPanel { } ActiveView::TextThread { title_editor, - context_editor, + text_thread_editor, .. } => { - let summary = context_editor.read(cx).context().read(cx).summary(); + let summary = text_thread_editor.read(cx).text_thread().read(cx).summary(); match summary { - ContextSummary::Pending => Label::new(ContextSummary::DEFAULT) + TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT) .color(Color::Muted) .truncate() .into_any_element(), - ContextSummary::Content(summary) => { + TextThreadSummary::Content(summary) => { if summary.done { div() .w_full() @@ -1530,17 +1536,17 @@ impl AgentPanel { .into_any_element() } } - ContextSummary::Error => h_flex() + TextThreadSummary::Error => h_flex() .w_full() .child(title_editor.clone()) .child( IconButton::new("retry-summary-generation", IconName::RotateCcw) .icon_size(IconSize::Small) .on_click({ - let context_editor = context_editor.clone(); + let text_thread_editor = text_thread_editor.clone(); move |_, _window, cx| { - context_editor.update(cx, |context_editor, cx| { - context_editor.regenerate_summary(cx); + text_thread_editor.update(cx, |text_thread_editor, cx| { + text_thread_editor.regenerate_summary(cx); }); } }) @@ -2243,7 +2249,7 @@ impl AgentPanel { fn render_text_thread( &self, - context_editor: &Entity, + text_thread_editor: &Entity, buffer_search_bar: &Entity, window: &mut Window, cx: &mut Context, @@ -2277,7 +2283,7 @@ impl AgentPanel { ) }) }) - .child(context_editor.clone()) + .child(text_thread_editor.clone()) .child(self.render_drag_target(cx)) } @@ -2353,10 +2359,12 @@ impl AgentPanel { thread_view.insert_dragged_files(paths, added_worktrees, window, cx); }); } - ActiveView::TextThread { context_editor, .. } => { - context_editor.update(cx, |context_editor, cx| { + ActiveView::TextThread { + text_thread_editor, .. + } => { + text_thread_editor.update(cx, |text_thread_editor, cx| { TextThreadEditor::insert_dragged_files( - context_editor, + text_thread_editor, paths, added_worktrees, window, @@ -2427,7 +2435,7 @@ impl Render for AgentPanel { .child(self.render_drag_target(cx)), ActiveView::History => parent.child(self.acp_history.clone()), ActiveView::TextThread { - context_editor, + text_thread_editor, buffer_search_bar, .. } => { @@ -2450,7 +2458,7 @@ impl Render for AgentPanel { } }) .child(self.render_text_thread( - context_editor, + text_thread_editor, buffer_search_bar, window, cx, @@ -2528,17 +2536,17 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist { pub struct ConcreteAssistantPanelDelegate; impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { - fn active_context_editor( + fn active_text_thread_editor( &self, workspace: &mut Workspace, _window: &mut Window, cx: &mut Context, ) -> Option> { let panel = workspace.panel::(cx)?; - panel.read(cx).active_context_editor() + panel.read(cx).active_text_thread_editor() } - fn open_saved_context( + fn open_local_text_thread( &self, workspace: &mut Workspace, path: Arc, @@ -2554,10 +2562,10 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { }) } - fn open_remote_context( + fn open_remote_text_thread( &self, _workspace: &mut Workspace, - _context_id: assistant_context::ContextId, + _text_thread_id: assistant_text_thread::TextThreadId, _window: &mut Window, _cx: &mut Context, ) -> Task>> { @@ -2588,15 +2596,15 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { thread_view.update(cx, |thread_view, cx| { thread_view.insert_selections(window, cx); }); - } else if let Some(context_editor) = panel.active_context_editor() { + } else if let Some(text_thread_editor) = panel.active_text_thread_editor() { let snapshot = buffer.read(cx).snapshot(cx); let selection_ranges = selection_ranges .into_iter() .map(|range| range.to_point(&snapshot)) .collect::>(); - context_editor.update(cx, |context_editor, cx| { - context_editor.quote_ranges(selection_ranges, snapshot, window, cx) + text_thread_editor.update(cx, |text_thread_editor, cx| { + text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx) }); } }); diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index cc0d212a86f5db3b0d5cf8ad4b0457689512f33c..7869aa4e0191f393a05ff1b2c0307bccaef41dc8 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -250,7 +250,7 @@ pub fn init( ) { AgentSettings::register(cx); - assistant_context::init(client.clone(), cx); + assistant_text_thread::init(client.clone(), cx); rules_library::init(cx); if !is_eval { // Initializing the language model from the user settings messes with the eval, so we only initialize them when diff --git a/crates/agent_ui/src/context.rs b/crates/agent_ui/src/context.rs index 3d0600605153fd8343205f3889953c100bde7a7a..2a1ff4a1d9d3e0bb6c8b128cf7f944e9ed3ff657 100644 --- a/crates/agent_ui/src/context.rs +++ b/crates/agent_ui/src/context.rs @@ -1,5 +1,5 @@ use agent::outline; -use assistant_context::AssistantContext; +use assistant_text_thread::TextThread; use futures::future; use futures::{FutureExt, future::Shared}; use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task}; @@ -581,7 +581,7 @@ impl Display for ThreadContext { #[derive(Debug, Clone)] pub struct TextThreadContextHandle { - pub context: Entity, + pub text_thread: Entity, pub context_id: ContextId, } @@ -595,20 +595,20 @@ pub struct TextThreadContext { impl TextThreadContextHandle { // pub fn lookup_key() -> pub fn eq_for_key(&self, other: &Self) -> bool { - self.context == other.context + self.text_thread == other.text_thread } pub fn hash_for_key(&self, state: &mut H) { - self.context.hash(state) + self.text_thread.hash(state) } pub fn title(&self, cx: &App) -> SharedString { - self.context.read(cx).summary().or_default() + self.text_thread.read(cx).summary().or_default() } fn load(self, cx: &App) -> Task> { let title = self.title(cx); - let text = self.context.read(cx).to_xml(cx); + let text = self.text_thread.read(cx).to_xml(cx); let context = AgentContext::TextThread(TextThreadContext { title, text: text.into(), diff --git a/crates/agent_ui/src/context_store.rs b/crates/agent_ui/src/context_store.rs index e2ee1cd0c94fd6132719ffcc0bd352865b5f9cf9..18aa59c8f716d59e4a0d717904b09472494c4dbc 100644 --- a/crates/agent_ui/src/context_store.rs +++ b/crates/agent_ui/src/context_store.rs @@ -5,7 +5,7 @@ use crate::context::{ }; use agent_client_protocol as acp; use anyhow::{Context as _, Result, anyhow}; -use assistant_context::AssistantContext; +use assistant_text_thread::TextThread; use collections::{HashSet, IndexSet}; use futures::{self, FutureExt}; use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity}; @@ -200,13 +200,13 @@ impl ContextStore { pub fn add_text_thread( &mut self, - context: Entity, + text_thread: Entity, remove_if_exists: bool, cx: &mut Context, ) -> Option { let context_id = self.next_context_id.post_inc(); let context = AgentContextHandle::TextThread(TextThreadContextHandle { - context, + text_thread, context_id, }); @@ -353,21 +353,15 @@ impl ContextStore { ); }; } - // SuggestedContext::Thread { thread, name: _ } => { - // if let Some(thread) = thread.upgrade() { - // let context_id = self.next_context_id.post_inc(); - // self.insert_context( - // AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }), - // cx, - // ); - // } - // } - SuggestedContext::TextThread { context, name: _ } => { - if let Some(context) = context.upgrade() { + SuggestedContext::TextThread { + text_thread, + name: _, + } => { + if let Some(text_thread) = text_thread.upgrade() { let context_id = self.next_context_id.post_inc(); self.insert_context( AgentContextHandle::TextThread(TextThreadContextHandle { - context, + text_thread, context_id, }), cx, @@ -392,7 +386,7 @@ impl ContextStore { // } AgentContextHandle::TextThread(text_thread_context) => { self.context_text_thread_paths - .extend(text_thread_context.context.read(cx).path().cloned()); + .extend(text_thread_context.text_thread.read(cx).path().cloned()); } _ => {} } @@ -414,7 +408,7 @@ impl ContextStore { .remove(thread_context.thread.read(cx).id()); } AgentContextHandle::TextThread(text_thread_context) => { - if let Some(path) = text_thread_context.context.read(cx).path() { + if let Some(path) = text_thread_context.text_thread.read(cx).path() { self.context_text_thread_paths.remove(path); } } @@ -538,13 +532,9 @@ pub enum SuggestedContext { icon_path: Option, buffer: WeakEntity, }, - // Thread { - // name: SharedString, - // thread: WeakEntity, - // }, TextThread { name: SharedString, - context: WeakEntity, + text_thread: WeakEntity, }, } @@ -552,7 +542,6 @@ impl SuggestedContext { pub fn name(&self) -> &SharedString { match self { Self::File { name, .. } => name, - // Self::Thread { name, .. } => name, Self::TextThread { name, .. } => name, } } @@ -560,7 +549,6 @@ impl SuggestedContext { pub fn icon_path(&self) -> Option { match self { Self::File { icon_path, .. } => icon_path.clone(), - // Self::Thread { .. } => None, Self::TextThread { .. } => None, } } @@ -568,7 +556,6 @@ impl SuggestedContext { pub fn kind(&self) -> ContextKind { match self { Self::File { .. } => ContextKind::File, - // Self::Thread { .. } => ContextKind::Thread, Self::TextThread { .. } => ContextKind::TextThread, } } diff --git a/crates/agent_ui/src/context_strip.rs b/crates/agent_ui/src/context_strip.rs index 3eaf59aba39cbaef12e7a4079209956e0e8bed17..d2393ac4f612cebc6cf97d10a38894e7022e53b9 100644 --- a/crates/agent_ui/src/context_strip.rs +++ b/crates/agent_ui/src/context_strip.rs @@ -132,19 +132,19 @@ impl ContextStrip { let workspace = self.workspace.upgrade()?; let panel = workspace.read(cx).panel::(cx)?.read(cx); - if let Some(active_context_editor) = panel.active_context_editor() { - let context = active_context_editor.read(cx).context(); - let weak_context = context.downgrade(); - let context = context.read(cx); - let path = context.path()?; + if let Some(active_text_thread_editor) = panel.active_text_thread_editor() { + let text_thread = active_text_thread_editor.read(cx).text_thread(); + let weak_text_thread = text_thread.downgrade(); + let text_thread = text_thread.read(cx); + let path = text_thread.path()?; if self.context_store.read(cx).includes_text_thread(path) { return None; } Some(SuggestedContext::TextThread { - name: context.summary().or_default(), - context: weak_context, + name: text_thread.summary().or_default(), + text_thread: weak_text_thread, }) } else { None @@ -332,7 +332,7 @@ impl ContextStrip { AgentContextHandle::TextThread(text_thread_context) => { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::(cx) { - let context = text_thread_context.context.clone(); + let context = text_thread_context.text_thread.clone(); window.defer(cx, move |window, cx| { panel.update(cx, |panel, cx| { panel.open_text_thread(context, window, cx) diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 0f7f1f1d78056553f758796c7e6b2f14781fce0f..b05dba59e6b19fa5091903882748de853cd9cb93 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -1508,8 +1508,8 @@ impl InlineAssistant { return Some(InlineAssistTarget::Terminal(terminal_view)); } - let context_editor = agent_panel - .and_then(|panel| panel.read(cx).active_context_editor()) + let text_thread_editor = agent_panel + .and_then(|panel| panel.read(cx).active_text_thread_editor()) .and_then(|editor| { let editor = &editor.read(cx).editor().clone(); if editor.read(cx).is_focused(window) { @@ -1519,8 +1519,8 @@ impl InlineAssistant { } }); - if let Some(context_editor) = context_editor { - Some(InlineAssistTarget::Editor(context_editor)) + if let Some(text_thread_editor) = text_thread_editor { + Some(InlineAssistTarget::Editor(text_thread_editor)) } else if let Some(workspace_editor) = workspace .active_item(cx) .and_then(|item| item.act_as::(cx)) diff --git a/crates/agent_ui/src/slash_command_picker.rs b/crates/agent_ui/src/slash_command_picker.rs index a6bb61510cbeb557e22018c73082bba17d177d7e..0c3cf37599887fe8e97dcdc67bb0bd7e28a744a7 100644 --- a/crates/agent_ui/src/slash_command_picker.rs +++ b/crates/agent_ui/src/slash_command_picker.rs @@ -155,8 +155,8 @@ impl PickerDelegate for SlashCommandDelegate { match command { SlashCommandEntry::Info(info) => { self.active_context_editor - .update(cx, |context_editor, cx| { - context_editor.insert_command(&info.name, window, cx) + .update(cx, |text_thread_editor, cx| { + text_thread_editor.insert_command(&info.name, window, cx) }) .ok(); } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 5aa6f1f6d9405dc7556cb87c82d5300308f059d1..667ccb8938b892dcf59232d5cd7ea8dda04bc4b2 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -74,10 +74,10 @@ use workspace::{ use zed_actions::agent::{AddSelectionToThread, ToggleModelSelector}; use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker}; -use assistant_context::{ - AssistantContext, CacheStatus, Content, ContextEvent, ContextId, InvokedSlashCommandId, - InvokedSlashCommandStatus, Message, MessageId, MessageMetadata, MessageStatus, - PendingSlashCommandStatus, ThoughtProcessOutputSection, +use assistant_text_thread::{ + CacheStatus, Content, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId, + MessageMetadata, MessageStatus, PendingSlashCommandStatus, TextThread, TextThreadEvent, + TextThreadId, ThoughtProcessOutputSection, }; actions!( @@ -126,14 +126,14 @@ pub enum ThoughtProcessStatus { } pub trait AgentPanelDelegate { - fn active_context_editor( + fn active_text_thread_editor( &self, workspace: &mut Workspace, window: &mut Window, cx: &mut Context, ) -> Option>; - fn open_saved_context( + fn open_local_text_thread( &self, workspace: &mut Workspace, path: Arc, @@ -141,10 +141,10 @@ pub trait AgentPanelDelegate { cx: &mut Context, ) -> Task>; - fn open_remote_context( + fn open_remote_text_thread( &self, workspace: &mut Workspace, - context_id: ContextId, + text_thread_id: TextThreadId, window: &mut Window, cx: &mut Context, ) -> Task>>; @@ -177,7 +177,7 @@ struct GlobalAssistantPanelDelegate(Arc); impl Global for GlobalAssistantPanelDelegate {} pub struct TextThreadEditor { - context: Entity, + text_thread: Entity, fs: Arc, slash_commands: Arc, workspace: WeakEntity, @@ -223,8 +223,8 @@ impl TextThreadEditor { .detach(); } - pub fn for_context( - context: Entity, + pub fn for_text_thread( + text_thread: Entity, fs: Arc, workspace: WeakEntity, project: Entity, @@ -233,14 +233,14 @@ impl TextThreadEditor { cx: &mut Context, ) -> Self { let completion_provider = SlashCommandCompletionProvider::new( - context.read(cx).slash_commands().clone(), + text_thread.read(cx).slash_commands().clone(), Some(cx.entity().downgrade()), Some(workspace.clone()), ); let editor = cx.new(|cx| { let mut editor = - Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx); + Editor::for_buffer(text_thread.read(cx).buffer().clone(), None, window, cx); editor.disable_scrollbars_and_minimap(window, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_line_numbers(false, cx); @@ -264,18 +264,24 @@ impl TextThreadEditor { }); let _subscriptions = vec![ - cx.observe(&context, |_, _, cx| cx.notify()), - cx.subscribe_in(&context, window, Self::handle_context_event), + cx.observe(&text_thread, |_, _, cx| cx.notify()), + cx.subscribe_in(&text_thread, window, Self::handle_text_thread_event), cx.subscribe_in(&editor, window, Self::handle_editor_event), cx.subscribe_in(&editor, window, Self::handle_editor_search_event), cx.observe_global_in::(window, Self::settings_changed), ]; - let slash_command_sections = context.read(cx).slash_command_output_sections().to_vec(); - let thought_process_sections = context.read(cx).thought_process_output_sections().to_vec(); - let slash_commands = context.read(cx).slash_commands().clone(); + let slash_command_sections = text_thread + .read(cx) + .slash_command_output_sections() + .to_vec(); + let thought_process_sections = text_thread + .read(cx) + .thought_process_output_sections() + .to_vec(); + let slash_commands = text_thread.read(cx).slash_commands().clone(); let mut this = Self { - context, + text_thread, slash_commands, editor, lsp_adapter_delegate, @@ -337,8 +343,8 @@ impl TextThreadEditor { }); } - pub fn context(&self) -> &Entity { - &self.context + pub fn text_thread(&self) -> &Entity { + &self.text_thread } pub fn editor(&self) -> &Entity { @@ -350,9 +356,9 @@ impl TextThreadEditor { self.editor.update(cx, |editor, cx| { editor.insert(&format!("/{command_name}\n\n"), window, cx) }); - let command = self.context.update(cx, |context, cx| { - context.reparse(cx); - context.parsed_slash_commands()[0].clone() + let command = self.text_thread.update(cx, |text_thread, cx| { + text_thread.reparse(cx); + text_thread.parsed_slash_commands()[0].clone() }); self.run_command( command.source_range, @@ -375,11 +381,14 @@ impl TextThreadEditor { fn send_to_model(&mut self, window: &mut Window, cx: &mut Context) { self.last_error = None; - if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) { + if let Some(user_message) = self + .text_thread + .update(cx, |text_thread, cx| text_thread.assist(cx)) + { let new_selection = { let cursor = user_message .start - .to_offset(self.context.read(cx).buffer().read(cx)); + .to_offset(self.text_thread.read(cx).buffer().read(cx)); cursor..cursor }; self.editor.update(cx, |editor, cx| { @@ -403,8 +412,8 @@ impl TextThreadEditor { self.last_error = None; if self - .context - .update(cx, |context, cx| context.cancel_last_assist(cx)) + .text_thread + .update(cx, |text_thread, cx| text_thread.cancel_last_assist(cx)) { return; } @@ -419,13 +428,13 @@ impl TextThreadEditor { cx: &mut Context, ) { let cursors = self.cursors(cx); - self.context.update(cx, |context, cx| { - let messages = context + self.text_thread.update(cx, |text_thread, cx| { + let messages = text_thread .messages_for_offsets(cursors, cx) .into_iter() .map(|message| message.id) .collect(); - context.cycle_message_roles(messages, cx) + text_thread.cycle_message_roles(messages, cx) }); } @@ -491,11 +500,11 @@ impl TextThreadEditor { let selections = self.editor.read(cx).selections.disjoint_anchors_arc(); let mut commands_by_range = HashMap::default(); let workspace = self.workspace.clone(); - self.context.update(cx, |context, cx| { - context.reparse(cx); + self.text_thread.update(cx, |text_thread, cx| { + text_thread.reparse(cx); for selection in selections.iter() { if let Some(command) = - context.pending_command_for_position(selection.head().text_anchor, cx) + text_thread.pending_command_for_position(selection.head().text_anchor, cx) { commands_by_range .entry(command.source_range.clone()) @@ -533,14 +542,14 @@ impl TextThreadEditor { cx: &mut Context, ) { if let Some(command) = self.slash_commands.command(name, cx) { - let context = self.context.read(cx); - let sections = context + let text_thread = self.text_thread.read(cx); + let sections = text_thread .slash_command_output_sections() .iter() - .filter(|section| section.is_valid(context.buffer().read(cx))) + .filter(|section| section.is_valid(text_thread.buffer().read(cx))) .cloned() .collect::>(); - let snapshot = context.buffer().read(cx).snapshot(); + let snapshot = text_thread.buffer().read(cx).snapshot(); let output = command.run( arguments, §ions, @@ -550,8 +559,8 @@ impl TextThreadEditor { window, cx, ); - self.context.update(cx, |context, cx| { - context.insert_command_output( + self.text_thread.update(cx, |text_thread, cx| { + text_thread.insert_command_output( command_range, name, output, @@ -562,32 +571,32 @@ impl TextThreadEditor { } } - fn handle_context_event( + fn handle_text_thread_event( &mut self, - _: &Entity, - event: &ContextEvent, + _: &Entity, + event: &TextThreadEvent, window: &mut Window, cx: &mut Context, ) { - let context_editor = cx.entity().downgrade(); + let text_thread_editor = cx.entity().downgrade(); match event { - ContextEvent::MessagesEdited => { + TextThreadEvent::MessagesEdited => { self.update_message_headers(cx); self.update_image_blocks(cx); - self.context.update(cx, |context, cx| { - context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + self.text_thread.update(cx, |text_thread, cx| { + text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); }); } - ContextEvent::SummaryChanged => { + TextThreadEvent::SummaryChanged => { cx.emit(EditorEvent::TitleChanged); - self.context.update(cx, |context, cx| { - context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + self.text_thread.update(cx, |text_thread, cx| { + text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); }); } - ContextEvent::SummaryGenerated => {} - ContextEvent::PathChanged { .. } => {} - ContextEvent::StartedThoughtProcess(range) => { + TextThreadEvent::SummaryGenerated => {} + TextThreadEvent::PathChanged { .. } => {} + TextThreadEvent::StartedThoughtProcess(range) => { let creases = self.insert_thought_process_output_sections( [( ThoughtProcessOutputSection { @@ -600,7 +609,7 @@ impl TextThreadEditor { ); self.pending_thought_process = Some((creases[0], range.start)); } - ContextEvent::EndedThoughtProcess(end) => { + TextThreadEvent::EndedThoughtProcess(end) => { if let Some((crease_id, start)) = self.pending_thought_process.take() { self.editor.update(cx, |editor, cx| { let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); @@ -626,7 +635,7 @@ impl TextThreadEditor { ); } } - ContextEvent::StreamedCompletion => { + TextThreadEvent::StreamedCompletion => { self.editor.update(cx, |editor, cx| { if let Some(scroll_position) = self.scroll_position { let snapshot = editor.snapshot(window, cx); @@ -641,7 +650,7 @@ impl TextThreadEditor { } }); } - ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { + TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => { self.editor.update(cx, |editor, cx| { let buffer = editor.buffer().read(cx).snapshot(cx); let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); @@ -657,12 +666,12 @@ impl TextThreadEditor { updated.iter().map(|command| { let workspace = self.workspace.clone(); let confirm_command = Arc::new({ - let context_editor = context_editor.clone(); + let text_thread_editor = text_thread_editor.clone(); let command = command.clone(); move |window: &mut Window, cx: &mut App| { - context_editor - .update(cx, |context_editor, cx| { - context_editor.run_command( + text_thread_editor + .update(cx, |text_thread_editor, cx| { + text_thread_editor.run_command( command.source_range.clone(), &command.name, &command.arguments, @@ -712,17 +721,17 @@ impl TextThreadEditor { ); }) } - ContextEvent::InvokedSlashCommandChanged { command_id } => { + TextThreadEvent::InvokedSlashCommandChanged { command_id } => { self.update_invoked_slash_command(*command_id, window, cx); } - ContextEvent::SlashCommandOutputSectionAdded { section } => { + TextThreadEvent::SlashCommandOutputSectionAdded { section } => { self.insert_slash_command_output_sections([section.clone()], false, window, cx); } - ContextEvent::Operation(_) => {} - ContextEvent::ShowAssistError(error_message) => { + TextThreadEvent::Operation(_) => {} + TextThreadEvent::ShowAssistError(error_message) => { self.last_error = Some(AssistError::Message(error_message.clone())); } - ContextEvent::ShowPaymentRequiredError => { + TextThreadEvent::ShowPaymentRequiredError => { self.last_error = Some(AssistError::PaymentRequired); } } @@ -735,14 +744,14 @@ impl TextThreadEditor { cx: &mut Context, ) { if let Some(invoked_slash_command) = - self.context.read(cx).invoked_slash_command(&command_id) + self.text_thread.read(cx).invoked_slash_command(&command_id) && let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { let run_commands_in_ranges = invoked_slash_command.run_commands_in_ranges.clone(); for range in run_commands_in_ranges { - let commands = self.context.update(cx, |context, cx| { - context.reparse(cx); - context + let commands = self.text_thread.update(cx, |text_thread, cx| { + text_thread.reparse(cx); + text_thread .pending_commands_for_range(range.clone(), cx) .to_vec() }); @@ -763,7 +772,7 @@ impl TextThreadEditor { self.editor.update(cx, |editor, cx| { if let Some(invoked_slash_command) = - self.context.read(cx).invoked_slash_command(&command_id) + self.text_thread.read(cx).invoked_slash_command(&command_id) { if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status { let buffer = editor.buffer().read(cx).snapshot(cx); @@ -790,7 +799,7 @@ impl TextThreadEditor { let buffer = editor.buffer().read(cx).snapshot(cx); let (&excerpt_id, _buffer_id, _buffer_snapshot) = buffer.as_singleton().unwrap(); - let context = self.context.downgrade(); + let context = self.text_thread.downgrade(); let range = buffer .anchor_range_in_excerpt(excerpt_id, invoked_slash_command.range.clone()) .unwrap(); @@ -1020,7 +1029,7 @@ impl TextThreadEditor { let render_block = |message: MessageMetadata| -> RenderBlock { Arc::new({ - let context = self.context.clone(); + let text_thread = self.text_thread.clone(); move |cx| { let message_id = MessageId(message.timestamp); @@ -1093,10 +1102,10 @@ impl TextThreadEditor { ) }) .on_click({ - let context = context.clone(); + let text_thread = text_thread.clone(); move |_, _window, cx| { - context.update(cx, |context, cx| { - context.cycle_message_roles( + text_thread.update(cx, |text_thread, cx| { + text_thread.cycle_message_roles( HashSet::from_iter(Some(message_id)), cx, ) @@ -1158,11 +1167,11 @@ impl TextThreadEditor { .icon_position(IconPosition::Start) .tooltip(Tooltip::text("View Details")) .on_click({ - let context = context.clone(); + let text_thread = text_thread.clone(); let error = error.clone(); move |_, _window, cx| { - context.update(cx, |_, cx| { - cx.emit(ContextEvent::ShowAssistError( + text_thread.update(cx, |_, cx| { + cx.emit(TextThreadEvent::ShowAssistError( error.clone(), )); }); @@ -1205,7 +1214,7 @@ impl TextThreadEditor { }; let mut new_blocks = vec![]; let mut block_index_to_message = vec![]; - for message in self.context.read(cx).messages(cx) { + for message in self.text_thread.read(cx).messages(cx) { if blocks_to_remove.remove(&message.id).is_some() { // This is an old message that we might modify. let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else { @@ -1246,18 +1255,18 @@ impl TextThreadEditor { ) -> Option<(String, bool)> { const CODE_FENCE_DELIMITER: &str = "```"; - let context_editor = context_editor_view.read(cx).editor.clone(); - context_editor.update(cx, |context_editor, cx| { - let display_map = context_editor.display_snapshot(cx); - if context_editor + let text_thread_editor = context_editor_view.read(cx).editor.clone(); + text_thread_editor.update(cx, |text_thread_editor, cx| { + let display_map = text_thread_editor.display_snapshot(cx); + if text_thread_editor .selections .newest::(&display_map) .is_empty() { - let snapshot = context_editor.buffer().read(cx).snapshot(cx); + let snapshot = text_thread_editor.buffer().read(cx).snapshot(cx); let (_, _, snapshot) = snapshot.as_singleton()?; - let head = context_editor + let head = text_thread_editor .selections .newest::(&display_map) .head(); @@ -1277,8 +1286,8 @@ impl TextThreadEditor { (!text.is_empty()).then_some((text, true)) } else { - let selection = context_editor.selections.newest_adjusted(&display_map); - let buffer = context_editor.buffer().read(cx).snapshot(cx); + let selection = text_thread_editor.selections.newest_adjusted(&display_map); + let buffer = text_thread_editor.buffer().read(cx).snapshot(cx); let selected_text = buffer.text_for_range(selection.range()).collect::(); (!selected_text.is_empty()).then_some((selected_text, false)) @@ -1296,7 +1305,7 @@ impl TextThreadEditor { return; }; let Some(context_editor_view) = - agent_panel_delegate.active_context_editor(workspace, window, cx) + agent_panel_delegate.active_text_thread_editor(workspace, window, cx) else { return; }; @@ -1324,7 +1333,7 @@ impl TextThreadEditor { let result = maybe!({ let agent_panel_delegate = ::try_global(cx)?; let context_editor_view = - agent_panel_delegate.active_context_editor(workspace, window, cx)?; + agent_panel_delegate.active_text_thread_editor(workspace, window, cx)?; Self::get_selection_or_code_block(&context_editor_view, cx) }); let Some((text, is_code_block)) = result else { @@ -1361,7 +1370,7 @@ impl TextThreadEditor { return; }; let Some(context_editor_view) = - agent_panel_delegate.active_context_editor(workspace, window, cx) + agent_panel_delegate.active_text_thread_editor(workspace, window, cx) else { return; }; @@ -1622,29 +1631,33 @@ impl TextThreadEditor { ) }); - let context = self.context.read(cx); + let text_thread = self.text_thread.read(cx); let mut text = String::new(); // If selection is empty, we want to copy the entire line if selection.range().is_empty() { - let snapshot = context.buffer().read(cx).snapshot(); + let snapshot = text_thread.buffer().read(cx).snapshot(); let point = snapshot.offset_to_point(selection.range().start); selection.start = snapshot.point_to_offset(Point::new(point.row, 0)); selection.end = snapshot .point_to_offset(cmp::min(Point::new(point.row + 1, 0), snapshot.max_point())); - for chunk in context.buffer().read(cx).text_for_range(selection.range()) { + for chunk in text_thread + .buffer() + .read(cx) + .text_for_range(selection.range()) + { text.push_str(chunk); } } else { - for message in context.messages(cx) { + for message in text_thread.messages(cx) { if message.offset_range.start >= selection.range().end { break; } else if message.offset_range.end >= selection.range().start { let range = cmp::max(message.offset_range.start, selection.range().start) ..cmp::min(message.offset_range.end, selection.range().end); if !range.is_empty() { - for chunk in context.buffer().read(cx).text_for_range(range) { + for chunk in text_thread.buffer().read(cx).text_for_range(range) { text.push_str(chunk); } if message.offset_range.end < selection.range().end { @@ -1755,7 +1768,7 @@ impl TextThreadEditor { }); }); - self.context.update(cx, |context, cx| { + self.text_thread.update(cx, |text_thread, cx| { for image in images { let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err() else { @@ -1765,7 +1778,7 @@ impl TextThreadEditor { let image_task = LanguageModelImage::from_image(Arc::new(image), cx).shared(); for image_position in image_positions.iter() { - context.insert_content( + text_thread.insert_content( Content::Image { anchor: image_position.text_anchor, image_id, @@ -1786,7 +1799,7 @@ impl TextThreadEditor { let excerpt_id = *buffer.as_singleton().unwrap().0; let old_blocks = std::mem::take(&mut self.image_blocks); let new_blocks = self - .context + .text_thread .read(cx) .contents(cx) .map( @@ -1834,36 +1847,36 @@ impl TextThreadEditor { } fn split(&mut self, _: &Split, _window: &mut Window, cx: &mut Context) { - self.context.update(cx, |context, cx| { + self.text_thread.update(cx, |text_thread, cx| { let selections = self.editor.read(cx).selections.disjoint_anchors_arc(); for selection in selections.as_ref() { let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); let range = selection .map(|endpoint| endpoint.to_offset(&buffer)) .range(); - context.split_message(range, cx); + text_thread.split_message(range, cx); } }); } fn save(&mut self, _: &Save, _window: &mut Window, cx: &mut Context) { - self.context.update(cx, |context, cx| { - context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx) + self.text_thread.update(cx, |text_thread, cx| { + text_thread.save(Some(Duration::from_millis(500)), self.fs.clone(), cx) }); } pub fn title(&self, cx: &App) -> SharedString { - self.context.read(cx).summary().or_default() + self.text_thread.read(cx).summary().or_default() } pub fn regenerate_summary(&mut self, cx: &mut Context) { - self.context - .update(cx, |context, cx| context.summarize(true, cx)); + self.text_thread + .update(cx, |text_thread, cx| text_thread.summarize(true, cx)); } fn render_remaining_tokens(&self, cx: &App) -> Option> { let (token_count_color, token_count, max_token_count, tooltip) = - match token_state(&self.context, cx)? { + match token_state(&self.text_thread, cx)? { TokenState::NoTokensLeft { max_token_count, token_count, @@ -1911,7 +1924,7 @@ impl TextThreadEditor { fn render_send_button(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let focus_handle = self.focus_handle(cx); - let (style, tooltip) = match token_state(&self.context, cx) { + let (style, tooltip) = match token_state(&self.text_thread, cx) { Some(TokenState::NoTokensLeft { .. }) => ( ButtonStyle::Tinted(TintColor::Error), Some(Tooltip::text("Token limit reached")(window, cx)), @@ -1986,7 +1999,7 @@ impl TextThreadEditor { } fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { - let context = self.context().read(cx); + let text_thread = self.text_thread().read(cx); let active_model = LanguageModelRegistry::read_global(cx) .default_model() .map(|default| default.model)?; @@ -1994,7 +2007,7 @@ impl TextThreadEditor { return None; } - let active_completion_mode = context.completion_mode(); + let active_completion_mode = text_thread.completion_mode(); let burn_mode_enabled = active_completion_mode == CompletionMode::Burn; let icon = if burn_mode_enabled { IconName::ZedBurnModeOn @@ -2009,8 +2022,8 @@ impl TextThreadEditor { .toggle_state(burn_mode_enabled) .selected_icon_color(Color::Error) .on_click(cx.listener(move |this, _event, _window, cx| { - this.context().update(cx, |context, _cx| { - context.set_completion_mode(match active_completion_mode { + this.text_thread().update(cx, |text_thread, _cx| { + text_thread.set_completion_mode(match active_completion_mode { CompletionMode::Burn => CompletionMode::Normal, CompletionMode::Normal => CompletionMode::Burn, }); @@ -2637,10 +2650,10 @@ impl FollowableItem for TextThreadEditor { } fn to_state_proto(&self, window: &Window, cx: &App) -> Option { - let context = self.context.read(cx); + let text_thread = self.text_thread.read(cx); Some(proto::view::Variant::ContextEditor( proto::view::ContextEditor { - context_id: context.id().to_proto(), + context_id: text_thread.id().to_proto(), editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(window, cx) { @@ -2666,22 +2679,22 @@ impl FollowableItem for TextThreadEditor { unreachable!() }; - let context_id = ContextId::from_proto(state.context_id); + let text_thread_id = TextThreadId::from_proto(state.context_id); let editor_state = state.editor?; let project = workspace.read(cx).project().clone(); let agent_panel_delegate = ::try_global(cx)?; - let context_editor_task = workspace.update(cx, |workspace, cx| { - agent_panel_delegate.open_remote_context(workspace, context_id, window, cx) + let text_thread_editor_task = workspace.update(cx, |workspace, cx| { + agent_panel_delegate.open_remote_text_thread(workspace, text_thread_id, window, cx) }); Some(window.spawn(cx, async move |cx| { - let context_editor = context_editor_task.await?; - context_editor - .update_in(cx, |context_editor, window, cx| { - context_editor.remote_id = Some(id); - context_editor.editor.update(cx, |editor, cx| { + let text_thread_editor = text_thread_editor_task.await?; + text_thread_editor + .update_in(cx, |text_thread_editor, window, cx| { + text_thread_editor.remote_id = Some(id); + text_thread_editor.editor.update(cx, |editor, cx| { editor.apply_update_proto( &project, proto::update_view::Variant::Editor(proto::update_view::Editor { @@ -2698,7 +2711,7 @@ impl FollowableItem for TextThreadEditor { }) })? .await?; - Ok(context_editor) + Ok(text_thread_editor) })) } @@ -2745,7 +2758,7 @@ impl FollowableItem for TextThreadEditor { } fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option { - if existing.context.read(cx).id() == self.context.read(cx).id() { + if existing.text_thread.read(cx).id() == self.text_thread.read(cx).id() { Some(item::Dedup::KeepExisting) } else { None @@ -2757,17 +2770,17 @@ enum PendingSlashCommand {} fn invoked_slash_command_fold_placeholder( command_id: InvokedSlashCommandId, - context: WeakEntity, + text_thread: WeakEntity, ) -> FoldPlaceholder { FoldPlaceholder { constrain_width: false, merge_adjacent: false, render: Arc::new(move |fold_id, _, cx| { - let Some(context) = context.upgrade() else { + let Some(text_thread) = text_thread.upgrade() else { return Empty.into_any(); }; - let Some(command) = context.read(cx).invoked_slash_command(&command_id) else { + let Some(command) = text_thread.read(cx).invoked_slash_command(&command_id) else { return Empty.into_any(); }; @@ -2808,14 +2821,15 @@ enum TokenState { }, } -fn token_state(context: &Entity, cx: &App) -> Option { +fn token_state(text_thread: &Entity, cx: &App) -> Option { const WARNING_TOKEN_THRESHOLD: f32 = 0.8; let model = LanguageModelRegistry::read_global(cx) .default_model()? .model; - let token_count = context.read(cx).token_count()?; - let max_token_count = model.max_token_count_for_mode(context.read(cx).completion_mode().into()); + let token_count = text_thread.read(cx).token_count()?; + let max_token_count = + model.max_token_count_for_mode(text_thread.read(cx).completion_mode().into()); let token_state = if max_token_count.saturating_sub(token_count) == 0 { TokenState::NoTokensLeft { max_token_count, @@ -2927,7 +2941,7 @@ mod tests { #[gpui::test] async fn test_copy_paste_whole_message(cx: &mut TestAppContext) { - let (context, context_editor, mut cx) = setup_context_editor_text(vec![ + let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text(vec![ (Role::User, "What is the Zed editor?"), ( Role::Assistant, @@ -2937,8 +2951,8 @@ mod tests { ],cx).await; // Select & Copy whole user message - assert_copy_paste_context_editor( - &context_editor, + assert_copy_paste_text_thread_editor( + &text_thread_editor, message_range(&context, 0, &mut cx), indoc! {" What is the Zed editor? @@ -2949,8 +2963,8 @@ mod tests { ); // Select & Copy whole assistant message - assert_copy_paste_context_editor( - &context_editor, + assert_copy_paste_text_thread_editor( + &text_thread_editor, message_range(&context, 1, &mut cx), indoc! {" What is the Zed editor? @@ -2964,7 +2978,7 @@ mod tests { #[gpui::test] async fn test_copy_paste_no_selection(cx: &mut TestAppContext) { - let (context, context_editor, mut cx) = setup_context_editor_text( + let (context, text_thread_editor, mut cx) = setup_text_thread_editor_text( vec![ (Role::User, "user1"), (Role::Assistant, "assistant1"), @@ -2977,8 +2991,8 @@ mod tests { // Copy and paste first assistant message let message_2_range = message_range(&context, 1, &mut cx); - assert_copy_paste_context_editor( - &context_editor, + assert_copy_paste_text_thread_editor( + &text_thread_editor, message_2_range.start..message_2_range.start, indoc! {" user1 @@ -2991,8 +3005,8 @@ mod tests { // Copy and cut second assistant message let message_3_range = message_range(&context, 2, &mut cx); - assert_copy_paste_context_editor( - &context_editor, + assert_copy_paste_text_thread_editor( + &text_thread_editor, message_3_range.start..message_3_range.start, indoc! {" user1 @@ -3079,29 +3093,29 @@ mod tests { } } - async fn setup_context_editor_text( + async fn setup_text_thread_editor_text( messages: Vec<(Role, &str)>, cx: &mut TestAppContext, ) -> ( - Entity, + Entity, Entity, VisualTestContext, ) { cx.update(init_test); let fs = FakeFs::new(cx.executor()); - let context = create_context_with_messages(messages, cx); + let text_thread = create_text_thread_with_messages(messages, cx); let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); let workspace = window.root(cx).unwrap(); let mut cx = VisualTestContext::from_window(*window, cx); - let context_editor = window + let text_thread_editor = window .update(&mut cx, |_, window, cx| { cx.new(|cx| { - TextThreadEditor::for_context( - context.clone(), + TextThreadEditor::for_text_thread( + text_thread.clone(), fs, workspace.downgrade(), project, @@ -3113,59 +3127,59 @@ mod tests { }) .unwrap(); - (context, context_editor, cx) + (text_thread, text_thread_editor, cx) } fn message_range( - context: &Entity, + text_thread: &Entity, message_ix: usize, cx: &mut TestAppContext, ) -> Range { - context.update(cx, |context, cx| { - context + text_thread.update(cx, |text_thread, cx| { + text_thread .messages(cx) .nth(message_ix) .unwrap() .anchor_range - .to_offset(&context.buffer().read(cx).snapshot()) + .to_offset(&text_thread.buffer().read(cx).snapshot()) }) } - fn assert_copy_paste_context_editor( - context_editor: &Entity, + fn assert_copy_paste_text_thread_editor( + text_thread_editor: &Entity, range: Range, expected_text: &str, cx: &mut VisualTestContext, ) { - context_editor.update_in(cx, |context_editor, window, cx| { - context_editor.editor.update(cx, |editor, cx| { + text_thread_editor.update_in(cx, |text_thread_editor, window, cx| { + text_thread_editor.editor.update(cx, |editor, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([range]) }); }); - context_editor.copy(&Default::default(), window, cx); + text_thread_editor.copy(&Default::default(), window, cx); - context_editor.editor.update(cx, |editor, cx| { + text_thread_editor.editor.update(cx, |editor, cx| { editor.move_to_end(&Default::default(), window, cx); }); - context_editor.paste(&Default::default(), window, cx); + text_thread_editor.paste(&Default::default(), window, cx); - context_editor.editor.update(cx, |editor, cx| { + text_thread_editor.editor.update(cx, |editor, cx| { assert_eq!(editor.text(cx), expected_text); }); }); } - fn create_context_with_messages( + fn create_text_thread_with_messages( mut messages: Vec<(Role, &str)>, cx: &mut TestAppContext, - ) -> Entity { + ) -> Entity { let registry = Arc::new(LanguageRegistry::test(cx.executor())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); cx.new(|cx| { - let mut context = AssistantContext::local( + let mut text_thread = TextThread::local( registry, None, None, @@ -3173,33 +3187,33 @@ mod tests { Arc::new(SlashCommandWorkingSet::default()), cx, ); - let mut message_1 = context.messages(cx).next().unwrap(); + let mut message_1 = text_thread.messages(cx).next().unwrap(); let (role, text) = messages.remove(0); loop { if role == message_1.role { - context.buffer().update(cx, |buffer, cx| { + text_thread.buffer().update(cx, |buffer, cx| { buffer.edit([(message_1.offset_range, text)], None, cx); }); break; } let mut ids = HashSet::default(); ids.insert(message_1.id); - context.cycle_message_roles(ids, cx); - message_1 = context.messages(cx).next().unwrap(); + text_thread.cycle_message_roles(ids, cx); + message_1 = text_thread.messages(cx).next().unwrap(); } let mut last_message_id = message_1.id; for (role, text) in messages { - context.insert_message_after(last_message_id, role, MessageStatus::Done, cx); - let message = context.messages(cx).last().unwrap(); + text_thread.insert_message_after(last_message_id, role, MessageStatus::Done, cx); + let message = text_thread.messages(cx).last().unwrap(); last_message_id = message.id; - context.buffer().update(cx, |buffer, cx| { + text_thread.buffer().update(cx, |buffer, cx| { buffer.edit([(message.offset_range, text)], None, cx); }) } - context + text_thread }) } diff --git a/crates/agent_ui/src/ui/context_pill.rs b/crates/agent_ui/src/ui/context_pill.rs index 43d3799d697e28d43c71fc6e6e77cc058eaec5b2..89bf618a16d3fb8e7abc5afaf34ee6e8bb43ab67 100644 --- a/crates/agent_ui/src/ui/context_pill.rs +++ b/crates/agent_ui/src/ui/context_pill.rs @@ -497,9 +497,9 @@ impl AddedContext { icon_path: None, status: ContextStatus::Ready, render_hover: { - let context = handle.context.clone(); + let text_thread = handle.text_thread.clone(); Some(Rc::new(move |_, cx| { - let text = context.read(cx).to_xml(cx); + let text = text_thread.read(cx).to_xml(cx); ContextPillHover::new_text(text.into(), cx).into() })) }, diff --git a/crates/assistant_context/Cargo.toml b/crates/assistant_text_thread/Cargo.toml similarity index 95% rename from crates/assistant_context/Cargo.toml rename to crates/assistant_text_thread/Cargo.toml index 2d3e8bc4080a314c480bb11e459a745cb7ce6704..8dfdfa3828340217456088a246eee5b1568a7a77 100644 --- a/crates/assistant_context/Cargo.toml +++ b/crates/assistant_text_thread/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "assistant_context" +name = "assistant_text_thread" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,7 +9,7 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/assistant_context.rs" +path = "src/assistant_text_thread.rs" [features] test-support = [] diff --git a/crates/assistant_context/LICENSE-GPL b/crates/assistant_text_thread/LICENSE-GPL similarity index 100% rename from crates/assistant_context/LICENSE-GPL rename to crates/assistant_text_thread/LICENSE-GPL diff --git a/crates/assistant_text_thread/src/assistant_text_thread.rs b/crates/assistant_text_thread/src/assistant_text_thread.rs new file mode 100644 index 0000000000000000000000000000000000000000..7eab9800d5d6f43ba8eabec0682961e073781ace --- /dev/null +++ b/crates/assistant_text_thread/src/assistant_text_thread.rs @@ -0,0 +1,15 @@ +#[cfg(test)] +mod assistant_text_thread_tests; +mod text_thread; +mod text_thread_store; + +pub use crate::text_thread::*; +pub use crate::text_thread_store::*; + +use client::Client; +use gpui::App; +use std::sync::Arc; + +pub fn init(client: Arc, _: &mut App) { + text_thread_store::init(&client.into()); +} diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_text_thread/src/assistant_text_thread_tests.rs similarity index 75% rename from crates/assistant_context/src/assistant_context_tests.rs rename to crates/assistant_text_thread/src/assistant_text_thread_tests.rs index 2d987f9f845b471438cfb3eb0667fbc36161c53c..fbd5dcafa6e142538f1f5821bc9e0a89ccbfd881 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_text_thread/src/assistant_text_thread_tests.rs @@ -1,6 +1,6 @@ use crate::{ - AssistantContext, CacheStatus, ContextEvent, ContextId, ContextOperation, ContextSummary, - InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus, + CacheStatus, InvokedSlashCommandId, MessageCacheMetadata, MessageId, MessageStatus, TextThread, + TextThreadEvent, TextThreadId, TextThreadOperation, TextThreadSummary, }; use anyhow::Result; use assistant_slash_command::{ @@ -47,8 +47,8 @@ fn test_inserting_and_removing_messages(cx: &mut App) { let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new(|cx| { - AssistantContext::local( + let text_thread = cx.new(|cx| { + TextThread::local( registry, None, None, @@ -57,21 +57,21 @@ fn test_inserting_and_removing_messages(cx: &mut App) { cx, ) }); - let buffer = context.read(cx).buffer.clone(); + let buffer = text_thread.read(cx).buffer().clone(); - let message_1 = context.read(cx).message_anchors[0].clone(); + let message_1 = text_thread.read(cx).message_anchors[0].clone(); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![(message_1.id, Role::User, 0..0)] ); - let message_2 = context.update(cx, |context, cx| { + let message_2 = text_thread.update(cx, |context, cx| { context .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..1), (message_2.id, Role::Assistant, 1..1) @@ -82,20 +82,20 @@ fn test_inserting_and_removing_messages(cx: &mut App) { buffer.edit([(0..0, "1"), (1..1, "2")], None, cx) }); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..3) ] ); - let message_3 = context.update(cx, |context, cx| { + let message_3 = text_thread.update(cx, |context, cx| { context .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -103,13 +103,13 @@ fn test_inserting_and_removing_messages(cx: &mut App) { ] ); - let message_4 = context.update(cx, |context, cx| { + let message_4 = text_thread.update(cx, |context, cx| { context .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -122,7 +122,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) { buffer.edit([(4..4, "C"), (5..5, "D")], None, cx) }); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -134,7 +134,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) { // Deleting across message boundaries merges the messages. buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx)); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..3), (message_3.id, Role::User, 3..4), @@ -144,7 +144,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) { // Undoing the deletion should also undo the merge. buffer.update(cx, |buffer, cx| buffer.undo(cx)); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -156,7 +156,7 @@ fn test_inserting_and_removing_messages(cx: &mut App) { // Redoing the deletion should also redo the merge. buffer.update(cx, |buffer, cx| buffer.redo(cx)); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..3), (message_3.id, Role::User, 3..4), @@ -164,13 +164,13 @@ fn test_inserting_and_removing_messages(cx: &mut App) { ); // Ensure we can still insert after a merged message. - let message_5 = context.update(cx, |context, cx| { + let message_5 = text_thread.update(cx, |context, cx| { context .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..3), (message_5.id, Role::System, 3..4), @@ -186,8 +186,8 @@ fn test_message_splitting(cx: &mut App) { let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new(|cx| { - AssistantContext::local( + let text_thread = cx.new(|cx| { + TextThread::local( registry.clone(), None, None, @@ -196,11 +196,11 @@ fn test_message_splitting(cx: &mut App) { cx, ) }); - let buffer = context.read(cx).buffer.clone(); + let buffer = text_thread.read(cx).buffer().clone(); - let message_1 = context.read(cx).message_anchors[0].clone(); + let message_1 = text_thread.read(cx).message_anchors[0].clone(); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![(message_1.id, Role::User, 0..0)] ); @@ -208,26 +208,28 @@ fn test_message_splitting(cx: &mut App) { buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx) }); - let (_, message_2) = context.update(cx, |context, cx| context.split_message(3..3, cx)); + let (_, message_2) = + text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx)); let message_2 = message_2.unwrap(); // We recycle newlines in the middle of a split message assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_2.id, Role::User, 4..16), ] ); - let (_, message_3) = context.update(cx, |context, cx| context.split_message(3..3, cx)); + let (_, message_3) = + text_thread.update(cx, |text_thread, cx| text_thread.split_message(3..3, cx)); let message_3 = message_3.unwrap(); // We don't recycle newlines at the end of a split message assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_3.id, Role::User, 4..5), @@ -235,11 +237,12 @@ fn test_message_splitting(cx: &mut App) { ] ); - let (_, message_4) = context.update(cx, |context, cx| context.split_message(9..9, cx)); + let (_, message_4) = + text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx)); let message_4 = message_4.unwrap(); assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_3.id, Role::User, 4..5), @@ -248,11 +251,12 @@ fn test_message_splitting(cx: &mut App) { ] ); - let (_, message_5) = context.update(cx, |context, cx| context.split_message(9..9, cx)); + let (_, message_5) = + text_thread.update(cx, |text_thread, cx| text_thread.split_message(9..9, cx)); let message_5 = message_5.unwrap(); assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_3.id, Role::User, 4..5), @@ -263,12 +267,12 @@ fn test_message_splitting(cx: &mut App) { ); let (message_6, message_7) = - context.update(cx, |context, cx| context.split_message(14..16, cx)); + text_thread.update(cx, |text_thread, cx| text_thread.split_message(14..16, cx)); let message_6 = message_6.unwrap(); let message_7 = message_7.unwrap(); assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_3.id, Role::User, 4..5), @@ -287,8 +291,8 @@ fn test_messages_for_offsets(cx: &mut App) { let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new(|cx| { - AssistantContext::local( + let text_thread = cx.new(|cx| { + TextThread::local( registry, None, None, @@ -297,32 +301,32 @@ fn test_messages_for_offsets(cx: &mut App) { cx, ) }); - let buffer = context.read(cx).buffer.clone(); + let buffer = text_thread.read(cx).buffer().clone(); - let message_1 = context.read(cx).message_anchors[0].clone(); + let message_1 = text_thread.read(cx).message_anchors[0].clone(); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![(message_1.id, Role::User, 0..0)] ); buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); - let message_2 = context - .update(cx, |context, cx| { - context.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) + let message_2 = text_thread + .update(cx, |text_thread, cx| { + text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) }) .unwrap(); buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx)); - let message_3 = context - .update(cx, |context, cx| { - context.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + let message_3 = text_thread + .update(cx, |text_thread, cx| { + text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) }) .unwrap(); buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx)); assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_2.id, Role::User, 4..8), @@ -331,22 +335,22 @@ fn test_messages_for_offsets(cx: &mut App) { ); assert_eq!( - message_ids_for_offsets(&context, &[0, 4, 9], cx), + message_ids_for_offsets(&text_thread, &[0, 4, 9], cx), [message_1.id, message_2.id, message_3.id] ); assert_eq!( - message_ids_for_offsets(&context, &[0, 1, 11], cx), + message_ids_for_offsets(&text_thread, &[0, 1, 11], cx), [message_1.id, message_3.id] ); - let message_4 = context - .update(cx, |context, cx| { - context.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) + let message_4 = text_thread + .update(cx, |text_thread, cx| { + text_thread.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) }) .unwrap(); assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n"); assert_eq!( - messages(&context, cx), + messages(&text_thread, cx), vec![ (message_1.id, Role::User, 0..4), (message_2.id, Role::User, 4..8), @@ -355,12 +359,12 @@ fn test_messages_for_offsets(cx: &mut App) { ] ); assert_eq!( - message_ids_for_offsets(&context, &[0, 4, 8, 12], cx), + message_ids_for_offsets(&text_thread, &[0, 4, 8, 12], cx), [message_1.id, message_2.id, message_3.id, message_4.id] ); fn message_ids_for_offsets( - context: &Entity, + context: &Entity, offsets: &[usize], cx: &App, ) -> Vec { @@ -398,8 +402,8 @@ async fn test_slash_commands(cx: &mut TestAppContext) { let registry = Arc::new(LanguageRegistry::test(cx.executor())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new(|cx| { - AssistantContext::local( + let text_thread = cx.new(|cx| { + TextThread::local( registry.clone(), None, None, @@ -417,19 +421,19 @@ async fn test_slash_commands(cx: &mut TestAppContext) { } let context_ranges = Rc::new(RefCell::new(ContextRanges::default())); - context.update(cx, |_, cx| { - cx.subscribe(&context, { + text_thread.update(cx, |_, cx| { + cx.subscribe(&text_thread, { let context_ranges = context_ranges.clone(); - move |context, _, event, _| { + move |text_thread, _, event, _| { let mut context_ranges = context_ranges.borrow_mut(); match event { - ContextEvent::InvokedSlashCommandChanged { command_id } => { - let command = context.invoked_slash_command(command_id).unwrap(); + TextThreadEvent::InvokedSlashCommandChanged { command_id } => { + let command = text_thread.invoked_slash_command(command_id).unwrap(); context_ranges .command_outputs .insert(*command_id, command.range.clone()); } - ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => { + TextThreadEvent::ParsedSlashCommandsUpdated { removed, updated } => { for range in removed { context_ranges.parsed_commands.remove(range); } @@ -439,7 +443,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { .insert(command.source_range.clone()); } } - ContextEvent::SlashCommandOutputSectionAdded { section } => { + TextThreadEvent::SlashCommandOutputSectionAdded { section } => { context_ranges.output_sections.insert(section.range.clone()); } _ => {} @@ -449,7 +453,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) { .detach(); }); - let buffer = context.read_with(cx, |context, _| context.buffer.clone()); + let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone()); // Insert a slash command buffer.update(cx, |buffer, cx| { @@ -508,9 +512,9 @@ async fn test_slash_commands(cx: &mut TestAppContext) { ); let (command_output_tx, command_output_rx) = mpsc::unbounded(); - context.update(cx, |context, cx| { - let command_source_range = context.parsed_slash_commands[0].source_range.clone(); - context.insert_command_output( + text_thread.update(cx, |text_thread, cx| { + let command_source_range = text_thread.parsed_slash_commands[0].source_range.clone(); + text_thread.insert_command_output( command_source_range, "file", Task::ready(Ok(command_output_rx.boxed())), @@ -670,8 +674,8 @@ async fn test_serialization(cx: &mut TestAppContext) { let registry = Arc::new(LanguageRegistry::test(cx.executor())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new(|cx| { - AssistantContext::local( + let text_thread = cx.new(|cx| { + TextThread::local( registry.clone(), None, None, @@ -680,15 +684,15 @@ async fn test_serialization(cx: &mut TestAppContext) { cx, ) }); - let buffer = context.read_with(cx, |context, _| context.buffer.clone()); - let message_0 = context.read_with(cx, |context, _| context.message_anchors[0].id); - let message_1 = context.update(cx, |context, cx| { - context + let buffer = text_thread.read_with(cx, |text_thread, _| text_thread.buffer().clone()); + let message_0 = text_thread.read_with(cx, |text_thread, _| text_thread.message_anchors[0].id); + let message_1 = text_thread.update(cx, |text_thread, cx| { + text_thread .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx) .unwrap() }); - let message_2 = context.update(cx, |context, cx| { - context + let message_2 = text_thread.update(cx, |text_thread, cx| { + text_thread .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) .unwrap() }); @@ -696,15 +700,15 @@ async fn test_serialization(cx: &mut TestAppContext) { buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx); buffer.finalize_last_transaction(); }); - let _message_3 = context.update(cx, |context, cx| { - context + let _message_3 = text_thread.update(cx, |text_thread, cx| { + text_thread .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx) .unwrap() }); buffer.update(cx, |buffer, cx| buffer.undo(cx)); assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n"); assert_eq!( - cx.read(|cx| messages(&context, cx)), + cx.read(|cx| messages(&text_thread, cx)), [ (message_0, Role::User, 0..2), (message_1.id, Role::Assistant, 2..6), @@ -712,9 +716,9 @@ async fn test_serialization(cx: &mut TestAppContext) { ] ); - let serialized_context = context.read_with(cx, |context, cx| context.serialize(cx)); + let serialized_context = text_thread.read_with(cx, |text_thread, cx| text_thread.serialize(cx)); let deserialized_context = cx.new(|cx| { - AssistantContext::deserialize( + TextThread::deserialize( serialized_context, Path::new("").into(), registry.clone(), @@ -726,7 +730,7 @@ async fn test_serialization(cx: &mut TestAppContext) { ) }); let deserialized_buffer = - deserialized_context.read_with(cx, |context, _| context.buffer.clone()); + deserialized_context.read_with(cx, |text_thread, _| text_thread.buffer().clone()); assert_eq!( deserialized_buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n" @@ -762,14 +766,14 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let registry = Arc::new(LanguageRegistry::test(cx.background_executor.clone())); let network = Arc::new(Mutex::new(Network::new(rng.clone()))); - let mut contexts = Vec::new(); + let mut text_threads = Vec::new(); let num_peers = rng.random_range(min_peers..=max_peers); - let context_id = ContextId::new(); + let context_id = TextThreadId::new(); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); for i in 0..num_peers { let context = cx.new(|cx| { - AssistantContext::new( + TextThread::new( context_id.clone(), ReplicaId::new(i as u16), language::Capability::ReadWrite, @@ -786,7 +790,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std cx.subscribe(&context, { let network = network.clone(); move |_, event, _| { - if let ContextEvent::Operation(op) = event { + if let TextThreadEvent::Operation(op) = event { network .lock() .broadcast(ReplicaId::new(i as u16), vec![op.to_proto()]); @@ -796,7 +800,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std .detach(); }); - contexts.push(context); + text_threads.push(context); network.lock().add_peer(ReplicaId::new(i as u16)); } @@ -806,30 +810,30 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std || !network.lock().is_idle() || network.lock().contains_disconnected_peers() { - let context_index = rng.random_range(0..contexts.len()); - let context = &contexts[context_index]; + let context_index = rng.random_range(0..text_threads.len()); + let text_thread = &text_threads[context_index]; match rng.random_range(0..100) { 0..=29 if mutation_count > 0 => { log::info!("Context {}: edit buffer", context_index); - context.update(cx, |context, cx| { - context - .buffer + text_thread.update(cx, |text_thread, cx| { + text_thread + .buffer() .update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx)); }); mutation_count -= 1; } 30..=44 if mutation_count > 0 => { - context.update(cx, |context, cx| { - let range = context.buffer.read(cx).random_byte_range(0, &mut rng); + text_thread.update(cx, |text_thread, cx| { + let range = text_thread.buffer().read(cx).random_byte_range(0, &mut rng); log::info!("Context {}: split message at {:?}", context_index, range); - context.split_message(range, cx); + text_thread.split_message(range, cx); }); mutation_count -= 1; } 45..=59 if mutation_count > 0 => { - context.update(cx, |context, cx| { - if let Some(message) = context.messages(cx).choose(&mut rng) { + text_thread.update(cx, |text_thread, cx| { + if let Some(message) = text_thread.messages(cx).choose(&mut rng) { let role = *[Role::User, Role::Assistant, Role::System] .choose(&mut rng) .unwrap(); @@ -839,13 +843,13 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std message.id, role ); - context.insert_message_after(message.id, role, MessageStatus::Done, cx); + text_thread.insert_message_after(message.id, role, MessageStatus::Done, cx); } }); mutation_count -= 1; } 60..=74 if mutation_count > 0 => { - context.update(cx, |context, cx| { + text_thread.update(cx, |text_thread, cx| { let command_text = "/".to_string() + slash_commands .command_names() @@ -854,7 +858,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std .clone() .as_ref(); - let command_range = context.buffer.update(cx, |buffer, cx| { + let command_range = text_thread.buffer().update(cx, |buffer, cx| { let offset = buffer.random_byte_range(0, &mut rng).start; buffer.edit( [(offset..offset, format!("\n{}\n", command_text))], @@ -908,9 +912,15 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std events.len() ); - let command_range = context.buffer.read(cx).anchor_after(command_range.start) - ..context.buffer.read(cx).anchor_after(command_range.end); - context.insert_command_output( + let command_range = text_thread + .buffer() + .read(cx) + .anchor_after(command_range.start) + ..text_thread + .buffer() + .read(cx) + .anchor_after(command_range.end); + text_thread.insert_command_output( command_range, "/command", Task::ready(Ok(stream::iter(events).boxed())), @@ -922,8 +932,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std mutation_count -= 1; } 75..=84 if mutation_count > 0 => { - context.update(cx, |context, cx| { - if let Some(message) = context.messages(cx).choose(&mut rng) { + text_thread.update(cx, |text_thread, cx| { + if let Some(message) = text_thread.messages(cx).choose(&mut rng) { let new_status = match rng.random_range(0..3) { 0 => MessageStatus::Done, 1 => MessageStatus::Pending, @@ -935,7 +945,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std message.id, new_status ); - context.update_metadata(message.id, cx, |metadata| { + text_thread.update_metadata(message.id, cx, |metadata| { metadata.status = new_status; }); } @@ -948,8 +958,8 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std network.lock().reconnect_peer(replica_id, ReplicaId::new(0)); let (ops_to_send, ops_to_receive) = cx.read(|cx| { - let host_context = &contexts[0].read(cx); - let guest_context = context.read(cx); + let host_context = &text_threads[0].read(cx); + let guest_context = text_thread.read(cx); ( guest_context.serialize_ops(&host_context.version(cx), cx), host_context.serialize_ops(&guest_context.version(cx), cx), @@ -959,7 +969,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let ops_to_receive = ops_to_receive .await .into_iter() - .map(ContextOperation::from_proto) + .map(TextThreadOperation::from_proto) .collect::>>() .unwrap(); log::info!( @@ -970,7 +980,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std ); network.lock().broadcast(replica_id, ops_to_send); - context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx)); + text_thread.update(cx, |text_thread, cx| { + text_thread.apply_ops(ops_to_receive, cx) + }); } else if rng.random_bool(0.1) && replica_id != ReplicaId::new(0) { log::info!("Context {}: disconnecting", context_index); network.lock().disconnect_peer(replica_id); @@ -979,43 +991,43 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std let ops = network.lock().receive(replica_id); let ops = ops .into_iter() - .map(ContextOperation::from_proto) + .map(TextThreadOperation::from_proto) .collect::>>() .unwrap(); - context.update(cx, |context, cx| context.apply_ops(ops, cx)); + text_thread.update(cx, |text_thread, cx| text_thread.apply_ops(ops, cx)); } } } } cx.read(|cx| { - let first_context = contexts[0].read(cx); - for context in &contexts[1..] { - let context = context.read(cx); - assert!(context.pending_ops.is_empty(), "pending ops: {:?}", context.pending_ops); + let first_context = text_threads[0].read(cx); + for text_thread in &text_threads[1..] { + let text_thread = text_thread.read(cx); + assert!(text_thread.pending_ops.is_empty(), "pending ops: {:?}", text_thread.pending_ops); assert_eq!( - context.buffer.read(cx).text(), - first_context.buffer.read(cx).text(), + text_thread.buffer().read(cx).text(), + first_context.buffer().read(cx).text(), "Context {:?} text != Context 0 text", - context.buffer.read(cx).replica_id() + text_thread.buffer().read(cx).replica_id() ); assert_eq!( - context.message_anchors, + text_thread.message_anchors, first_context.message_anchors, "Context {:?} messages != Context 0 messages", - context.buffer.read(cx).replica_id() + text_thread.buffer().read(cx).replica_id() ); assert_eq!( - context.messages_metadata, + text_thread.messages_metadata, first_context.messages_metadata, "Context {:?} message metadata != Context 0 message metadata", - context.buffer.read(cx).replica_id() + text_thread.buffer().read(cx).replica_id() ); assert_eq!( - context.slash_command_output_sections, + text_thread.slash_command_output_sections, first_context.slash_command_output_sections, "Context {:?} slash command output sections != Context 0 slash command output sections", - context.buffer.read(cx).replica_id() + text_thread.buffer().read(cx).replica_id() ); } }); @@ -1027,8 +1039,8 @@ fn test_mark_cache_anchors(cx: &mut App) { let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context = cx.new(|cx| { - AssistantContext::local( + let text_thread = cx.new(|cx| { + TextThread::local( registry, None, None, @@ -1037,7 +1049,7 @@ fn test_mark_cache_anchors(cx: &mut App) { cx, ) }); - let buffer = context.read(cx).buffer.clone(); + let buffer = text_thread.read(cx).buffer().clone(); // Create a test cache configuration let cache_configuration = &Some(LanguageModelCacheConfiguration { @@ -1046,14 +1058,14 @@ fn test_mark_cache_anchors(cx: &mut App) { min_total_token: 10, }); - let message_1 = context.read(cx).message_anchors[0].clone(); + let message_1 = text_thread.read(cx).message_anchors[0].clone(); - context.update(cx, |context, cx| { - context.mark_cache_anchors(cache_configuration, false, cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.mark_cache_anchors(cache_configuration, false, cx) }); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .count(), @@ -1062,41 +1074,41 @@ fn test_mark_cache_anchors(cx: &mut App) { ); buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); - let message_2 = context - .update(cx, |context, cx| { - context.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx) + let message_2 = text_thread + .update(cx, |text_thread, cx| { + text_thread.insert_message_after(message_1.id, Role::User, MessageStatus::Pending, cx) }) .unwrap(); buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbbbbbb")], None, cx)); - let message_3 = context - .update(cx, |context, cx| { - context.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx) + let message_3 = text_thread + .update(cx, |text_thread, cx| { + text_thread.insert_message_after(message_2.id, Role::User, MessageStatus::Pending, cx) }) .unwrap(); buffer.update(cx, |buffer, cx| buffer.edit([(12..12, "cccccc")], None, cx)); - context.update(cx, |context, cx| { - context.mark_cache_anchors(cache_configuration, false, cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.mark_cache_anchors(cache_configuration, false, cx) }); assert_eq!(buffer.read(cx).text(), "aaa\nbbbbbbb\ncccccc"); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .filter(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .count(), 0, "Messages should not be marked for cache before going over the token minimum." ); - context.update(cx, |context, _| { - context.token_count = Some(20); + text_thread.update(cx, |text_thread, _| { + text_thread.token_count = Some(20); }); - context.update(cx, |context, cx| { - context.mark_cache_anchors(cache_configuration, true, cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.mark_cache_anchors(cache_configuration, true, cx) }); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .collect::>(), @@ -1104,28 +1116,33 @@ fn test_mark_cache_anchors(cx: &mut App) { "Last message should not be an anchor on speculative request." ); - context - .update(cx, |context, cx| { - context.insert_message_after(message_3.id, Role::Assistant, MessageStatus::Pending, cx) + text_thread + .update(cx, |text_thread, cx| { + text_thread.insert_message_after( + message_3.id, + Role::Assistant, + MessageStatus::Pending, + cx, + ) }) .unwrap(); - context.update(cx, |context, cx| { - context.mark_cache_anchors(cache_configuration, false, cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.mark_cache_anchors(cache_configuration, false, cx) }); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .map(|(_, cache)| cache.as_ref().is_some_and(|cache| cache.is_anchor)) .collect::>(), vec![false, true, true, false], "Most recent message should also be cached if not a speculative request." ); - context.update(cx, |context, cx| { - context.update_cache_status_for_completion(cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.update_cache_status_for_completion(cx) }); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .map(|(_, cache)| cache .as_ref() @@ -1141,11 +1158,11 @@ fn test_mark_cache_anchors(cx: &mut App) { ); buffer.update(cx, |buffer, cx| buffer.edit([(14..14, "d")], None, cx)); - context.update(cx, |context, cx| { - context.mark_cache_anchors(cache_configuration, false, cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.mark_cache_anchors(cache_configuration, false, cx) }); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .map(|(_, cache)| cache .as_ref() @@ -1160,11 +1177,11 @@ fn test_mark_cache_anchors(cx: &mut App) { "Modifying a message should invalidate it's cache but leave previous messages." ); buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "e")], None, cx)); - context.update(cx, |context, cx| { - context.mark_cache_anchors(cache_configuration, false, cx) + text_thread.update(cx, |text_thread, cx| { + text_thread.mark_cache_anchors(cache_configuration, false, cx) }); assert_eq!( - messages_cache(&context, cx) + messages_cache(&text_thread, cx) .iter() .map(|(_, cache)| cache .as_ref() @@ -1182,31 +1199,36 @@ fn test_mark_cache_anchors(cx: &mut App) { #[gpui::test] async fn test_summarization(cx: &mut TestAppContext) { - let (context, fake_model) = setup_context_editor_with_fake_model(cx); + let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx); // Initial state should be pending - context.read_with(cx, |context, _| { - assert!(matches!(context.summary(), ContextSummary::Pending)); - assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT); + text_thread.read_with(cx, |text_thread, _| { + assert!(matches!(text_thread.summary(), TextThreadSummary::Pending)); + assert_eq!( + text_thread.summary().or_default(), + TextThreadSummary::DEFAULT + ); }); - let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone()); - context.update(cx, |context, cx| { + let message_1 = text_thread.read_with(cx, |text_thread, _cx| { + text_thread.message_anchors[0].clone() + }); + text_thread.update(cx, |context, cx| { context .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) .unwrap(); }); // Send a message - context.update(cx, |context, cx| { - context.assist(cx); + text_thread.update(cx, |text_thread, cx| { + text_thread.assist(cx); }); simulate_successful_response(&fake_model, cx); // Should start generating summary when there are >= 2 messages - context.read_with(cx, |context, _| { - assert!(!context.summary().content().unwrap().done); + text_thread.read_with(cx, |text_thread, _| { + assert!(!text_thread.summary().content().unwrap().done); }); cx.run_until_parked(); @@ -1216,61 +1238,61 @@ async fn test_summarization(cx: &mut TestAppContext) { cx.run_until_parked(); // Summary should be set - context.read_with(cx, |context, _| { - assert_eq!(context.summary().or_default(), "Brief Introduction"); + text_thread.read_with(cx, |text_thread, _| { + assert_eq!(text_thread.summary().or_default(), "Brief Introduction"); }); // We should be able to manually set a summary - context.update(cx, |context, cx| { - context.set_custom_summary("Brief Intro".into(), cx); + text_thread.update(cx, |text_thread, cx| { + text_thread.set_custom_summary("Brief Intro".into(), cx); }); - context.read_with(cx, |context, _| { - assert_eq!(context.summary().or_default(), "Brief Intro"); + text_thread.read_with(cx, |text_thread, _| { + assert_eq!(text_thread.summary().or_default(), "Brief Intro"); }); } #[gpui::test] async fn test_thread_summary_error_set_manually(cx: &mut TestAppContext) { - let (context, fake_model) = setup_context_editor_with_fake_model(cx); + let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx); - test_summarize_error(&fake_model, &context, cx); + test_summarize_error(&fake_model, &text_thread, cx); // Now we should be able to set a summary - context.update(cx, |context, cx| { - context.set_custom_summary("Brief Intro".into(), cx); + text_thread.update(cx, |text_thread, cx| { + text_thread.set_custom_summary("Brief Intro".into(), cx); }); - context.read_with(cx, |context, _| { - assert_eq!(context.summary().or_default(), "Brief Intro"); + text_thread.read_with(cx, |text_thread, _| { + assert_eq!(text_thread.summary().or_default(), "Brief Intro"); }); } #[gpui::test] async fn test_thread_summary_error_retry(cx: &mut TestAppContext) { - let (context, fake_model) = setup_context_editor_with_fake_model(cx); + let (text_thread, fake_model) = setup_context_editor_with_fake_model(cx); - test_summarize_error(&fake_model, &context, cx); + test_summarize_error(&fake_model, &text_thread, cx); // Sending another message should not trigger another summarize request - context.update(cx, |context, cx| { - context.assist(cx); + text_thread.update(cx, |text_thread, cx| { + text_thread.assist(cx); }); simulate_successful_response(&fake_model, cx); - context.read_with(cx, |context, _| { + text_thread.read_with(cx, |text_thread, _| { // State is still Error, not Generating - assert!(matches!(context.summary(), ContextSummary::Error)); + assert!(matches!(text_thread.summary(), TextThreadSummary::Error)); }); // But the summarize request can be invoked manually - context.update(cx, |context, cx| { - context.summarize(true, cx); + text_thread.update(cx, |text_thread, cx| { + text_thread.summarize(true, cx); }); - context.read_with(cx, |context, _| { - assert!(!context.summary().content().unwrap().done); + text_thread.read_with(cx, |text_thread, _| { + assert!(!text_thread.summary().content().unwrap().done); }); cx.run_until_parked(); @@ -1278,32 +1300,34 @@ async fn test_thread_summary_error_retry(cx: &mut TestAppContext) { fake_model.end_last_completion_stream(); cx.run_until_parked(); - context.read_with(cx, |context, _| { - assert_eq!(context.summary().or_default(), "A successful summary"); + text_thread.read_with(cx, |text_thread, _| { + assert_eq!(text_thread.summary().or_default(), "A successful summary"); }); } fn test_summarize_error( model: &Arc, - context: &Entity, + text_thread: &Entity, cx: &mut TestAppContext, ) { - let message_1 = context.read_with(cx, |context, _cx| context.message_anchors[0].clone()); - context.update(cx, |context, cx| { - context + let message_1 = text_thread.read_with(cx, |text_thread, _cx| { + text_thread.message_anchors[0].clone() + }); + text_thread.update(cx, |text_thread, cx| { + text_thread .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) .unwrap(); }); // Send a message - context.update(cx, |context, cx| { - context.assist(cx); + text_thread.update(cx, |text_thread, cx| { + text_thread.assist(cx); }); simulate_successful_response(model, cx); - context.read_with(cx, |context, _| { - assert!(!context.summary().content().unwrap().done); + text_thread.read_with(cx, |text_thread, _| { + assert!(!text_thread.summary().content().unwrap().done); }); // Simulate summary request ending @@ -1312,15 +1336,18 @@ fn test_summarize_error( cx.run_until_parked(); // State is set to Error and default message - context.read_with(cx, |context, _| { - assert_eq!(*context.summary(), ContextSummary::Error); - assert_eq!(context.summary().or_default(), ContextSummary::DEFAULT); + text_thread.read_with(cx, |text_thread, _| { + assert_eq!(*text_thread.summary(), TextThreadSummary::Error); + assert_eq!( + text_thread.summary().or_default(), + TextThreadSummary::DEFAULT + ); }); } fn setup_context_editor_with_fake_model( cx: &mut TestAppContext, -) -> (Entity, Arc) { +) -> (Entity, Arc) { let registry = Arc::new(LanguageRegistry::test(cx.executor())); let fake_provider = Arc::new(FakeLanguageModelProvider::default()); @@ -1340,7 +1367,7 @@ fn setup_context_editor_with_fake_model( let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); let context = cx.new(|cx| { - AssistantContext::local( + TextThread::local( registry, None, None, @@ -1360,7 +1387,7 @@ fn simulate_successful_response(fake_model: &FakeLanguageModel, cx: &mut TestApp cx.run_until_parked(); } -fn messages(context: &Entity, cx: &App) -> Vec<(MessageId, Role, Range)> { +fn messages(context: &Entity, cx: &App) -> Vec<(MessageId, Role, Range)> { context .read(cx) .messages(cx) @@ -1369,7 +1396,7 @@ fn messages(context: &Entity, cx: &App) -> Vec<(MessageId, Rol } fn messages_cache( - context: &Entity, + context: &Entity, cx: &App, ) -> Vec<(MessageId, Option)> { context diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_text_thread/src/text_thread.rs similarity index 92% rename from crates/assistant_context/src/assistant_context.rs rename to crates/assistant_text_thread/src/text_thread.rs index 5a1fa707ff04ac3b0cd719c3d0a5e67dfeb3e625..9ad383cdfd43eed236268349e2ff97c34a0178c0 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_text_thread/src/text_thread.rs @@ -1,7 +1,3 @@ -#[cfg(test)] -mod assistant_context_tests; -mod context_store; - use agent_settings::{AgentSettings, SUMMARIZE_THREAD_PROMPT}; use anyhow::{Context as _, Result, bail}; use assistant_slash_command::{ @@ -9,7 +5,7 @@ use assistant_slash_command::{ SlashCommandResult, SlashCommandWorkingSet, }; use assistant_slash_commands::FileCommandMetadata; -use client::{self, Client, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry}; +use client::{self, ModelRequestUsage, RequestUsage, proto, telemetry::Telemetry}; use clock::ReplicaId; use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::{HashMap, HashSet}; @@ -27,7 +23,7 @@ use language_model::{ report_assistant_event, }; use open_ai::Model as OpenAiModel; -use paths::contexts_dir; +use paths::text_threads_dir; use project::Project; use prompt_store::PromptBuilder; use serde::{Deserialize, Serialize}; @@ -48,16 +44,10 @@ use ui::IconName; use util::{ResultExt, TryFutureExt, post_inc}; use uuid::Uuid; -pub use crate::context_store::*; - -pub fn init(client: Arc, _: &mut App) { - context_store::init(&client.into()); -} - #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] -pub struct ContextId(String); +pub struct TextThreadId(String); -impl ContextId { +impl TextThreadId { pub fn new() -> Self { Self(Uuid::new_v4().to_string()) } @@ -130,7 +120,7 @@ impl MessageStatus { } #[derive(Clone, Debug)] -pub enum ContextOperation { +pub enum TextThreadOperation { InsertMessage { anchor: MessageAnchor, metadata: MessageMetadata, @@ -142,7 +132,7 @@ pub enum ContextOperation { version: clock::Global, }, UpdateSummary { - summary: ContextSummaryContent, + summary: TextThreadSummaryContent, version: clock::Global, }, SlashCommandStarted { @@ -170,7 +160,7 @@ pub enum ContextOperation { BufferOperation(language::Operation), } -impl ContextOperation { +impl TextThreadOperation { pub fn from_proto(op: proto::ContextOperation) -> Result { match op.variant.context("invalid variant")? { proto::context_operation::Variant::InsertMessage(insert) => { @@ -212,7 +202,7 @@ impl ContextOperation { version: language::proto::deserialize_version(&update.version), }), proto::context_operation::Variant::UpdateSummary(update) => Ok(Self::UpdateSummary { - summary: ContextSummaryContent { + summary: TextThreadSummaryContent { text: update.summary, done: update.done, timestamp: language::proto::deserialize_timestamp( @@ -453,7 +443,7 @@ impl ContextOperation { } #[derive(Debug, Clone)] -pub enum ContextEvent { +pub enum TextThreadEvent { ShowAssistError(SharedString), ShowPaymentRequiredError, MessagesEdited, @@ -476,24 +466,24 @@ pub enum ContextEvent { SlashCommandOutputSectionAdded { section: SlashCommandOutputSection, }, - Operation(ContextOperation), + Operation(TextThreadOperation), } #[derive(Clone, Debug, Eq, PartialEq)] -pub enum ContextSummary { +pub enum TextThreadSummary { Pending, - Content(ContextSummaryContent), + Content(TextThreadSummaryContent), Error, } #[derive(Clone, Debug, Eq, PartialEq)] -pub struct ContextSummaryContent { +pub struct TextThreadSummaryContent { pub text: String, pub done: bool, pub timestamp: clock::Lamport, } -impl ContextSummary { +impl TextThreadSummary { pub const DEFAULT: &str = "New Text Thread"; pub fn or_default(&self) -> SharedString { @@ -505,48 +495,48 @@ impl ContextSummary { .map_or_else(|| message.into(), |content| content.text.clone().into()) } - pub fn content(&self) -> Option<&ContextSummaryContent> { + pub fn content(&self) -> Option<&TextThreadSummaryContent> { match self { - ContextSummary::Content(content) => Some(content), - ContextSummary::Pending | ContextSummary::Error => None, + TextThreadSummary::Content(content) => Some(content), + TextThreadSummary::Pending | TextThreadSummary::Error => None, } } - fn content_as_mut(&mut self) -> Option<&mut ContextSummaryContent> { + fn content_as_mut(&mut self) -> Option<&mut TextThreadSummaryContent> { match self { - ContextSummary::Content(content) => Some(content), - ContextSummary::Pending | ContextSummary::Error => None, + TextThreadSummary::Content(content) => Some(content), + TextThreadSummary::Pending | TextThreadSummary::Error => None, } } - fn content_or_set_empty(&mut self) -> &mut ContextSummaryContent { + fn content_or_set_empty(&mut self) -> &mut TextThreadSummaryContent { match self { - ContextSummary::Content(content) => content, - ContextSummary::Pending | ContextSummary::Error => { - let content = ContextSummaryContent { + TextThreadSummary::Content(content) => content, + TextThreadSummary::Pending | TextThreadSummary::Error => { + let content = TextThreadSummaryContent { text: "".to_string(), done: false, timestamp: clock::Lamport::MIN, }; - *self = ContextSummary::Content(content); + *self = TextThreadSummary::Content(content); self.content_as_mut().unwrap() } } } pub fn is_pending(&self) -> bool { - matches!(self, ContextSummary::Pending) + matches!(self, TextThreadSummary::Pending) } fn timestamp(&self) -> Option { match self { - ContextSummary::Content(content) => Some(content.timestamp), - ContextSummary::Pending | ContextSummary::Error => None, + TextThreadSummary::Content(content) => Some(content.timestamp), + TextThreadSummary::Pending | TextThreadSummary::Error => None, } } } -impl PartialOrd for ContextSummary { +impl PartialOrd for TextThreadSummary { fn partial_cmp(&self, other: &Self) -> Option { self.timestamp().partial_cmp(&other.timestamp()) } @@ -668,27 +658,27 @@ struct PendingCompletion { #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] pub struct InvokedSlashCommandId(clock::Lamport); -pub struct AssistantContext { - id: ContextId, +pub struct TextThread { + id: TextThreadId, timestamp: clock::Lamport, version: clock::Global, - pending_ops: Vec, - operations: Vec, + pub(crate) pending_ops: Vec, + operations: Vec, buffer: Entity, - parsed_slash_commands: Vec, + pub(crate) parsed_slash_commands: Vec, invoked_slash_commands: HashMap, edits_since_last_parse: language::Subscription, slash_commands: Arc, - slash_command_output_sections: Vec>, + pub(crate) slash_command_output_sections: Vec>, thought_process_output_sections: Vec>, - message_anchors: Vec, + pub(crate) message_anchors: Vec, contents: Vec, - messages_metadata: HashMap, - summary: ContextSummary, + pub(crate) messages_metadata: HashMap, + summary: TextThreadSummary, summary_task: Task>, completion_count: usize, pending_completions: Vec, - token_count: Option, + pub(crate) token_count: Option, pending_token_count: Task>, pending_save: Task>, pending_cache_warming_task: Task>, @@ -711,9 +701,9 @@ impl ContextAnnotation for ParsedSlashCommand { } } -impl EventEmitter for AssistantContext {} +impl EventEmitter for TextThread {} -impl AssistantContext { +impl TextThread { pub fn local( language_registry: Arc, project: Option>, @@ -723,7 +713,7 @@ impl AssistantContext { cx: &mut Context, ) -> Self { Self::new( - ContextId::new(), + TextThreadId::new(), ReplicaId::default(), language::Capability::ReadWrite, language_registry, @@ -744,7 +734,7 @@ impl AssistantContext { } pub fn new( - id: ContextId, + id: TextThreadId, replica_id: ReplicaId, capability: language::Capability, language_registry: Arc, @@ -780,7 +770,7 @@ impl AssistantContext { slash_command_output_sections: Vec::new(), thought_process_output_sections: Vec::new(), edits_since_last_parse: edits_since_last_slash_command_parse, - summary: ContextSummary::Pending, + summary: TextThreadSummary::Pending, summary_task: Task::ready(None), completion_count: Default::default(), pending_completions: Default::default(), @@ -823,12 +813,12 @@ impl AssistantContext { this } - pub(crate) fn serialize(&self, cx: &App) -> SavedContext { + pub(crate) fn serialize(&self, cx: &App) -> SavedTextThread { let buffer = self.buffer.read(cx); - SavedContext { + SavedTextThread { id: Some(self.id.clone()), zed: "context".into(), - version: SavedContext::VERSION.into(), + version: SavedTextThread::VERSION.into(), text: buffer.text(), messages: self .messages(cx) @@ -876,7 +866,7 @@ impl AssistantContext { } pub fn deserialize( - saved_context: SavedContext, + saved_context: SavedTextThread, path: Arc, language_registry: Arc, prompt_builder: Arc, @@ -885,7 +875,7 @@ impl AssistantContext { telemetry: Option>, cx: &mut Context, ) -> Self { - let id = saved_context.id.clone().unwrap_or_else(ContextId::new); + let id = saved_context.id.clone().unwrap_or_else(TextThreadId::new); let mut this = Self::new( id, ReplicaId::default(), @@ -906,7 +896,7 @@ impl AssistantContext { this } - pub fn id(&self) -> &ContextId { + pub fn id(&self) -> &TextThreadId { &self.id } @@ -914,9 +904,9 @@ impl AssistantContext { self.timestamp.replica_id } - pub fn version(&self, cx: &App) -> ContextVersion { - ContextVersion { - context: self.version.clone(), + pub fn version(&self, cx: &App) -> TextThreadVersion { + TextThreadVersion { + text_thread: self.version.clone(), buffer: self.buffer.read(cx).version(), } } @@ -938,7 +928,7 @@ impl AssistantContext { pub fn serialize_ops( &self, - since: &ContextVersion, + since: &TextThreadVersion, cx: &App, ) -> Task> { let buffer_ops = self @@ -949,7 +939,7 @@ impl AssistantContext { let mut context_ops = self .operations .iter() - .filter(|op| !since.context.observed(op.timestamp())) + .filter(|op| !since.text_thread.observed(op.timestamp())) .cloned() .collect::>(); context_ops.extend(self.pending_ops.iter().cloned()); @@ -973,13 +963,13 @@ impl AssistantContext { pub fn apply_ops( &mut self, - ops: impl IntoIterator, + ops: impl IntoIterator, cx: &mut Context, ) { let mut buffer_ops = Vec::new(); for op in ops { match op { - ContextOperation::BufferOperation(buffer_op) => buffer_ops.push(buffer_op), + TextThreadOperation::BufferOperation(buffer_op) => buffer_ops.push(buffer_op), op @ _ => self.pending_ops.push(op), } } @@ -988,7 +978,7 @@ impl AssistantContext { self.flush_ops(cx); } - fn flush_ops(&mut self, cx: &mut Context) { + fn flush_ops(&mut self, cx: &mut Context) { let mut changed_messages = HashSet::default(); let mut summary_generated = false; @@ -1001,7 +991,7 @@ impl AssistantContext { let timestamp = op.timestamp(); match op.clone() { - ContextOperation::InsertMessage { + TextThreadOperation::InsertMessage { anchor, metadata, .. } => { if self.messages_metadata.contains_key(&anchor.id) { @@ -1011,7 +1001,7 @@ impl AssistantContext { self.insert_message(anchor, metadata, cx); } } - ContextOperation::UpdateMessage { + TextThreadOperation::UpdateMessage { message_id, metadata: new_metadata, .. @@ -1022,7 +1012,7 @@ impl AssistantContext { changed_messages.insert(message_id); } } - ContextOperation::UpdateSummary { + TextThreadOperation::UpdateSummary { summary: new_summary, .. } => { @@ -1031,11 +1021,11 @@ impl AssistantContext { .timestamp() .is_none_or(|current_timestamp| new_summary.timestamp > current_timestamp) { - self.summary = ContextSummary::Content(new_summary); + self.summary = TextThreadSummary::Content(new_summary); summary_generated = true; } } - ContextOperation::SlashCommandStarted { + TextThreadOperation::SlashCommandStarted { id, output_range, name, @@ -1052,9 +1042,9 @@ impl AssistantContext { timestamp: id.0, }, ); - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); + cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id: id }); } - ContextOperation::SlashCommandOutputSectionAdded { section, .. } => { + TextThreadOperation::SlashCommandOutputSectionAdded { section, .. } => { let buffer = self.buffer.read(cx); if let Err(ix) = self .slash_command_output_sections @@ -1062,10 +1052,10 @@ impl AssistantContext { { self.slash_command_output_sections .insert(ix, section.clone()); - cx.emit(ContextEvent::SlashCommandOutputSectionAdded { section }); + cx.emit(TextThreadEvent::SlashCommandOutputSectionAdded { section }); } } - ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => { + TextThreadOperation::ThoughtProcessOutputSectionAdded { section, .. } => { let buffer = self.buffer.read(cx); if let Err(ix) = self .thought_process_output_sections @@ -1075,7 +1065,7 @@ impl AssistantContext { .insert(ix, section.clone()); } } - ContextOperation::SlashCommandFinished { + TextThreadOperation::SlashCommandFinished { id, error_message, timestamp, @@ -1094,10 +1084,10 @@ impl AssistantContext { slash_command.status = InvokedSlashCommandStatus::Finished; } } - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id: id }); + cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id: id }); } } - ContextOperation::BufferOperation(_) => unreachable!(), + TextThreadOperation::BufferOperation(_) => unreachable!(), } self.version.observe(timestamp); @@ -1107,43 +1097,43 @@ impl AssistantContext { if !changed_messages.is_empty() { self.message_roles_updated(changed_messages, cx); - cx.emit(ContextEvent::MessagesEdited); + cx.emit(TextThreadEvent::MessagesEdited); cx.notify(); } if summary_generated { - cx.emit(ContextEvent::SummaryChanged); - cx.emit(ContextEvent::SummaryGenerated); + cx.emit(TextThreadEvent::SummaryChanged); + cx.emit(TextThreadEvent::SummaryGenerated); cx.notify(); } } - fn can_apply_op(&self, op: &ContextOperation, cx: &App) -> bool { + fn can_apply_op(&self, op: &TextThreadOperation, cx: &App) -> bool { if !self.version.observed_all(op.version()) { return false; } match op { - ContextOperation::InsertMessage { anchor, .. } => self + TextThreadOperation::InsertMessage { anchor, .. } => self .buffer .read(cx) .version .observed(anchor.start.timestamp), - ContextOperation::UpdateMessage { message_id, .. } => { + TextThreadOperation::UpdateMessage { message_id, .. } => { self.messages_metadata.contains_key(message_id) } - ContextOperation::UpdateSummary { .. } => true, - ContextOperation::SlashCommandStarted { output_range, .. } => { + TextThreadOperation::UpdateSummary { .. } => true, + TextThreadOperation::SlashCommandStarted { output_range, .. } => { self.has_received_operations_for_anchor_range(output_range.clone(), cx) } - ContextOperation::SlashCommandOutputSectionAdded { section, .. } => { + TextThreadOperation::SlashCommandOutputSectionAdded { section, .. } => { self.has_received_operations_for_anchor_range(section.range.clone(), cx) } - ContextOperation::ThoughtProcessOutputSectionAdded { section, .. } => { + TextThreadOperation::ThoughtProcessOutputSectionAdded { section, .. } => { self.has_received_operations_for_anchor_range(section.range.clone(), cx) } - ContextOperation::SlashCommandFinished { .. } => true, - ContextOperation::BufferOperation(_) => { + TextThreadOperation::SlashCommandFinished { .. } => true, + TextThreadOperation::BufferOperation(_) => { panic!("buffer operations should always be applied") } } @@ -1164,9 +1154,9 @@ impl AssistantContext { observed_start && observed_end } - fn push_op(&mut self, op: ContextOperation, cx: &mut Context) { + fn push_op(&mut self, op: TextThreadOperation, cx: &mut Context) { self.operations.push(op.clone()); - cx.emit(ContextEvent::Operation(op)); + cx.emit(TextThreadEvent::Operation(op)); } pub fn buffer(&self) -> &Entity { @@ -1189,7 +1179,7 @@ impl AssistantContext { self.path.as_ref() } - pub fn summary(&self) -> &ContextSummary { + pub fn summary(&self) -> &TextThreadSummary { &self.summary } @@ -1250,13 +1240,13 @@ impl AssistantContext { language::BufferEvent::Operation { operation, is_local: true, - } => cx.emit(ContextEvent::Operation(ContextOperation::BufferOperation( - operation.clone(), - ))), + } => cx.emit(TextThreadEvent::Operation( + TextThreadOperation::BufferOperation(operation.clone()), + )), language::BufferEvent::Edited => { self.count_remaining_tokens(cx); self.reparse(cx); - cx.emit(ContextEvent::MessagesEdited); + cx.emit(TextThreadEvent::MessagesEdited); } _ => {} } @@ -1522,7 +1512,7 @@ impl AssistantContext { if !updated_parsed_slash_commands.is_empty() || !removed_parsed_slash_command_ranges.is_empty() { - cx.emit(ContextEvent::ParsedSlashCommandsUpdated { + cx.emit(TextThreadEvent::ParsedSlashCommandsUpdated { removed: removed_parsed_slash_command_ranges, updated: updated_parsed_slash_commands, }); @@ -1596,7 +1586,7 @@ impl AssistantContext { && (!command.range.start.is_valid(buffer) || !command.range.end.is_valid(buffer)) { command.status = InvokedSlashCommandStatus::Finished; - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id }); invalidated_command_ids.push(command_id); } } @@ -1605,7 +1595,7 @@ impl AssistantContext { let version = self.version.clone(); let timestamp = self.next_timestamp(); self.push_op( - ContextOperation::SlashCommandFinished { + TextThreadOperation::SlashCommandFinished { id: command_id, timestamp, error_message: None, @@ -1910,9 +1900,9 @@ impl AssistantContext { } } - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id }); this.push_op( - ContextOperation::SlashCommandFinished { + TextThreadOperation::SlashCommandFinished { id: command_id, timestamp, error_message, @@ -1935,9 +1925,9 @@ impl AssistantContext { timestamp: command_id.0, }, ); - cx.emit(ContextEvent::InvokedSlashCommandChanged { command_id }); + cx.emit(TextThreadEvent::InvokedSlashCommandChanged { command_id }); self.push_op( - ContextOperation::SlashCommandStarted { + TextThreadOperation::SlashCommandStarted { id: command_id, output_range: command_range, name: name.to_string(), @@ -1961,13 +1951,13 @@ impl AssistantContext { }; self.slash_command_output_sections .insert(insertion_ix, section.clone()); - cx.emit(ContextEvent::SlashCommandOutputSectionAdded { + cx.emit(TextThreadEvent::SlashCommandOutputSectionAdded { section: section.clone(), }); let version = self.version.clone(); let timestamp = self.next_timestamp(); self.push_op( - ContextOperation::SlashCommandOutputSectionAdded { + TextThreadOperation::SlashCommandOutputSectionAdded { timestamp, section, version, @@ -1996,7 +1986,7 @@ impl AssistantContext { let version = self.version.clone(); let timestamp = self.next_timestamp(); self.push_op( - ContextOperation::ThoughtProcessOutputSectionAdded { + TextThreadOperation::ThoughtProcessOutputSectionAdded { timestamp, section, version, @@ -2115,7 +2105,7 @@ impl AssistantContext { let end = buffer .anchor_before(message_old_end_offset + chunk_len); context_event = Some( - ContextEvent::StartedThoughtProcess(start..end), + TextThreadEvent::StartedThoughtProcess(start..end), ); } else { // This ensures that all the thinking chunks are inserted inside the thinking tag @@ -2133,7 +2123,7 @@ impl AssistantContext { if let Some(start) = thought_process_stack.pop() { let end = buffer.anchor_before(message_old_end_offset); context_event = - Some(ContextEvent::EndedThoughtProcess(end)); + Some(TextThreadEvent::EndedThoughtProcess(end)); thought_process_output_section = Some(ThoughtProcessOutputSection { range: start..end, @@ -2163,7 +2153,7 @@ impl AssistantContext { cx.emit(context_event); } - cx.emit(ContextEvent::StreamedCompletion); + cx.emit(TextThreadEvent::StreamedCompletion); Some(()) })?; @@ -2184,7 +2174,7 @@ impl AssistantContext { this.update(cx, |this, cx| { let error_message = if let Some(error) = result.as_ref().err() { if error.is::() { - cx.emit(ContextEvent::ShowPaymentRequiredError); + cx.emit(TextThreadEvent::ShowPaymentRequiredError); this.update_metadata(assistant_message_id, cx, |metadata| { metadata.status = MessageStatus::Canceled; }); @@ -2195,7 +2185,7 @@ impl AssistantContext { .map(|err| err.to_string()) .collect::>() .join("\n"); - cx.emit(ContextEvent::ShowAssistError(SharedString::from( + cx.emit(TextThreadEvent::ShowAssistError(SharedString::from( error_message.clone(), ))); this.update_metadata(assistant_message_id, cx, |metadata| { @@ -2412,13 +2402,13 @@ impl AssistantContext { if let Some(metadata) = self.messages_metadata.get_mut(&id) { f(metadata); metadata.timestamp = timestamp; - let operation = ContextOperation::UpdateMessage { + let operation = TextThreadOperation::UpdateMessage { message_id: id, metadata: metadata.clone(), version, }; self.push_op(operation, cx); - cx.emit(ContextEvent::MessagesEdited); + cx.emit(TextThreadEvent::MessagesEdited); cx.notify(); } } @@ -2482,7 +2472,7 @@ impl AssistantContext { }; self.insert_message(anchor.clone(), metadata.clone(), cx); self.push_op( - ContextOperation::InsertMessage { + TextThreadOperation::InsertMessage { anchor: anchor.clone(), metadata, version, @@ -2505,7 +2495,7 @@ impl AssistantContext { Err(ix) => ix, }; self.contents.insert(insertion_ix, content); - cx.emit(ContextEvent::MessagesEdited); + cx.emit(TextThreadEvent::MessagesEdited); } pub fn contents<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator { @@ -2580,7 +2570,7 @@ impl AssistantContext { }; self.insert_message(suffix.clone(), suffix_metadata.clone(), cx); self.push_op( - ContextOperation::InsertMessage { + TextThreadOperation::InsertMessage { anchor: suffix.clone(), metadata: suffix_metadata, version, @@ -2630,7 +2620,7 @@ impl AssistantContext { }; self.insert_message(selection.clone(), selection_metadata.clone(), cx); self.push_op( - ContextOperation::InsertMessage { + TextThreadOperation::InsertMessage { anchor: selection.clone(), metadata: selection_metadata, version, @@ -2642,7 +2632,7 @@ impl AssistantContext { }; if !edited_buffer { - cx.emit(ContextEvent::MessagesEdited); + cx.emit(TextThreadEvent::MessagesEdited); } new_messages } else { @@ -2656,7 +2646,7 @@ impl AssistantContext { new_metadata: MessageMetadata, cx: &mut Context, ) { - cx.emit(ContextEvent::MessagesEdited); + cx.emit(TextThreadEvent::MessagesEdited); self.messages_metadata.insert(new_anchor.id, new_metadata); @@ -2692,15 +2682,15 @@ impl AssistantContext { // If there is no summary, it is set with `done: false` so that "Loading Summary…" can // be displayed. match self.summary { - ContextSummary::Pending | ContextSummary::Error => { - self.summary = ContextSummary::Content(ContextSummaryContent { + TextThreadSummary::Pending | TextThreadSummary::Error => { + self.summary = TextThreadSummary::Content(TextThreadSummaryContent { text: "".to_string(), done: false, timestamp: clock::Lamport::MIN, }); replace_old = true; } - ContextSummary::Content(_) => {} + TextThreadSummary::Content(_) => {} } self.summary_task = cx.spawn(async move |this, cx| { @@ -2722,13 +2712,13 @@ impl AssistantContext { } summary.text.extend(lines.next()); summary.timestamp = timestamp; - let operation = ContextOperation::UpdateSummary { + let operation = TextThreadOperation::UpdateSummary { summary: summary.clone(), version, }; this.push_op(operation, cx); - cx.emit(ContextEvent::SummaryChanged); - cx.emit(ContextEvent::SummaryGenerated); + cx.emit(TextThreadEvent::SummaryChanged); + cx.emit(TextThreadEvent::SummaryGenerated); })?; // Stop if the LLM generated multiple lines. @@ -2752,13 +2742,13 @@ impl AssistantContext { if let Some(summary) = this.summary.content_as_mut() { summary.done = true; summary.timestamp = timestamp; - let operation = ContextOperation::UpdateSummary { + let operation = TextThreadOperation::UpdateSummary { summary: summary.clone(), version, }; this.push_op(operation, cx); - cx.emit(ContextEvent::SummaryChanged); - cx.emit(ContextEvent::SummaryGenerated); + cx.emit(TextThreadEvent::SummaryChanged); + cx.emit(TextThreadEvent::SummaryGenerated); } })?; @@ -2768,8 +2758,8 @@ impl AssistantContext { if let Err(err) = result { this.update(cx, |this, cx| { - this.summary = ContextSummary::Error; - cx.emit(ContextEvent::SummaryChanged); + this.summary = TextThreadSummary::Error; + cx.emit(TextThreadEvent::SummaryChanged); }) .log_err(); log::error!("Error generating context summary: {}", err); @@ -2875,7 +2865,7 @@ impl AssistantContext { &mut self, debounce: Option, fs: Arc, - cx: &mut Context, + cx: &mut Context, ) { if self.replica_id() != ReplicaId::default() { // Prevent saving a remote context for now. @@ -2906,7 +2896,7 @@ impl AssistantContext { let mut discriminant = 1; let mut new_path; loop { - new_path = contexts_dir().join(&format!( + new_path = text_threads_dir().join(&format!( "{} - {}.zed.json", summary.trim(), discriminant @@ -2918,7 +2908,7 @@ impl AssistantContext { } } - fs.create_dir(contexts_dir().as_ref()).await?; + fs.create_dir(text_threads_dir().as_ref()).await?; // rename before write ensures that only one file exists if let Some(old_path) = old_path.as_ref() @@ -2940,7 +2930,7 @@ impl AssistantContext { let new_path: Arc = new_path.clone().into(); move |this, cx| { this.path = Some(new_path.clone()); - cx.emit(ContextEvent::PathChanged { old_path, new_path }); + cx.emit(TextThreadEvent::PathChanged { old_path, new_path }); } }) .ok(); @@ -2959,7 +2949,7 @@ impl AssistantContext { summary.timestamp = timestamp; summary.done = true; summary.text = custom_summary; - cx.emit(ContextEvent::SummaryChanged); + cx.emit(TextThreadEvent::SummaryChanged); } fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut App) { @@ -2979,23 +2969,23 @@ impl AssistantContext { } #[derive(Debug, Default)] -pub struct ContextVersion { - context: clock::Global, +pub struct TextThreadVersion { + text_thread: clock::Global, buffer: clock::Global, } -impl ContextVersion { +impl TextThreadVersion { pub fn from_proto(proto: &proto::ContextVersion) -> Self { Self { - context: language::proto::deserialize_version(&proto.context_version), + text_thread: language::proto::deserialize_version(&proto.context_version), buffer: language::proto::deserialize_version(&proto.buffer_version), } } - pub fn to_proto(&self, context_id: ContextId) -> proto::ContextVersion { + pub fn to_proto(&self, context_id: TextThreadId) -> proto::ContextVersion { proto::ContextVersion { context_id: context_id.to_proto(), - context_version: language::proto::serialize_version(&self.context), + context_version: language::proto::serialize_version(&self.text_thread), buffer_version: language::proto::serialize_version(&self.buffer), } } @@ -3063,8 +3053,8 @@ pub struct SavedMessage { } #[derive(Serialize, Deserialize)] -pub struct SavedContext { - pub id: Option, +pub struct SavedTextThread { + pub id: Option, pub zed: String, pub version: String, pub text: String, @@ -3076,7 +3066,7 @@ pub struct SavedContext { pub thought_process_output_sections: Vec>, } -impl SavedContext { +impl SavedTextThread { pub const VERSION: &'static str = "0.4.0"; pub fn from_json(json: &str) -> Result { @@ -3086,9 +3076,9 @@ impl SavedContext { .context("version not found")? { serde_json::Value::String(version) => match version.as_str() { - SavedContext::VERSION => { - Ok(serde_json::from_value::(saved_context_json)?) - } + SavedTextThread::VERSION => Ok(serde_json::from_value::( + saved_context_json, + )?), SavedContextV0_3_0::VERSION => { let saved_context = serde_json::from_value::(saved_context_json)?; @@ -3113,8 +3103,8 @@ impl SavedContext { fn into_ops( self, buffer: &Entity, - cx: &mut Context, - ) -> Vec { + cx: &mut Context, + ) -> Vec { let mut operations = Vec::new(); let mut version = clock::Global::new(); let mut next_timestamp = clock::Lamport::new(ReplicaId::default()); @@ -3124,7 +3114,7 @@ impl SavedContext { if message.id == MessageId(clock::Lamport::MIN) { first_message_metadata = Some(message.metadata); } else { - operations.push(ContextOperation::InsertMessage { + operations.push(TextThreadOperation::InsertMessage { anchor: MessageAnchor { id: message.id, start: buffer.read(cx).anchor_before(message.start), @@ -3144,7 +3134,7 @@ impl SavedContext { if let Some(metadata) = first_message_metadata { let timestamp = next_timestamp.tick(); - operations.push(ContextOperation::UpdateMessage { + operations.push(TextThreadOperation::UpdateMessage { message_id: MessageId(clock::Lamport::MIN), metadata: MessageMetadata { role: metadata.role, @@ -3160,7 +3150,7 @@ impl SavedContext { let buffer = buffer.read(cx); for section in self.slash_command_output_sections { let timestamp = next_timestamp.tick(); - operations.push(ContextOperation::SlashCommandOutputSectionAdded { + operations.push(TextThreadOperation::SlashCommandOutputSectionAdded { timestamp, section: SlashCommandOutputSection { range: buffer.anchor_after(section.range.start) @@ -3177,7 +3167,7 @@ impl SavedContext { for section in self.thought_process_output_sections { let timestamp = next_timestamp.tick(); - operations.push(ContextOperation::ThoughtProcessOutputSectionAdded { + operations.push(TextThreadOperation::ThoughtProcessOutputSectionAdded { timestamp, section: ThoughtProcessOutputSection { range: buffer.anchor_after(section.range.start) @@ -3190,8 +3180,8 @@ impl SavedContext { } let timestamp = next_timestamp.tick(); - operations.push(ContextOperation::UpdateSummary { - summary: ContextSummaryContent { + operations.push(TextThreadOperation::UpdateSummary { + summary: TextThreadSummaryContent { text: self.summary, done: true, timestamp, @@ -3221,7 +3211,7 @@ struct SavedMessageMetadataPreV0_4_0 { #[derive(Serialize, Deserialize)] struct SavedContextV0_3_0 { - id: Option, + id: Option, zed: String, version: String, text: String, @@ -3234,11 +3224,11 @@ struct SavedContextV0_3_0 { impl SavedContextV0_3_0 { const VERSION: &'static str = "0.3.0"; - fn upgrade(self) -> SavedContext { - SavedContext { + fn upgrade(self) -> SavedTextThread { + SavedTextThread { id: self.id, zed: self.zed, - version: SavedContext::VERSION.into(), + version: SavedTextThread::VERSION.into(), text: self.text, messages: self .messages @@ -3270,7 +3260,7 @@ impl SavedContextV0_3_0 { #[derive(Serialize, Deserialize)] struct SavedContextV0_2_0 { - id: Option, + id: Option, zed: String, version: String, text: String, @@ -3282,7 +3272,7 @@ struct SavedContextV0_2_0 { impl SavedContextV0_2_0 { const VERSION: &'static str = "0.2.0"; - fn upgrade(self) -> SavedContext { + fn upgrade(self) -> SavedTextThread { SavedContextV0_3_0 { id: self.id, zed: self.zed, @@ -3299,7 +3289,7 @@ impl SavedContextV0_2_0 { #[derive(Serialize, Deserialize)] struct SavedContextV0_1_0 { - id: Option, + id: Option, zed: String, version: String, text: String, @@ -3313,7 +3303,7 @@ struct SavedContextV0_1_0 { impl SavedContextV0_1_0 { const VERSION: &'static str = "0.1.0"; - fn upgrade(self) -> SavedContext { + fn upgrade(self) -> SavedTextThread { SavedContextV0_2_0 { id: self.id, zed: self.zed, @@ -3328,7 +3318,7 @@ impl SavedContextV0_1_0 { } #[derive(Debug, Clone)] -pub struct SavedContextMetadata { +pub struct SavedTextThreadMetadata { pub title: SharedString, pub path: Arc, pub mtime: chrono::DateTime, diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_text_thread/src/text_thread_store.rs similarity index 71% rename from crates/assistant_context/src/context_store.rs rename to crates/assistant_text_thread/src/text_thread_store.rs index 5fac44e31f4cc073af8fe6bbb57f75fc03b27f45..19c317baf0fa728c77faebc388b5e36008aa39b3 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_text_thread/src/text_thread_store.rs @@ -1,6 +1,6 @@ use crate::{ - AssistantContext, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, - SavedContextMetadata, + SavedTextThread, SavedTextThreadMetadata, TextThread, TextThreadEvent, TextThreadId, + TextThreadOperation, TextThreadVersion, }; use anyhow::{Context as _, Result}; use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet}; @@ -11,9 +11,9 @@ use context_server::ContextServerId; use fs::{Fs, RemoveOptions}; use futures::StreamExt; use fuzzy::StringMatchCandidate; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; +use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; use language::LanguageRegistry; -use paths::contexts_dir; +use paths::text_threads_dir; use project::{ Project, context_server_store::{ContextServerStatus, ContextServerStore}, @@ -27,24 +27,24 @@ use util::{ResultExt, TryFutureExt}; use zed_env_vars::ZED_STATELESS; pub(crate) fn init(client: &AnyProtoClient) { - client.add_entity_message_handler(ContextStore::handle_advertise_contexts); - client.add_entity_request_handler(ContextStore::handle_open_context); - client.add_entity_request_handler(ContextStore::handle_create_context); - client.add_entity_message_handler(ContextStore::handle_update_context); - client.add_entity_request_handler(ContextStore::handle_synchronize_contexts); + client.add_entity_message_handler(TextThreadStore::handle_advertise_contexts); + client.add_entity_request_handler(TextThreadStore::handle_open_context); + client.add_entity_request_handler(TextThreadStore::handle_create_context); + client.add_entity_message_handler(TextThreadStore::handle_update_context); + client.add_entity_request_handler(TextThreadStore::handle_synchronize_contexts); } #[derive(Clone)] -pub struct RemoteContextMetadata { - pub id: ContextId, +pub struct RemoteTextThreadMetadata { + pub id: TextThreadId, pub summary: Option, } -pub struct ContextStore { - contexts: Vec, - contexts_metadata: Vec, +pub struct TextThreadStore { + text_threads: Vec, + text_threads_metadata: Vec, context_server_slash_command_ids: HashMap>, - host_contexts: Vec, + host_text_threads: Vec, fs: Arc, languages: Arc, slash_commands: Arc, @@ -58,34 +58,28 @@ pub struct ContextStore { prompt_builder: Arc, } -pub enum ContextStoreEvent { - ContextCreated(ContextId), +enum TextThreadHandle { + Weak(WeakEntity), + Strong(Entity), } -impl EventEmitter for ContextStore {} - -enum ContextHandle { - Weak(WeakEntity), - Strong(Entity), -} - -impl ContextHandle { - fn upgrade(&self) -> Option> { +impl TextThreadHandle { + fn upgrade(&self) -> Option> { match self { - ContextHandle::Weak(weak) => weak.upgrade(), - ContextHandle::Strong(strong) => Some(strong.clone()), + TextThreadHandle::Weak(weak) => weak.upgrade(), + TextThreadHandle::Strong(strong) => Some(strong.clone()), } } - fn downgrade(&self) -> WeakEntity { + fn downgrade(&self) -> WeakEntity { match self { - ContextHandle::Weak(weak) => weak.clone(), - ContextHandle::Strong(strong) => strong.downgrade(), + TextThreadHandle::Weak(weak) => weak.clone(), + TextThreadHandle::Strong(strong) => strong.downgrade(), } } } -impl ContextStore { +impl TextThreadStore { pub fn new( project: Entity, prompt_builder: Arc, @@ -97,14 +91,14 @@ impl ContextStore { let telemetry = project.read(cx).client().telemetry().clone(); cx.spawn(async move |cx| { const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100); - let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await; + let (mut events, _) = fs.watch(text_threads_dir(), CONTEXT_WATCH_DURATION).await; let this = cx.new(|cx: &mut Context| { let mut this = Self { - contexts: Vec::new(), - contexts_metadata: Vec::new(), + text_threads: Vec::new(), + text_threads_metadata: Vec::new(), context_server_slash_command_ids: HashMap::default(), - host_contexts: Vec::new(), + host_text_threads: Vec::new(), fs, languages, slash_commands, @@ -142,10 +136,10 @@ impl ContextStore { #[cfg(any(test, feature = "test-support"))] pub fn fake(project: Entity, cx: &mut Context) -> Self { Self { - contexts: Default::default(), - contexts_metadata: Default::default(), + text_threads: Default::default(), + text_threads_metadata: Default::default(), context_server_slash_command_ids: Default::default(), - host_contexts: Default::default(), + host_text_threads: Default::default(), fs: project.read(cx).fs().clone(), languages: project.read(cx).languages().clone(), slash_commands: Arc::default(), @@ -166,13 +160,13 @@ impl ContextStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - this.host_contexts = envelope + this.host_text_threads = envelope .payload .contexts .into_iter() - .map(|context| RemoteContextMetadata { - id: ContextId::from_proto(context.context_id), - summary: context.summary, + .map(|text_thread| RemoteTextThreadMetadata { + id: TextThreadId::from_proto(text_thread.context_id), + summary: text_thread.summary, }) .collect(); cx.notify(); @@ -184,25 +178,25 @@ impl ContextStore { envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - let context_id = ContextId::from_proto(envelope.payload.context_id); + let context_id = TextThreadId::from_proto(envelope.payload.context_id); let operations = this.update(&mut cx, |this, cx| { anyhow::ensure!( !this.project.read(cx).is_via_collab(), "only the host contexts can be opened" ); - let context = this - .loaded_context_for_id(&context_id, cx) + let text_thread = this + .loaded_text_thread_for_id(&context_id, cx) .context("context not found")?; anyhow::ensure!( - context.read(cx).replica_id() == ReplicaId::default(), + text_thread.read(cx).replica_id() == ReplicaId::default(), "context must be opened via the host" ); anyhow::Ok( - context + text_thread .read(cx) - .serialize_ops(&ContextVersion::default(), cx), + .serialize_ops(&TextThreadVersion::default(), cx), ) })??; let operations = operations.await; @@ -222,15 +216,14 @@ impl ContextStore { "can only create contexts as the host" ); - let context = this.create(cx); - let context_id = context.read(cx).id().clone(); - cx.emit(ContextStoreEvent::ContextCreated(context_id.clone())); + let text_thread = this.create(cx); + let context_id = text_thread.read(cx).id().clone(); anyhow::Ok(( context_id, - context + text_thread .read(cx) - .serialize_ops(&ContextVersion::default(), cx), + .serialize_ops(&TextThreadVersion::default(), cx), )) })??; let operations = operations.await; @@ -246,11 +239,11 @@ impl ContextStore { mut cx: AsyncApp, ) -> Result<()> { this.update(&mut cx, |this, cx| { - let context_id = ContextId::from_proto(envelope.payload.context_id); - if let Some(context) = this.loaded_context_for_id(&context_id, cx) { + let context_id = TextThreadId::from_proto(envelope.payload.context_id); + if let Some(text_thread) = this.loaded_text_thread_for_id(&context_id, cx) { let operation_proto = envelope.payload.operation.context("invalid operation")?; - let operation = ContextOperation::from_proto(operation_proto)?; - context.update(cx, |context, cx| context.apply_ops([operation], cx)); + let operation = TextThreadOperation::from_proto(operation_proto)?; + text_thread.update(cx, |text_thread, cx| text_thread.apply_ops([operation], cx)); } Ok(()) })? @@ -269,12 +262,12 @@ impl ContextStore { let mut local_versions = Vec::new(); for remote_version_proto in envelope.payload.contexts { - let remote_version = ContextVersion::from_proto(&remote_version_proto); - let context_id = ContextId::from_proto(remote_version_proto.context_id); - if let Some(context) = this.loaded_context_for_id(&context_id, cx) { - let context = context.read(cx); - let operations = context.serialize_ops(&remote_version, cx); - local_versions.push(context.version(cx).to_proto(context_id.clone())); + let remote_version = TextThreadVersion::from_proto(&remote_version_proto); + let context_id = TextThreadId::from_proto(remote_version_proto.context_id); + if let Some(text_thread) = this.loaded_text_thread_for_id(&context_id, cx) { + let text_thread = text_thread.read(cx); + let operations = text_thread.serialize_ops(&remote_version, cx); + local_versions.push(text_thread.version(cx).to_proto(context_id.clone())); let client = this.client.clone(); let project_id = envelope.payload.project_id; cx.background_spawn(async move { @@ -308,9 +301,9 @@ impl ContextStore { } if is_shared { - self.contexts.retain_mut(|context| { - if let Some(strong_context) = context.upgrade() { - *context = ContextHandle::Strong(strong_context); + self.text_threads.retain_mut(|text_thread| { + if let Some(strong_context) = text_thread.upgrade() { + *text_thread = TextThreadHandle::Strong(strong_context); true } else { false @@ -345,12 +338,12 @@ impl ContextStore { self.synchronize_contexts(cx); } project::Event::DisconnectedFromHost => { - self.contexts.retain_mut(|context| { - if let Some(strong_context) = context.upgrade() { - *context = ContextHandle::Weak(context.downgrade()); - strong_context.update(cx, |context, cx| { - if context.replica_id() != ReplicaId::default() { - context.set_capability(language::Capability::ReadOnly, cx); + self.text_threads.retain_mut(|text_thread| { + if let Some(strong_context) = text_thread.upgrade() { + *text_thread = TextThreadHandle::Weak(text_thread.downgrade()); + strong_context.update(cx, |text_thread, cx| { + if text_thread.replica_id() != ReplicaId::default() { + text_thread.set_capability(language::Capability::ReadOnly, cx); } }); true @@ -358,20 +351,24 @@ impl ContextStore { false } }); - self.host_contexts.clear(); + self.host_text_threads.clear(); cx.notify(); } _ => {} } } - pub fn unordered_contexts(&self) -> impl Iterator { - self.contexts_metadata.iter() + pub fn unordered_text_threads(&self) -> impl Iterator { + self.text_threads_metadata.iter() } - pub fn create(&mut self, cx: &mut Context) -> Entity { + pub fn host_text_threads(&self) -> impl Iterator { + self.host_text_threads.iter() + } + + pub fn create(&mut self, cx: &mut Context) -> Entity { let context = cx.new(|cx| { - AssistantContext::local( + TextThread::local( self.languages.clone(), Some(self.project.clone()), Some(self.telemetry.clone()), @@ -380,14 +377,11 @@ impl ContextStore { cx, ) }); - self.register_context(&context, cx); + self.register_text_thread(&context, cx); context } - pub fn create_remote_context( - &mut self, - cx: &mut Context, - ) -> Task>> { + pub fn create_remote(&mut self, cx: &mut Context) -> Task>> { let project = self.project.read(cx); let Some(project_id) = project.remote_id() else { return Task::ready(Err(anyhow::anyhow!("project was not remote"))); @@ -403,10 +397,10 @@ impl ContextStore { let request = self.client.request(proto::CreateContext { project_id }); cx.spawn(async move |this, cx| { let response = request.await?; - let context_id = ContextId::from_proto(response.context_id); + let context_id = TextThreadId::from_proto(response.context_id); let context_proto = response.context.context("invalid context")?; - let context = cx.new(|cx| { - AssistantContext::new( + let text_thread = cx.new(|cx| { + TextThread::new( context_id.clone(), replica_id, capability, @@ -423,29 +417,29 @@ impl ContextStore { context_proto .operations .into_iter() - .map(ContextOperation::from_proto) + .map(TextThreadOperation::from_proto) .collect::>>() }) .await?; - context.update(cx, |context, cx| context.apply_ops(operations, cx))?; + text_thread.update(cx, |context, cx| context.apply_ops(operations, cx))?; this.update(cx, |this, cx| { - if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) { + if let Some(existing_context) = this.loaded_text_thread_for_id(&context_id, cx) { existing_context } else { - this.register_context(&context, cx); + this.register_text_thread(&text_thread, cx); this.synchronize_contexts(cx); - context + text_thread } }) }) } - pub fn open_local_context( + pub fn open_local( &mut self, path: Arc, cx: &Context, - ) -> Task>> { - if let Some(existing_context) = self.loaded_context_for_path(&path, cx) { + ) -> Task>> { + if let Some(existing_context) = self.loaded_text_thread_for_path(&path, cx) { return Task::ready(Ok(existing_context)); } @@ -457,7 +451,7 @@ impl ContextStore { let path = path.clone(); async move { let saved_context = fs.load(&path).await?; - SavedContext::from_json(&saved_context) + SavedTextThread::from_json(&saved_context) } }); let prompt_builder = self.prompt_builder.clone(); @@ -466,7 +460,7 @@ impl ContextStore { cx.spawn(async move |this, cx| { let saved_context = load.await?; let context = cx.new(|cx| { - AssistantContext::deserialize( + TextThread::deserialize( saved_context, path.clone(), languages, @@ -478,21 +472,17 @@ impl ContextStore { ) })?; this.update(cx, |this, cx| { - if let Some(existing_context) = this.loaded_context_for_path(&path, cx) { + if let Some(existing_context) = this.loaded_text_thread_for_path(&path, cx) { existing_context } else { - this.register_context(&context, cx); + this.register_text_thread(&context, cx); context } }) }) } - pub fn delete_local_context( - &mut self, - path: Arc, - cx: &mut Context, - ) -> Task> { + pub fn delete_local(&mut self, path: Arc, cx: &mut Context) -> Task> { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { @@ -506,57 +496,57 @@ impl ContextStore { .await?; this.update(cx, |this, cx| { - this.contexts.retain(|context| { - context + this.text_threads.retain(|text_thread| { + text_thread .upgrade() - .and_then(|context| context.read(cx).path()) + .and_then(|text_thread| text_thread.read(cx).path()) != Some(&path) }); - this.contexts_metadata - .retain(|context| context.path.as_ref() != path.as_ref()); + this.text_threads_metadata + .retain(|text_thread| text_thread.path.as_ref() != path.as_ref()); })?; Ok(()) }) } - fn loaded_context_for_path(&self, path: &Path, cx: &App) -> Option> { - self.contexts.iter().find_map(|context| { - let context = context.upgrade()?; - if context.read(cx).path().map(Arc::as_ref) == Some(path) { - Some(context) + fn loaded_text_thread_for_path(&self, path: &Path, cx: &App) -> Option> { + self.text_threads.iter().find_map(|text_thread| { + let text_thread = text_thread.upgrade()?; + if text_thread.read(cx).path().map(Arc::as_ref) == Some(path) { + Some(text_thread) } else { None } }) } - pub fn loaded_context_for_id( + pub fn loaded_text_thread_for_id( &self, - id: &ContextId, + id: &TextThreadId, cx: &App, - ) -> Option> { - self.contexts.iter().find_map(|context| { - let context = context.upgrade()?; - if context.read(cx).id() == id { - Some(context) + ) -> Option> { + self.text_threads.iter().find_map(|text_thread| { + let text_thread = text_thread.upgrade()?; + if text_thread.read(cx).id() == id { + Some(text_thread) } else { None } }) } - pub fn open_remote_context( + pub fn open_remote( &mut self, - context_id: ContextId, + text_thread_id: TextThreadId, cx: &mut Context, - ) -> Task>> { + ) -> Task>> { let project = self.project.read(cx); let Some(project_id) = project.remote_id() else { return Task::ready(Err(anyhow::anyhow!("project was not remote"))); }; - if let Some(context) = self.loaded_context_for_id(&context_id, cx) { + if let Some(context) = self.loaded_text_thread_for_id(&text_thread_id, cx) { return Task::ready(Ok(context)); } @@ -567,16 +557,16 @@ impl ContextStore { let telemetry = self.telemetry.clone(); let request = self.client.request(proto::OpenContext { project_id, - context_id: context_id.to_proto(), + context_id: text_thread_id.to_proto(), }); let prompt_builder = self.prompt_builder.clone(); let slash_commands = self.slash_commands.clone(); cx.spawn(async move |this, cx| { let response = request.await?; let context_proto = response.context.context("invalid context")?; - let context = cx.new(|cx| { - AssistantContext::new( - context_id.clone(), + let text_thread = cx.new(|cx| { + TextThread::new( + text_thread_id.clone(), replica_id, capability, language_registry, @@ -592,38 +582,40 @@ impl ContextStore { context_proto .operations .into_iter() - .map(ContextOperation::from_proto) + .map(TextThreadOperation::from_proto) .collect::>>() }) .await?; - context.update(cx, |context, cx| context.apply_ops(operations, cx))?; + text_thread.update(cx, |context, cx| context.apply_ops(operations, cx))?; this.update(cx, |this, cx| { - if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) { + if let Some(existing_context) = this.loaded_text_thread_for_id(&text_thread_id, cx) + { existing_context } else { - this.register_context(&context, cx); + this.register_text_thread(&text_thread, cx); this.synchronize_contexts(cx); - context + text_thread } }) }) } - fn register_context(&mut self, context: &Entity, cx: &mut Context) { + fn register_text_thread(&mut self, text_thread: &Entity, cx: &mut Context) { let handle = if self.project_is_shared { - ContextHandle::Strong(context.clone()) + TextThreadHandle::Strong(text_thread.clone()) } else { - ContextHandle::Weak(context.downgrade()) + TextThreadHandle::Weak(text_thread.downgrade()) }; - self.contexts.push(handle); + self.text_threads.push(handle); self.advertise_contexts(cx); - cx.subscribe(context, Self::handle_context_event).detach(); + cx.subscribe(text_thread, Self::handle_context_event) + .detach(); } fn handle_context_event( &mut self, - context: Entity, - event: &ContextEvent, + text_thread: Entity, + event: &TextThreadEvent, cx: &mut Context, ) { let Some(project_id) = self.project.read(cx).remote_id() else { @@ -631,12 +623,12 @@ impl ContextStore { }; match event { - ContextEvent::SummaryChanged => { + TextThreadEvent::SummaryChanged => { self.advertise_contexts(cx); } - ContextEvent::PathChanged { old_path, new_path } => { + TextThreadEvent::PathChanged { old_path, new_path } => { if let Some(old_path) = old_path.as_ref() { - for metadata in &mut self.contexts_metadata { + for metadata in &mut self.text_threads_metadata { if &metadata.path == old_path { metadata.path = new_path.clone(); break; @@ -644,8 +636,8 @@ impl ContextStore { } } } - ContextEvent::Operation(operation) => { - let context_id = context.read(cx).id().to_proto(); + TextThreadEvent::Operation(operation) => { + let context_id = text_thread.read(cx).id().to_proto(); let operation = operation.to_proto(); self.client .send(proto::UpdateContext { @@ -670,15 +662,15 @@ impl ContextStore { } let contexts = self - .contexts + .text_threads .iter() .rev() - .filter_map(|context| { - let context = context.upgrade()?.read(cx); - if context.replica_id() == ReplicaId::default() { + .filter_map(|text_thread| { + let text_thread = text_thread.upgrade()?.read(cx); + if text_thread.replica_id() == ReplicaId::default() { Some(proto::ContextMetadata { - context_id: context.id().to_proto(), - summary: context + context_id: text_thread.id().to_proto(), + summary: text_thread .summary() .content() .map(|summary| summary.text.clone()), @@ -701,13 +693,13 @@ impl ContextStore { return; }; - let contexts = self - .contexts + let text_threads = self + .text_threads .iter() - .filter_map(|context| { - let context = context.upgrade()?.read(cx); - if context.replica_id() != ReplicaId::default() { - Some(context.version(cx).to_proto(context.id().clone())) + .filter_map(|text_thread| { + let text_thread = text_thread.upgrade()?.read(cx); + if text_thread.replica_id() != ReplicaId::default() { + Some(text_thread.version(cx).to_proto(text_thread.id().clone())) } else { None } @@ -717,26 +709,27 @@ impl ContextStore { let client = self.client.clone(); let request = self.client.request(proto::SynchronizeContexts { project_id, - contexts, + contexts: text_threads, }); cx.spawn(async move |this, cx| { let response = request.await?; - let mut context_ids = Vec::new(); + let mut text_thread_ids = Vec::new(); let mut operations = Vec::new(); this.read_with(cx, |this, cx| { for context_version_proto in response.contexts { - let context_version = ContextVersion::from_proto(&context_version_proto); - let context_id = ContextId::from_proto(context_version_proto.context_id); - if let Some(context) = this.loaded_context_for_id(&context_id, cx) { - context_ids.push(context_id); - operations.push(context.read(cx).serialize_ops(&context_version, cx)); + let text_thread_version = TextThreadVersion::from_proto(&context_version_proto); + let text_thread_id = TextThreadId::from_proto(context_version_proto.context_id); + if let Some(text_thread) = this.loaded_text_thread_for_id(&text_thread_id, cx) { + text_thread_ids.push(text_thread_id); + operations + .push(text_thread.read(cx).serialize_ops(&text_thread_version, cx)); } } })?; let operations = futures::future::join_all(operations).await; - for (context_id, operations) in context_ids.into_iter().zip(operations) { + for (context_id, operations) in text_thread_ids.into_iter().zip(operations) { for operation in operations { client.send(proto::UpdateContext { project_id, @@ -751,8 +744,8 @@ impl ContextStore { .detach_and_log_err(cx); } - pub fn search(&self, query: String, cx: &App) -> Task> { - let metadata = self.contexts_metadata.clone(); + pub fn search(&self, query: String, cx: &App) -> Task> { + let metadata = self.text_threads_metadata.clone(); let executor = cx.background_executor().clone(); cx.background_spawn(async move { if query.is_empty() { @@ -782,20 +775,16 @@ impl ContextStore { }) } - pub fn host_contexts(&self) -> &[RemoteContextMetadata] { - &self.host_contexts - } - fn reload(&mut self, cx: &mut Context) -> Task> { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { if *ZED_STATELESS { return Ok(()); } - fs.create_dir(contexts_dir()).await?; + fs.create_dir(text_threads_dir()).await?; - let mut paths = fs.read_dir(contexts_dir()).await?; - let mut contexts = Vec::::new(); + let mut paths = fs.read_dir(text_threads_dir()).await?; + let mut contexts = Vec::::new(); while let Some(path) = paths.next().await { let path = path?; if path.extension() != Some(OsStr::new("json")) { @@ -821,7 +810,7 @@ impl ContextStore { .lines() .next() { - contexts.push(SavedContextMetadata { + contexts.push(SavedTextThreadMetadata { title: title.to_string().into(), path: path.into(), mtime: metadata.mtime.timestamp_for_user().into(), @@ -829,10 +818,10 @@ impl ContextStore { } } } - contexts.sort_unstable_by_key(|context| Reverse(context.mtime)); + contexts.sort_unstable_by_key(|text_thread| Reverse(text_thread.mtime)); this.update(cx, |this, cx| { - this.contexts_metadata = contexts; + this.text_threads_metadata = contexts; cx.notify(); }) }) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 52dbe46107501325e305a7e8e6e7bd9bb483affb..c8467da7954b195c0eef09ce1bed8361d7fa2c7b 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -73,7 +73,7 @@ uuid.workspace = true [dev-dependencies] agent_settings.workspace = true -assistant_context.workspace = true +assistant_text_thread.workspace = true assistant_slash_command.workspace = true async-trait.workspace = true audio.workspace = true diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index cc2c01b7857a1efefd88b47d2ea199fc571051ea..4fa32b6c9ba55e6962547510f52251f16fc9be81 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6,8 +6,8 @@ use crate::{ }, }; use anyhow::{Result, anyhow}; -use assistant_context::ContextStore; use assistant_slash_command::SlashCommandWorkingSet; +use assistant_text_thread::TextThreadStore; use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus, assert_hunks}; use call::{ActiveCall, ParticipantLocation, Room, room}; use client::{RECEIVE_TIMEOUT, User}; @@ -6877,9 +6877,9 @@ async fn test_context_collaboration_with_reconnect( }); let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap()); - let context_store_a = cx_a + let text_thread_store_a = cx_a .update(|cx| { - ContextStore::new( + TextThreadStore::new( project_a.clone(), prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), @@ -6888,9 +6888,9 @@ async fn test_context_collaboration_with_reconnect( }) .await .unwrap(); - let context_store_b = cx_b + let text_thread_store_b = cx_b .update(|cx| { - ContextStore::new( + TextThreadStore::new( project_b.clone(), prompt_builder.clone(), Arc::new(SlashCommandWorkingSet::default()), @@ -6901,60 +6901,60 @@ async fn test_context_collaboration_with_reconnect( .unwrap(); // Client A creates a new chats. - let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx)); + let text_thread_a = text_thread_store_a.update(cx_a, |store, cx| store.create(cx)); executor.run_until_parked(); // Client B retrieves host's contexts and joins one. - let context_b = context_store_b + let text_thread_b = text_thread_store_b .update(cx_b, |store, cx| { - let host_contexts = store.host_contexts().to_vec(); - assert_eq!(host_contexts.len(), 1); - store.open_remote_context(host_contexts[0].id.clone(), cx) + let host_text_threads = store.host_text_threads().collect::>(); + assert_eq!(host_text_threads.len(), 1); + store.open_remote(host_text_threads[0].id.clone(), cx) }) .await .unwrap(); // Host and guest make changes - context_a.update(cx_a, |context, cx| { - context.buffer().update(cx, |buffer, cx| { + text_thread_a.update(cx_a, |text_thread, cx| { + text_thread.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "Host change\n")], None, cx) }) }); - context_b.update(cx_b, |context, cx| { - context.buffer().update(cx, |buffer, cx| { + text_thread_b.update(cx_b, |text_thread, cx| { + text_thread.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "Guest change\n")], None, cx) }) }); executor.run_until_parked(); assert_eq!( - context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()), + text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()), "Guest change\nHost change\n" ); assert_eq!( - context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()), + text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()), "Guest change\nHost change\n" ); // Disconnect client A and make some changes while disconnected. server.disconnect_client(client_a.peer_id().unwrap()); server.forbid_connections(); - context_a.update(cx_a, |context, cx| { - context.buffer().update(cx, |buffer, cx| { + text_thread_a.update(cx_a, |text_thread, cx| { + text_thread.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "Host offline change\n")], None, cx) }) }); - context_b.update(cx_b, |context, cx| { - context.buffer().update(cx, |buffer, cx| { + text_thread_b.update(cx_b, |text_thread, cx| { + text_thread.buffer().update(cx, |buffer, cx| { buffer.edit([(0..0, "Guest offline change\n")], None, cx) }) }); executor.run_until_parked(); assert_eq!( - context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()), + text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()), "Host offline change\nGuest change\nHost change\n" ); assert_eq!( - context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()), + text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()), "Guest offline change\nGuest change\nHost change\n" ); @@ -6962,11 +6962,11 @@ async fn test_context_collaboration_with_reconnect( server.allow_connections(); executor.advance_clock(RECEIVE_TIMEOUT); assert_eq!( - context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()), + text_thread_a.read_with(cx_a, |text_thread, cx| text_thread.buffer().read(cx).text()), "Guest offline change\nHost offline change\nGuest change\nHost change\n" ); assert_eq!( - context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()), + text_thread_b.read_with(cx_b, |text_thread, cx| text_thread.buffer().read(cx).text()), "Guest offline change\nHost offline change\nGuest change\nHost change\n" ); @@ -6974,8 +6974,8 @@ async fn test_context_collaboration_with_reconnect( server.forbid_connections(); server.disconnect_client(client_a.peer_id().unwrap()); executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - context_b.read_with(cx_b, |context, cx| { - assert!(context.buffer().read(cx).read_only()); + text_thread_b.read_with(cx_b, |text_thread, cx| { + assert!(text_thread.buffer().read(cx).read_only()); }); } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 528253f0dc2e9d4dc8b88a7d8d8c2926be2b2652..fbff269494f3f1ae5fb48d124ad090e61a558f31 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -358,7 +358,7 @@ impl TestServer { settings::KeymapFile::load_asset_allow_partial_failure(os_keymap, cx).unwrap(), ); language_model::LanguageModelRegistry::test(cx); - assistant_context::init(client.clone(), cx); + assistant_text_thread::init(client.clone(), cx); agent_settings::init(cx); }); diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index bbb6ddb976312b7baca5a11ace863b4a3be8d2bc..207e1f3bb4324d17784b1d8df53ba4bfbc4adddb 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -288,7 +288,7 @@ pub fn snippets_dir() -> &'static PathBuf { /// Returns the path to the contexts directory. /// /// This is where the saved contexts from the Assistant are stored. -pub fn contexts_dir() -> &'static PathBuf { +pub fn text_threads_dir() -> &'static PathBuf { static CONTEXTS_DIR: OnceLock = OnceLock::new(); CONTEXTS_DIR.get_or_init(|| { if cfg!(target_os = "macos") { From d83ed4e03eeaea45aad7a8a513783dc53e9809f7 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 23 Oct 2025 13:52:35 -0400 Subject: [PATCH 200/202] docs: Update docs for `theme_overrides` setting (#41038) This PR updates the docs to reference the `theme_overrides` setting instead of the old `experimental.theme_overrides` setting. Release Notes: - N/A --- docs/src/configuring-languages.md | 18 ++++++++++-------- docs/src/themes.md | 20 +++++++++++--------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index 9a7157ceaecbcc956e1e98688dfee01316225a3a..e04d63f5d16a83c84b933d9f59db901c276b7a6d 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -364,18 +364,20 @@ Zed offers customization options for syntax highlighting and themes, allowing yo ### Customizing Syntax Highlighting -Zed uses Tree-sitter grammars for syntax highlighting. Override the default highlighting using the `experimental.theme_overrides` setting. +Zed uses Tree-sitter grammars for syntax highlighting. Override the default highlighting using the `theme_overrides` setting. This example makes comments italic and changes the color of strings: ```json [settings] -"experimental.theme_overrides": { - "syntax": { - "comment": { - "font_style": "italic" - }, - "string": { - "color": "#00AA00" +"theme_overrides": { + "One Dark": { + "syntax": { + "comment": { + "font_style": "italic" + }, + "string": { + "color": "#00AA00" + } } } } diff --git a/docs/src/themes.md b/docs/src/themes.md index 438301dc13fe04b8b75ba0df348cdf499c49c329..460d00a7627e55f21958142c230b683d92301040 100644 --- a/docs/src/themes.md +++ b/docs/src/themes.md @@ -32,20 +32,22 @@ By default, Zed maintains two themes: one for light mode and one for dark mode. ## Theme Overrides -To override specific attributes of a theme, use the `experimental.theme_overrides` setting. +To override specific attributes of a theme, use the `theme_overrides` setting. This setting can be used to configure theme-specific overrides. For example, add the following to your `settings.json` if you wish to override the background color of the editor and display comments and doc comments as italics: ```json [settings] { - "experimental.theme_overrides": { - "editor.background": "#333", - "syntax": { - "comment": { - "font_style": "italic" - }, - "comment.doc": { - "font_style": "italic" + "theme_overrides": { + "One Dark": { + "editor.background": "#333", + "syntax": { + "comment": { + "font_style": "italic" + }, + "comment.doc": { + "font_style": "italic" + } } } } From 6aaf19f2761a833dd995767e30015209ecaa965f Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 23 Oct 2025 19:57:52 +0200 Subject: [PATCH 201/202] multi_buffer: Split multi_buffer into more modules (#41033) There are a of separate APIs in this, partially interleaved making it difficult to grasp. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/editor.rs | 1 - crates/editor/src/items.rs | 5 +- .../src/test/editor_lsp_test_context.rs | 1 - crates/language/src/buffer.rs | 11 +- crates/multi_buffer/src/anchor.rs | 8 +- crates/multi_buffer/src/multi_buffer.rs | 1268 +++-------------- crates/multi_buffer/src/multi_buffer_tests.rs | 3 +- crates/multi_buffer/src/path_key.rs | 417 ++++++ crates/multi_buffer/src/transaction.rs | 524 +++++++ crates/rope/src/chunk.rs | 13 + crates/rope/src/rope.rs | 49 + crates/text/src/text.rs | 12 + 12 files changed, 1234 insertions(+), 1078 deletions(-) create mode 100644 crates/multi_buffer/src/path_key.rs create mode 100644 crates/multi_buffer/src/transaction.rs diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f3aa166a691b8a29ec8d174b1c9503afbaafc6a4..40576d90c54cd80e637c536ee990496e3fc1c396 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -140,7 +140,6 @@ use mouse_context_menu::MouseContextMenu; use movement::TextLayoutDetails; use multi_buffer::{ ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - ToOffsetUtf16, }; use parking_lot::Mutex; use persistence::DB; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 28a416925672a937a163e85fcaa59066529481b1..6dab57db52700bc499376abb0ab80e9cdb45e5e9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -938,8 +938,9 @@ impl Item for Editor { fn breadcrumbs(&self, variant: &Theme, cx: &App) -> Option> { let cursor = self.selections.newest_anchor().head(); let multibuffer = &self.buffer().read(cx); - let (buffer_id, symbols) = - multibuffer.symbols_containing(cursor, Some(variant.syntax()), cx)?; + let (buffer_id, symbols) = multibuffer + .read(cx) + .symbols_containing(cursor, Some(variant.syntax()))?; let buffer = multibuffer.buffer(buffer_id)?; let buffer = buffer.read(cx); diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 5a850bf4cff924b85ea5599c3d75c2b602b4dd1d..3132e2e6d5976754d0bdb7fea312fa152d4c35ac 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -19,7 +19,6 @@ use language::{ point_to_lsp, }; use lsp::{notification, request}; -use multi_buffer::ToPointUtf16; use project::Project; use smol::stream::StreamExt; use workspace::{AppState, Workspace, WorkspaceHandle}; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 973add14f33a3a9554df4a20c55aff3eb3453683..6e4007fdae2ad4af4c6ab56b82bff78c196b2d73 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2078,12 +2078,15 @@ impl Buffer { } } + /// Set the change bit for all "listeners". fn was_changed(&mut self) { self.change_bits.retain(|change_bit| { - change_bit.upgrade().is_some_and(|bit| { - bit.replace(true); - true - }) + change_bit + .upgrade() + .inspect(|bit| { + _ = bit.replace(true); + }) + .is_some() }); } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index a2498cb02fb836c6a70af9407d2a4e520c9d3d3b..d5009172084d6d683f722a8ad2aa5b8b21ae0493 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -1,4 +1,4 @@ -use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint}; +use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; use language::{OffsetUtf16, Point, TextDimension}; use std::{ cmp::Ordering, @@ -185,9 +185,6 @@ impl ToOffset for Anchor { fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize { self.summary(snapshot) } -} - -impl ToOffsetUtf16 for Anchor { fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { self.summary(snapshot) } @@ -197,6 +194,9 @@ impl ToPoint for Anchor { fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { self.summary(snapshot) } + fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 { + self.summary(snapshot) + } } pub trait AnchorRangeExt { diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 646d7fce825c05204c07f42619d5f9964d5cd321..0163a49c95eeea5372a61824d2754a233ec07740 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1,7 +1,11 @@ mod anchor; #[cfg(test)] mod multi_buffer_tests; +mod path_key; mod position; +mod transaction; + +use self::transaction::History; pub use anchor::{Anchor, AnchorRangeExt, Offset}; pub use position::{TypedOffset, TypedPoint, TypedRow}; @@ -13,7 +17,7 @@ use buffer_diff::{ }; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; -use gpui::{App, AppContext as _, Context, Entity, EntityId, EventEmitter, Task}; +use gpui::{App, Context, Entity, EntityId, EventEmitter}; use itertools::Itertools; use language::{ AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier, @@ -24,6 +28,9 @@ use language::{ language_settings::{LanguageSettings, language_settings}, }; +#[cfg(any(test, feature = "test-support"))] +use gpui::AppContext as _; + use rope::DimensionPair; use smallvec::SmallVec; use smol::future::yield_now; @@ -40,7 +47,7 @@ use std::{ rc::Rc, str, sync::Arc, - time::{Duration, Instant}, + time::Duration, }; use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, Summary, TreeMap}; use text::{ @@ -49,9 +56,9 @@ use text::{ subscription::{Subscription, Topic}, }; use theme::SyntaxTheme; -use util::{post_inc, rel_path::RelPath}; +use util::post_inc; -const NEWLINES: &[u8] = &[b'\n'; rope::Chunk::MASK_BITS]; +pub use self::path_key::PathKey; #[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct ExcerptId(u32); @@ -163,35 +170,6 @@ impl MultiBufferDiffHunk { } } -#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)] -pub struct PathKey { - // Used by the derived PartialOrd & Ord - pub sort_prefix: Option, - pub path: Arc, -} - -impl PathKey { - pub fn with_sort_prefix(sort_prefix: u64, path: Arc) -> Self { - Self { - sort_prefix: Some(sort_prefix), - path, - } - } - - pub fn for_buffer(buffer: &Entity, cx: &App) -> Self { - if let Some(file) = buffer.read(cx).file() { - Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone()) - } else { - Self { - sort_prefix: None, - path: RelPath::unix(&buffer.entity_id().to_string()) - .unwrap() - .into_arc(), - } - } - } -} - pub type MultiBufferPoint = Point; type ExcerptOffset = TypedOffset; type ExcerptPoint = TypedPoint; @@ -213,37 +191,13 @@ impl std::ops::Add for MultiBufferRow { } } -#[derive(Clone)] -struct History { - next_transaction_id: TransactionId, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -#[derive(Clone)] -struct Transaction { - id: TransactionId, - buffer_transactions: HashMap, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - pub trait ToOffset: 'static + fmt::Debug { fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> usize; -} - -pub trait ToOffsetUtf16: 'static + fmt::Debug { fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16; } pub trait ToPoint: 'static + fmt::Debug { fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; -} - -pub trait ToPointUtf16: 'static + fmt::Debug { fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16; } @@ -281,24 +235,20 @@ impl DiffState { /// The contents of a [`MultiBuffer`] at a single point in time. #[derive(Clone, Default)] pub struct MultiBufferSnapshot { - singleton: bool, - /* mut */ excerpts: SumTree, - /* mut */ - excerpt_ids: SumTree, diffs: TreeMap, diff_transforms: SumTree, - /* mut */ - replaced_excerpts: TreeMap, - /* mut */ - trailing_excerpt_update_count: usize, - all_diff_hunks_expanded: bool, non_text_state_update_count: usize, edit_count: usize, - /* mut */ is_dirty: bool, has_deleted_file: bool, has_conflict: bool, + /// immutable fields + singleton: bool, + excerpt_ids: SumTree, + replaced_excerpts: TreeMap, + trailing_excerpt_update_count: usize, + all_diff_hunks_expanded: bool, show_headers: bool, } @@ -555,7 +505,7 @@ struct MultiBufferRegion<'a, D: TextDimension> { struct ExcerptChunks<'a> { excerpt_id: ExcerptId, content_chunks: BufferChunks<'a>, - footer_height: usize, + has_footer: bool, } #[derive(Debug)] @@ -660,13 +610,7 @@ impl MultiBuffer { excerpts_by_path: Default::default(), paths_by_excerpt: Default::default(), buffer_changed_since_sync: Default::default(), - history: History { - next_transaction_id: clock::Lamport::MIN, - undo_stack: Vec::new(), - redo_stack: Vec::new(), - transaction_depth: 0, - group_interval: Duration::from_millis(300), - }, + history: History::default(), } } @@ -712,6 +656,10 @@ impl MultiBuffer { } } + pub fn set_group_interval(&mut self, group_interval: Duration) { + self.history.set_group_interval(group_interval); + } + pub fn with_title(mut self, title: String) -> Self { self.title = Some(title); self @@ -770,17 +718,8 @@ impl MultiBuffer { self.buffers.is_empty() } - pub fn symbols_containing( - &self, - offset: T, - theme: Option<&SyntaxTheme>, - cx: &App, - ) -> Option<(BufferId, Vec>)> { - self.read(cx).symbols_containing(offset, theme) - } - pub fn edit( - &self, + &mut self, edits: I, autoindent_mode: Option, cx: &mut Context, @@ -789,11 +728,15 @@ impl MultiBuffer { S: ToOffset, T: Into>, { - let snapshot = self.read(cx); + if self.read_only() || self.buffers.is_empty() { + return; + } + self.sync_mut(cx); let edits = edits .into_iter() .map(|(range, new_text)| { - let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + let mut range = range.start.to_offset(self.snapshot.get_mut()) + ..range.end.to_offset(self.snapshot.get_mut()); if range.start > range.end { mem::swap(&mut range.start, &mut range.end); } @@ -801,20 +744,15 @@ impl MultiBuffer { }) .collect::>(); - return edit_internal(self, snapshot, edits, autoindent_mode, cx); + return edit_internal(self, edits, autoindent_mode, cx); // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. fn edit_internal( - this: &MultiBuffer, - snapshot: Ref, + this: &mut MultiBuffer, edits: Vec<(Range, Arc)>, mut autoindent_mode: Option, cx: &mut Context, ) { - if this.read_only() || this.buffers.is_empty() { - return; - } - let original_indent_columns = match &mut autoindent_mode { Some(AutoindentMode::Block { original_indent_columns, @@ -822,9 +760,11 @@ impl MultiBuffer { _ => Default::default(), }; - let (buffer_edits, edited_excerpt_ids) = - this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); - drop(snapshot); + let (buffer_edits, edited_excerpt_ids) = MultiBuffer::convert_edits_to_buffer_edits( + edits, + this.snapshot.get_mut(), + &original_indent_columns, + ); let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); for (buffer_id, mut edits) in buffer_edits { @@ -908,7 +848,6 @@ impl MultiBuffer { } fn convert_edits_to_buffer_edits( - &self, edits: Vec<(Range, Arc)>, snapshot: &MultiBufferSnapshot, original_indent_columns: &[Option], @@ -1028,17 +967,21 @@ impl MultiBuffer { (buffer_edits, edited_excerpt_ids) } - pub fn autoindent_ranges(&self, ranges: I, cx: &mut Context) + pub fn autoindent_ranges(&mut self, ranges: I, cx: &mut Context) where I: IntoIterator>, S: ToOffset, { - let snapshot = self.read(cx); + if self.read_only() || self.buffers.is_empty() { + return; + } + self.sync_mut(cx); let empty = Arc::::from(""); let edits = ranges .into_iter() .map(|range| { - let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot); + let mut range = range.start.to_offset(self.snapshot.get_mut()) + ..range.end.to_offset(&self.snapshot.get_mut()); if range.start > range.end { mem::swap(&mut range.start, &mut range.end); } @@ -1046,21 +989,15 @@ impl MultiBuffer { }) .collect::>(); - return autoindent_ranges_internal(self, snapshot, edits, cx); + return autoindent_ranges_internal(self, edits, cx); fn autoindent_ranges_internal( - this: &MultiBuffer, - snapshot: Ref, + this: &mut MultiBuffer, edits: Vec<(Range, Arc)>, cx: &mut Context, ) { - if this.read_only() || this.buffers.is_empty() { - return; - } - let (buffer_edits, edited_excerpt_ids) = - this.convert_edits_to_buffer_edits(edits, &snapshot, &[]); - drop(snapshot); + MultiBuffer::convert_edits_to_buffer_edits(edits, this.snapshot.get_mut(), &[]); let mut buffer_ids = Vec::new(); for (buffer_id, mut edits) in buffer_edits { @@ -1090,9 +1027,9 @@ impl MultiBuffer { } } - // Inserts newlines at the given position to create an empty line, returning the start of the new line. - // You can also request the insertion of empty lines above and below the line starting at the returned point. - // Panics if the given position is invalid. + /// Inserts newlines at the given position to create an empty line, returning the start of the new line. + /// You can also request the insertion of empty lines above and below the line starting at the returned point. + /// Panics if the given position is invalid. pub fn insert_empty_line( &mut self, position: impl ToPoint, @@ -1110,186 +1047,6 @@ impl MultiBuffer { multibuffer_point + (empty_line_start - buffer_point) } - pub fn start_transaction(&mut self, cx: &mut Context) -> Option { - self.start_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - self.history.start_transaction(now) - } - - pub fn last_transaction_id(&self, cx: &App) -> Option { - if let Some(buffer) = self.as_singleton() { - buffer - .read(cx) - .peek_undo_stack() - .map(|history_entry| history_entry.transaction_id()) - } else { - let last_transaction = self.history.undo_stack.last()?; - Some(last_transaction.id) - } - } - - pub fn end_transaction(&mut self, cx: &mut Context) -> Option { - self.end_transaction_at(Instant::now(), cx) - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); - } - - let mut buffer_transactions = HashMap::default(); - for BufferState { buffer, .. } in self.buffers.values() { - if let Some(transaction_id) = - buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); - } - } - - if self.history.end_transaction(now, buffer_transactions) { - let transaction_id = self.history.group().unwrap(); - Some(transaction_id) - } else { - None - } - } - - pub fn edited_ranges_for_transaction( - &self, - transaction_id: TransactionId, - cx: &App, - ) -> Vec> - where - D: TextDimension + Ord + Sub, - { - let Some(transaction) = self.history.transaction(transaction_id) else { - return Vec::new(); - }; - - let mut ranges = Vec::new(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::(()); - - for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { - let Some(buffer_state) = self.buffers.get(buffer_id) else { - continue; - }; - - let buffer = buffer_state.buffer.read(cx); - for range in buffer.edited_ranges_for_transaction_id::(*buffer_transaction) { - for excerpt_id in &buffer_state.excerpts { - cursor.seek(excerpt_id, Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *excerpt_id - { - let excerpt_buffer_start = excerpt.range.context.start.summary::(buffer); - let excerpt_buffer_end = excerpt.range.context.end.summary::(buffer); - let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; - if excerpt_range.contains(&range.start) - && excerpt_range.contains(&range.end) - { - let excerpt_start = D::from_text_summary(&cursor.start().text); - - let mut start = excerpt_start; - start.add_assign(&(range.start - excerpt_buffer_start)); - let mut end = excerpt_start; - end.add_assign(&(range.end - excerpt_buffer_start)); - - ranges.push(start..end); - break; - } - } - } - } - } - - ranges.sort_by_key(|range| range.start); - ranges - } - - pub fn merge_transactions( - &mut self, - transaction: TransactionId, - destination: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.merge_transactions(transaction, destination) - }); - } else if let Some(transaction) = self.history.forget(transaction) - && let Some(destination) = self.history.transaction_mut(destination) - { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(destination_buffer_transaction_id) = - destination.buffer_transactions.get(&buffer_id) - { - if let Some(state) = self.buffers.get(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.merge_transactions( - buffer_transaction_id, - *destination_buffer_transaction_id, - ) - }); - } - } else { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); - } - } - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut Context) { - self.history.finalize_last_transaction(); - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| { - buffer.finalize_last_transaction(); - }); - } - } - - pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context) - where - T: IntoIterator, &'a language::Transaction)>, - { - self.history - .push_transaction(buffer_transactions, Instant::now(), cx); - self.history.finalize_last_transaction(); - } - - pub fn group_until_transaction( - &mut self, - transaction_id: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.group_until_transaction(transaction_id) - }); - } else { - self.history.group_until(transaction_id); - } - } - pub fn set_active_selections( &self, selections: &[Selection], @@ -1357,325 +1114,30 @@ impl MultiBuffer { } } Some(selection) - })); - buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); - }); - } - } - - pub fn remove_active_selections(&self, cx: &mut Context) { - for buffer in self.buffers.values() { - buffer - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - pub fn undo(&mut self, cx: &mut Context) -> Option { - let mut transaction_id = None; - if let Some(buffer) = self.as_singleton() { - transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); - } else { - while let Some(transaction) = self.history.pop_undo() { - let mut undone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - undone |= buffer.update(cx, |buffer, cx| { - let undo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_undo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.undo_to_transaction(undo_to, cx) - }); - } - } - - if undone { - transaction_id = Some(transaction.id); - break; - } - } - } - - if let Some(transaction_id) = transaction_id { - cx.emit(Event::TransactionUndone { transaction_id }); - } - - transaction_id - } - - pub fn redo(&mut self, cx: &mut Context) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.redo(cx)); - } - - while let Some(transaction) = self.history.pop_redo() { - let mut redone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - redone |= buffer.update(cx, |buffer, cx| { - let redo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_redo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.redo_to_transaction(redo_to, cx) - }); - } - } - - if redone { - return Some(transaction.id); - } - } - - None - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); - } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { - for (buffer_id, transaction_id) in &transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.undo_transaction(*transaction_id, cx) - }); - } - } - } - } - - pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.forget_transaction(transaction_id); - }); - } else if let Some(transaction) = self.history.forget(transaction_id) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(state) = self.buffers.get_mut(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.forget_transaction(buffer_transaction_id); - }); - } - } - } - } - - pub fn push_excerpts( - &mut self, - buffer: Entity, - ranges: impl IntoIterator>, - cx: &mut Context, - ) -> Vec - where - O: text::ToOffset, - { - self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) - } - - pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { - let excerpt_id = self.excerpts_by_path.get(path)?.first()?; - let snapshot = self.snapshot(cx); - let excerpt = snapshot.excerpt(*excerpt_id)?; - Some(Anchor::in_buffer( - *excerpt_id, - excerpt.buffer_id, - excerpt.range.context.start, - )) - } - - pub fn excerpt_paths(&self) -> impl Iterator { - self.excerpts_by_path.keys() - } - - fn expand_excerpts_with_paths( - &mut self, - ids: impl IntoIterator, - line_count: u32, - direction: ExpandExcerptDirection, - cx: &mut Context, - ) { - let grouped = ids - .into_iter() - .chunk_by(|id| self.paths_by_excerpt.get(id).cloned()) - .into_iter() - .flat_map(|(k, v)| Some((k?, v.into_iter().collect::>()))) - .collect::>(); - let snapshot = self.snapshot(cx); - - for (path, ids) in grouped.into_iter() { - let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else { - continue; - }; - - let ids_to_expand = HashSet::from_iter(ids); - let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| { - let excerpt = snapshot.excerpt(*excerpt_id)?; - - let mut context = excerpt.range.context.to_point(&excerpt.buffer); - if ids_to_expand.contains(excerpt_id) { - match direction { - ExpandExcerptDirection::Up => { - context.start.row = context.start.row.saturating_sub(line_count); - context.start.column = 0; - } - ExpandExcerptDirection::Down => { - context.end.row = - (context.end.row + line_count).min(excerpt.buffer.max_point().row); - context.end.column = excerpt.buffer.line_len(context.end.row); - } - ExpandExcerptDirection::UpAndDown => { - context.start.row = context.start.row.saturating_sub(line_count); - context.start.column = 0; - context.end.row = - (context.end.row + line_count).min(excerpt.buffer.max_point().row); - context.end.column = excerpt.buffer.line_len(context.end.row); - } - } - } - - Some(ExcerptRange { - context, - primary: excerpt.range.primary.to_point(&excerpt.buffer), - }) - }); - let mut merged_ranges: Vec> = Vec::new(); - for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() - && last_range.context.end >= range.context.start - { - last_range.context.end = range.context.end; - continue; - } - merged_ranges.push(range) - } - let Some(excerpt_id) = excerpt_ids.first() else { - continue; - }; - let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else { - continue; - }; - - let Some(buffer) = self.buffers.get(buffer_id).map(|b| b.buffer.clone()) else { - continue; - }; - - let buffer_snapshot = buffer.read(cx).snapshot(); - self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx); - } - } - - /// Sets excerpts, returns `true` if at least one new excerpt was added. - pub fn set_excerpts_for_path( - &mut self, - path: PathKey, - buffer: Entity, - ranges: impl IntoIterator>, - context_line_count: u32, - cx: &mut Context, - ) -> (Vec>, bool) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot); - - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ) - } - - pub fn set_excerpt_ranges_for_path( - &mut self, - path: PathKey, - buffer: Entity, - buffer_snapshot: &BufferSnapshot, - excerpt_ranges: Vec>, - cx: &mut Context, - ) -> (Vec>, bool) { - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - buffer_snapshot, - new, - counts, - cx, - ) - } - - pub fn set_anchored_excerpts_for_path( - &self, - path_key: PathKey, - buffer: Entity, - ranges: Vec>, - context_line_count: u32, - cx: &mut Context, - ) -> Task>> { - let buffer_snapshot = buffer.read(cx).snapshot(); - cx.spawn(async move |multi_buffer, cx| { - let snapshot = buffer_snapshot.clone(); - let (excerpt_ranges, new, counts) = cx - .background_spawn(async move { - let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot)); - let excerpt_ranges = - build_excerpt_ranges(ranges, context_line_count, &snapshot); - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - (excerpt_ranges, new, counts) - }) - .await; - - multi_buffer - .update(cx, move |multi_buffer, cx| { - let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( - path_key, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ); - ranges - }) - .ok() - .unwrap_or_default() - }) + })); + buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); + }); + } + } + + pub fn remove_active_selections(&self, cx: &mut Context) { + for buffer in self.buffers.values() { + buffer + .buffer + .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); + } } - /// Sets excerpts, returns `true` if at least one new excerpt was added. - fn set_merged_excerpt_ranges_for_path( + pub fn push_excerpts( &mut self, - path: PathKey, buffer: Entity, - ranges: Vec>, - buffer_snapshot: &BufferSnapshot, - new: Vec>, - counts: Vec, + ranges: impl IntoIterator>, cx: &mut Context, - ) -> (Vec>, bool) { - let (excerpt_ids, added_a_new_excerpt) = - self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); - - let mut result = Vec::new(); - let mut ranges = ranges.into_iter(); - for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(counts.into_iter()) { - for range in ranges.by_ref().take(range_count) { - let range = Anchor::range_in_buffer( - excerpt_id, - buffer_snapshot.remote_id(), - buffer_snapshot.anchor_before(&range.primary.start) - ..buffer_snapshot.anchor_after(&range.primary.end), - ); - result.push(range) - } - } - (result, added_a_new_excerpt) + ) -> Vec + where + O: text::ToOffset, + { + self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) } fn merge_excerpt_ranges<'a>( @@ -1703,174 +1165,6 @@ impl MultiBuffer { (merged_ranges, counts) } - fn update_path_excerpts( - &mut self, - path: PathKey, - buffer: Entity, - buffer_snapshot: &BufferSnapshot, - new: Vec>, - cx: &mut Context, - ) -> (Vec, bool) { - let mut insert_after = self - .excerpts_by_path - .range(..path.clone()) - .next_back() - .map(|(_, value)| *value.last().unwrap()) - .unwrap_or(ExcerptId::min()); - - let existing = self - .excerpts_by_path - .get(&path) - .cloned() - .unwrap_or_default(); - - let mut new_iter = new.into_iter().peekable(); - let mut existing_iter = existing.into_iter().peekable(); - - let mut excerpt_ids = Vec::new(); - let mut to_remove = Vec::new(); - let mut to_insert: Vec<(ExcerptId, ExcerptRange)> = Vec::new(); - let mut added_a_new_excerpt = false; - let snapshot = self.snapshot(cx); - - let mut next_excerpt_id = - if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() { - last_entry.id.0 + 1 - } else { - 1 - }; - - let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id)); - - let mut excerpts_cursor = snapshot.excerpts.cursor::>(()); - excerpts_cursor.next(); - - loop { - let new = new_iter.peek(); - let existing = if let Some(existing_id) = existing_iter.peek() { - let locator = snapshot.excerpt_locator_for_id(*existing_id); - excerpts_cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts_cursor.item() { - if excerpt.buffer_id != buffer_snapshot.remote_id() { - to_remove.push(*existing_id); - existing_iter.next(); - continue; - } - Some(( - *existing_id, - excerpt.range.context.to_point(buffer_snapshot), - )) - } else { - None - } - } else { - None - }; - - if let Some((last_id, last)) = to_insert.last_mut() { - if let Some(new) = new - && last.context.end >= new.context.start - { - last.context.end = last.context.end.max(new.context.end); - excerpt_ids.push(*last_id); - new_iter.next(); - continue; - } - if let Some((existing_id, existing_range)) = &existing - && last.context.end >= existing_range.start - { - last.context.end = last.context.end.max(existing_range.end); - to_remove.push(*existing_id); - self.snapshot - .get_mut() - .replaced_excerpts - .insert(*existing_id, *last_id); - existing_iter.next(); - continue; - } - } - - match (new, existing) { - (None, None) => break, - (None, Some((existing_id, _))) => { - existing_iter.next(); - to_remove.push(existing_id); - continue; - } - (Some(_), None) => { - added_a_new_excerpt = true; - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - continue; - } - (Some(new), Some((_, existing_range))) => { - if existing_range.end < new.context.start { - let existing_id = existing_iter.next().unwrap(); - to_remove.push(existing_id); - continue; - } else if existing_range.start > new.context.end { - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - continue; - } - - if existing_range.start == new.context.start - && existing_range.end == new.context.end - { - self.insert_excerpts_with_ids_after( - insert_after, - buffer.clone(), - mem::take(&mut to_insert), - cx, - ); - insert_after = existing_iter.next().unwrap(); - excerpt_ids.push(insert_after); - new_iter.next(); - } else { - let existing_id = existing_iter.next().unwrap(); - let new_id = next_excerpt_id(); - self.snapshot - .get_mut() - .replaced_excerpts - .insert(existing_id, new_id); - to_remove.push(existing_id); - let mut range = new_iter.next().unwrap(); - range.context.start = range.context.start.min(existing_range.start); - range.context.end = range.context.end.max(existing_range.end); - excerpt_ids.push(new_id); - to_insert.push((new_id, range)); - } - } - }; - } - - self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx); - self.remove_excerpts(to_remove, cx); - if excerpt_ids.is_empty() { - self.excerpts_by_path.remove(&path); - } else { - for excerpt_id in &excerpt_ids { - self.paths_by_excerpt.insert(*excerpt_id, path.clone()); - } - self.excerpts_by_path - .insert(path, excerpt_ids.iter().dedup().cloned().collect()); - } - - (excerpt_ids, added_a_new_excerpt) - } - - pub fn paths(&self) -> impl Iterator + '_ { - self.excerpts_by_path.keys().cloned() - } - - pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { - if let Some(to_remove) = self.excerpts_by_path.remove(&path) { - self.remove_excerpts(to_remove, cx) - } - } - pub fn insert_excerpts_after( &mut self, prev_excerpt_id: ExcerptId, @@ -1910,13 +1204,13 @@ impl MultiBuffer { ) where O: text::ToOffset, { - assert_eq!(self.history.transaction_depth, 0); + assert_eq!(self.history.transaction_depth(), 0); let mut ranges = ranges.into_iter().peekable(); if ranges.peek().is_none() { return Default::default(); } - self.sync(cx); + self.sync_mut(cx); let buffer_snapshot = buffer.read(cx).snapshot(); let buffer_id = buffer_snapshot.remote_id(); @@ -2028,23 +1322,38 @@ impl MultiBuffer { } pub fn clear(&mut self, cx: &mut Context) { - self.sync(cx); + self.sync_mut(cx); let ids = self.excerpt_ids(); let removed_buffer_ids = self.buffers.drain().map(|(id, _)| id).collect(); self.excerpts_by_path.clear(); self.paths_by_excerpt.clear(); - let mut snapshot = self.snapshot.get_mut(); + let MultiBufferSnapshot { + excerpts, + diffs: _, + diff_transforms: _, + non_text_state_update_count: _, + edit_count: _, + is_dirty, + has_deleted_file, + has_conflict, + singleton: _, + excerpt_ids: _, + replaced_excerpts, + trailing_excerpt_update_count, + all_diff_hunks_expanded: _, + show_headers: _, + } = self.snapshot.get_mut(); let start = ExcerptOffset::new(0); - let prev_len = ExcerptOffset::new(snapshot.excerpts.summary().text.len); - snapshot.excerpts = Default::default(); - snapshot.trailing_excerpt_update_count += 1; - snapshot.is_dirty = false; - snapshot.has_deleted_file = false; - snapshot.has_conflict = false; - snapshot.replaced_excerpts.clear(); + let prev_len = ExcerptOffset::new(excerpts.summary().text.len); + *excerpts = Default::default(); + *trailing_excerpt_update_count += 1; + *is_dirty = false; + *has_deleted_file = false; + *has_conflict = false; + replaced_excerpts.clear(); let edits = Self::sync_diff_transforms( - &mut snapshot, + self.snapshot.get_mut(), vec![Edit { old: start..prev_len, new: start..start, @@ -2236,11 +1545,12 @@ impl MultiBuffer { excerpt_ids: impl IntoIterator, cx: &mut Context, ) { - self.sync(cx); + self.sync_mut(cx); let ids = excerpt_ids.into_iter().collect::>(); if ids.is_empty() { return; } + self.buffer_changed_since_sync.replace(true); let mut snapshot = self.snapshot.get_mut(); let mut new_excerpts = SumTree::default(); @@ -2327,7 +1637,6 @@ impl MultiBuffer { if !edits.is_empty() { self.subscriptions.publish(edits); } - self.buffer_changed_since_sync.replace(true); cx.emit(Event::Edited { edited_buffer: None, }); @@ -2408,12 +1717,10 @@ impl MultiBuffer { } fn buffer_diff_language_changed(&mut self, diff: Entity, cx: &mut Context) { - self.sync(cx); - let snapshot = self.snapshot.get_mut(); let diff = diff.read(cx); let buffer_id = diff.buffer_id; let diff = diff.snapshot(cx); - snapshot.diffs.insert(buffer_id, diff); + self.snapshot.get_mut().diffs.insert(buffer_id, diff); } fn buffer_diff_changed( @@ -2422,14 +1729,14 @@ impl MultiBuffer { range: Range, cx: &mut Context, ) { - self.sync(cx); - self.buffer_changed_since_sync.replace(true); + self.sync_mut(cx); let diff = diff.read(cx); let buffer_id = diff.buffer_id; let Some(buffer_state) = self.buffers.get(&buffer_id) else { return; }; + self.buffer_changed_since_sync.replace(true); let buffer = buffer_state.buffer.read(cx); let diff_change_range = range.to_offset(buffer); @@ -2728,7 +2035,7 @@ impl MultiBuffer { if self.snapshot.borrow().all_diff_hunks_expanded && !expand { return; } - self.sync(cx); + self.sync_mut(cx); let mut snapshot = self.snapshot.get_mut(); let mut excerpt_edits = Vec::new(); let mut last_hunk_row = None; @@ -2799,7 +2106,7 @@ impl MultiBuffer { range: Range, cx: &mut Context, ) { - self.sync(cx); + self.sync_mut(cx); let mut snapshot = self.snapshot.get_mut(); let locator = snapshot.excerpt_locator_for_id(id); @@ -2872,7 +2179,7 @@ impl MultiBuffer { if line_count == 0 { return; } - self.sync(cx); + self.sync_mut(cx); if !self.excerpts_by_path.is_empty() { self.expand_excerpts_with_paths(ids, line_count, direction, cx); return; @@ -2974,15 +2281,59 @@ impl MultiBuffer { if !changed { return; } + let edits = Self::sync_( + &mut self.snapshot.borrow_mut(), + &self.buffers, + &self.diffs, + cx, + ); + if !edits.is_empty() { + self.subscriptions.publish(edits); + } + } + + fn sync_mut(&mut self, cx: &App) { + let changed = self.buffer_changed_since_sync.replace(false); + if !changed { + return; + } + let edits = Self::sync_(self.snapshot.get_mut(), &self.buffers, &self.diffs, cx); + + if !edits.is_empty() { + self.subscriptions.publish(edits); + } + } + + fn sync_( + snapshot: &mut MultiBufferSnapshot, + buffers: &HashMap, + diffs: &HashMap, + cx: &App, + ) -> Vec> { + let MultiBufferSnapshot { + excerpts, + diffs: buffer_diff, + diff_transforms: _, + non_text_state_update_count, + edit_count, + is_dirty, + has_deleted_file, + has_conflict, + singleton: _, + excerpt_ids: _, + replaced_excerpts: _, + trailing_excerpt_update_count: _, + all_diff_hunks_expanded: _, + show_headers: _, + } = snapshot; + *is_dirty = false; + *has_deleted_file = false; + *has_conflict = false; - let mut snapshot = self.snapshot.borrow_mut(); let mut excerpts_to_edit = Vec::new(); let mut non_text_state_updated = false; - let mut is_dirty = false; - let mut has_deleted_file = false; - let mut has_conflict = false; let mut edited = false; - for buffer_state in self.buffers.values() { + for buffer_state in buffers.values() { let buffer = buffer_state.buffer.read(cx); let version = buffer.version(); let non_text_state_update_count = buffer.non_text_state_update_count(); @@ -3005,25 +2356,22 @@ impl MultiBuffer { edited |= buffer_edited; non_text_state_updated |= buffer_non_text_state_updated; - is_dirty |= buffer.is_dirty(); - has_deleted_file |= buffer + *is_dirty |= buffer.is_dirty(); + *has_deleted_file |= buffer .file() .is_some_and(|file| file.disk_state() == DiskState::Deleted); - has_conflict |= buffer.has_conflict(); + *has_conflict |= buffer.has_conflict(); } if edited { - snapshot.edit_count += 1; + *edit_count += 1; } if non_text_state_updated { - snapshot.non_text_state_update_count += 1; + *non_text_state_update_count += 1; } - snapshot.is_dirty = is_dirty; - snapshot.has_deleted_file = has_deleted_file; - snapshot.has_conflict = has_conflict; - for (id, diff) in self.diffs.iter() { - if snapshot.diffs.get(id).is_none() { - snapshot.diffs.insert(*id, diff.diff.read(cx).snapshot(cx)); + for (id, diff) in diffs.iter() { + if buffer_diff.get(id).is_none() { + buffer_diff.insert(*id, diff.diff.read(cx).snapshot(cx)); } } @@ -3031,9 +2379,7 @@ impl MultiBuffer { let mut edits = Vec::new(); let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); + let mut cursor = excerpts.cursor::, ExcerptOffset>>(()); for (locator, buffer, buffer_edited) in excerpts_to_edit { new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), ()); @@ -3083,12 +2429,8 @@ impl MultiBuffer { new_excerpts.append(cursor.suffix(), ()); drop(cursor); - snapshot.excerpts = new_excerpts; - - let edits = Self::sync_diff_transforms(&mut snapshot, edits, DiffChangeKind::BufferEdited); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } + *excerpts = new_excerpts; + Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited) } fn sync_diff_transforms( @@ -4365,10 +3707,18 @@ impl MultiBufferSnapshot { self.convert_dimension(point, text::BufferSnapshot::point_to_point_utf16) } + pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { + self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_point) + } + pub fn point_to_offset(&self, point: Point) -> usize { self.convert_dimension(point, text::BufferSnapshot::point_to_offset) } + pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { + self.convert_dimension(point, text::BufferSnapshot::point_to_offset_utf16) + } + pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { self.convert_dimension(offset, text::BufferSnapshot::offset_utf16_to_offset) } @@ -4381,6 +3731,10 @@ impl MultiBufferSnapshot { self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset) } + pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { + self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset_utf16) + } + fn clip_dimension( &self, position: D, @@ -6730,208 +6084,6 @@ where } } -impl History { - fn start_transaction(&mut self, now: Instant) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = self.next_transaction_id.tick(); - self.undo_stack.push(Transaction { - id, - buffer_transactions: Default::default(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction( - &mut self, - now: Instant, - buffer_transactions: HashMap, - ) -> bool { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if buffer_transactions.is_empty() { - self.undo_stack.pop(); - false - } else { - self.redo_stack.clear(); - let transaction = self.undo_stack.last_mut().unwrap(); - transaction.last_edit_at = now; - for (buffer_id, transaction_id) in buffer_transactions { - transaction - .buffer_transactions - .entry(buffer_id) - .or_insert(transaction_id); - } - true - } - } else { - false - } - } - - fn push_transaction<'a, T>( - &mut self, - buffer_transactions: T, - now: Instant, - cx: &Context, - ) where - T: IntoIterator, &'a language::Transaction)>, - { - assert_eq!(self.transaction_depth, 0); - let transaction = Transaction { - id: self.next_transaction_id.tick(), - buffer_transactions: buffer_transactions - .into_iter() - .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) - .collect(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }; - if !transaction.buffer_transactions.is_empty() { - self.undo_stack.push(transaction); - self.redo_stack.clear(); - } - } - - fn finalize_last_transaction(&mut self) { - if let Some(transaction) = self.undo_stack.last_mut() { - transaction.suppress_grouping = true; - } - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(ix) = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.undo_stack.remove(ix)) - } else if let Some(ix) = self - .redo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.redo_stack.remove(ix)) - } else { - None - } - } - - fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - self.undo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - self.undo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn pop_undo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.undo_stack.pop() { - self.redo_stack.push(transaction); - self.redo_stack.last_mut() - } else { - None - } - } - - fn pop_redo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.redo_stack.pop() { - self.undo_stack.push(transaction); - self.undo_stack.last_mut() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { - let ix = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id)?; - let transaction = self.undo_stack.remove(ix); - self.redo_stack.push(transaction); - self.redo_stack.last() - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut transactions = self.undo_stack.iter(); - if let Some(mut transaction) = transactions.next_back() { - while let Some(prev_transaction) = transactions.next_back() { - if !prev_transaction.suppress_grouping - && transaction.first_edit_at - prev_transaction.last_edit_at - <= self.group_interval - { - transaction = prev_transaction; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for transaction in self.undo_stack.iter().rev() { - if transaction.id == transaction_id { - self.group_trailing(count); - break; - } else if transaction.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_transaction) = transactions_to_keep.last_mut() { - if let Some(transaction) = transactions_to_merge.last() { - last_transaction.last_edit_at = transaction.last_edit_at; - } - for to_merge in transactions_to_merge { - for (buffer_id, transaction_id) in &to_merge.buffer_transactions { - last_transaction - .buffer_transactions - .entry(*buffer_id) - .or_insert(*transaction_id); - } - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|t| t.id) - } -} - impl Excerpt { fn new( id: ExcerptId, @@ -6959,21 +6111,16 @@ impl Excerpt { let chunks_start = content_start + range.start; let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); - let footer_height = if self.has_trailing_newline + let has_footer = self.has_trailing_newline && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; + && range.end > self.text_summary.len; let content_chunks = self.buffer.chunks(chunks_start..chunks_end, language_aware); ExcerptChunks { excerpt_id: self.id, content_chunks, - footer_height, + has_footer, } } @@ -6982,14 +6129,9 @@ impl Excerpt { let chunks_start = content_start + range.start; let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); excerpt_chunks.content_chunks.seek(chunks_start..chunks_end); - excerpt_chunks.footer_height = if self.has_trailing_newline + excerpt_chunks.has_footer = self.has_trailing_newline && range.start <= self.text_summary.len - && range.end > self.text_summary.len - { - 1 - } else { - 0 - }; + && range.end > self.text_summary.len; } fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { @@ -7879,12 +7021,10 @@ impl<'a> Iterator for ExcerptChunks<'a> { return Some(chunk); } - if self.footer_height > 0 { - let text = unsafe { str::from_utf8_unchecked(&NEWLINES[..self.footer_height]) }; - let chars = 1u128 - .unbounded_shl(self.footer_height as u32) - .wrapping_sub(1); - self.footer_height = 0; + if self.has_footer { + let text = "\n"; + let chars = 0b1; + self.has_footer = false; return Some(Chunk { text, chars, @@ -7900,6 +7040,9 @@ impl ToOffset for Point { fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { snapshot.point_to_offset(*self) } + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + snapshot.point_to_offset_utf16(*self) + } } impl ToOffset for usize { @@ -7913,29 +7056,27 @@ impl ToOffset for usize { ); *self } + fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { + snapshot.offset_to_offset_utf16(*self) + } } impl ToOffset for OffsetUtf16 { fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { snapshot.offset_utf16_to_offset(*self) } -} - -impl ToOffset for PointUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { - snapshot.point_utf16_to_offset(*self) - } -} -impl ToOffsetUtf16 for OffsetUtf16 { fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { *self } } -impl ToOffsetUtf16 for usize { +impl ToOffset for PointUtf16 { + fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> usize { + snapshot.point_utf16_to_offset(*self) + } fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> OffsetUtf16 { - snapshot.offset_to_offset_utf16(*self) + snapshot.point_utf16_to_offset_utf16(*self) } } @@ -7943,27 +7084,24 @@ impl ToPoint for usize { fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { snapshot.offset_to_point(*self) } + fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { + snapshot.offset_to_point_utf16(*self) + } } impl ToPoint for Point { fn to_point<'a>(&self, _: &MultiBufferSnapshot) -> Point { *self } -} - -impl ToPointUtf16 for usize { - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.offset_to_point_utf16(*self) - } -} - -impl ToPointUtf16 for Point { fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { snapshot.point_to_point_utf16(*self) } } -impl ToPointUtf16 for PointUtf16 { +impl ToPoint for PointUtf16 { + fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { + snapshot.point_utf16_to_point(*self) + } fn to_point_utf16<'a>(&self, _: &MultiBufferSnapshot) -> PointUtf16 { *self } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 49db1fc2e264583f90f1a96195c560f0e52e8205..a9121b9104400d88d5f22801db1bfebaeeb060d6 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -7,6 +7,7 @@ use parking_lot::RwLock; use rand::prelude::*; use settings::SettingsStore; use std::env; +use std::time::{Duration, Instant}; use util::RandomCharIter; use util::rel_path::rel_path; use util::test::sample_text; @@ -2984,7 +2985,7 @@ fn test_history(cx: &mut App) { }); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); multibuffer.update(cx, |this, _| { - this.history.group_interval = group_interval; + this.set_group_interval(group_interval); }); multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts( diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs new file mode 100644 index 0000000000000000000000000000000000000000..b6175b7aaab4f631728bcfaf8094120068032994 --- /dev/null +++ b/crates/multi_buffer/src/path_key.rs @@ -0,0 +1,417 @@ +use std::{mem, ops::Range, sync::Arc}; + +use collections::HashSet; +use gpui::{App, AppContext, Context, Entity, Task}; +use itertools::Itertools; +use language::{Buffer, BufferSnapshot}; +use rope::Point; +use text::{Bias, OffsetRangeExt, locator::Locator}; +use util::{post_inc, rel_path::RelPath}; + +use crate::{ + Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, build_excerpt_ranges, +}; + +#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)] +pub struct PathKey { + // Used by the derived PartialOrd & Ord + pub sort_prefix: Option, + pub path: Arc, +} + +impl PathKey { + pub fn with_sort_prefix(sort_prefix: u64, path: Arc) -> Self { + Self { + sort_prefix: Some(sort_prefix), + path, + } + } + + pub fn for_buffer(buffer: &Entity, cx: &App) -> Self { + if let Some(file) = buffer.read(cx).file() { + Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone()) + } else { + Self { + sort_prefix: None, + path: RelPath::unix(&buffer.entity_id().to_string()) + .unwrap() + .into_arc(), + } + } + } +} + +impl MultiBuffer { + pub fn paths(&self) -> impl Iterator + '_ { + self.excerpts_by_path.keys().cloned() + } + + pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { + if let Some(to_remove) = self.excerpts_by_path.remove(&path) { + self.remove_excerpts(to_remove, cx) + } + } + + pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { + let excerpt_id = self.excerpts_by_path.get(path)?.first()?; + let snapshot = self.read(cx); + let excerpt = snapshot.excerpt(*excerpt_id)?; + Some(Anchor::in_buffer( + *excerpt_id, + excerpt.buffer_id, + excerpt.range.context.start, + )) + } + + pub fn excerpt_paths(&self) -> impl Iterator { + self.excerpts_by_path.keys() + } + + /// Sets excerpts, returns `true` if at least one new excerpt was added. + pub fn set_excerpts_for_path( + &mut self, + path: PathKey, + buffer: Entity, + ranges: impl IntoIterator>, + context_line_count: u32, + cx: &mut Context, + ) -> (Vec>, bool) { + let buffer_snapshot = buffer.read(cx).snapshot(); + let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot); + + let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); + self.set_merged_excerpt_ranges_for_path( + path, + buffer, + excerpt_ranges, + &buffer_snapshot, + new, + counts, + cx, + ) + } + + pub fn set_excerpt_ranges_for_path( + &mut self, + path: PathKey, + buffer: Entity, + buffer_snapshot: &BufferSnapshot, + excerpt_ranges: Vec>, + cx: &mut Context, + ) -> (Vec>, bool) { + let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); + self.set_merged_excerpt_ranges_for_path( + path, + buffer, + excerpt_ranges, + buffer_snapshot, + new, + counts, + cx, + ) + } + + pub fn set_anchored_excerpts_for_path( + &self, + path_key: PathKey, + buffer: Entity, + ranges: Vec>, + context_line_count: u32, + cx: &mut Context, + ) -> Task>> { + let buffer_snapshot = buffer.read(cx).snapshot(); + cx.spawn(async move |multi_buffer, cx| { + let snapshot = buffer_snapshot.clone(); + let (excerpt_ranges, new, counts) = cx + .background_spawn(async move { + let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot)); + let excerpt_ranges = + build_excerpt_ranges(ranges, context_line_count, &snapshot); + let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); + (excerpt_ranges, new, counts) + }) + .await; + + multi_buffer + .update(cx, move |multi_buffer, cx| { + let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( + path_key, + buffer, + excerpt_ranges, + &buffer_snapshot, + new, + counts, + cx, + ); + ranges + }) + .ok() + .unwrap_or_default() + }) + } + + pub(super) fn expand_excerpts_with_paths( + &mut self, + ids: impl IntoIterator, + line_count: u32, + direction: ExpandExcerptDirection, + cx: &mut Context, + ) { + let grouped = ids + .into_iter() + .chunk_by(|id| self.paths_by_excerpt.get(id).cloned()) + .into_iter() + .flat_map(|(k, v)| Some((k?, v.into_iter().collect::>()))) + .collect::>(); + let snapshot = self.snapshot(cx); + + for (path, ids) in grouped.into_iter() { + let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else { + continue; + }; + + let ids_to_expand = HashSet::from_iter(ids); + let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| { + let excerpt = snapshot.excerpt(*excerpt_id)?; + + let mut context = excerpt.range.context.to_point(&excerpt.buffer); + if ids_to_expand.contains(excerpt_id) { + match direction { + ExpandExcerptDirection::Up => { + context.start.row = context.start.row.saturating_sub(line_count); + context.start.column = 0; + } + ExpandExcerptDirection::Down => { + context.end.row = + (context.end.row + line_count).min(excerpt.buffer.max_point().row); + context.end.column = excerpt.buffer.line_len(context.end.row); + } + ExpandExcerptDirection::UpAndDown => { + context.start.row = context.start.row.saturating_sub(line_count); + context.start.column = 0; + context.end.row = + (context.end.row + line_count).min(excerpt.buffer.max_point().row); + context.end.column = excerpt.buffer.line_len(context.end.row); + } + } + } + + Some(ExcerptRange { + context, + primary: excerpt.range.primary.to_point(&excerpt.buffer), + }) + }); + let mut merged_ranges: Vec> = Vec::new(); + for range in expanded_ranges { + if let Some(last_range) = merged_ranges.last_mut() + && last_range.context.end >= range.context.start + { + last_range.context.end = range.context.end; + continue; + } + merged_ranges.push(range) + } + let Some(excerpt_id) = excerpt_ids.first() else { + continue; + }; + let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(*excerpt_id) else { + continue; + }; + + let Some(buffer) = self.buffers.get(buffer_id).map(|b| b.buffer.clone()) else { + continue; + }; + + let buffer_snapshot = buffer.read(cx).snapshot(); + self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx); + } + } + + /// Sets excerpts, returns `true` if at least one new excerpt was added. + fn set_merged_excerpt_ranges_for_path( + &mut self, + path: PathKey, + buffer: Entity, + ranges: Vec>, + buffer_snapshot: &BufferSnapshot, + new: Vec>, + counts: Vec, + cx: &mut Context, + ) -> (Vec>, bool) { + let (excerpt_ids, added_a_new_excerpt) = + self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); + + let mut result = Vec::new(); + let mut ranges = ranges.into_iter(); + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(counts.into_iter()) { + for range in ranges.by_ref().take(range_count) { + let range = Anchor::range_in_buffer( + excerpt_id, + buffer_snapshot.remote_id(), + buffer_snapshot.anchor_before(&range.primary.start) + ..buffer_snapshot.anchor_after(&range.primary.end), + ); + result.push(range) + } + } + (result, added_a_new_excerpt) + } + + fn update_path_excerpts( + &mut self, + path: PathKey, + buffer: Entity, + buffer_snapshot: &BufferSnapshot, + new: Vec>, + cx: &mut Context, + ) -> (Vec, bool) { + let mut insert_after = self + .excerpts_by_path + .range(..path.clone()) + .next_back() + .map(|(_, value)| *value.last().unwrap()) + .unwrap_or(ExcerptId::min()); + + let existing = self + .excerpts_by_path + .get(&path) + .cloned() + .unwrap_or_default(); + + let mut new_iter = new.into_iter().peekable(); + let mut existing_iter = existing.into_iter().peekable(); + + let mut excerpt_ids = Vec::new(); + let mut to_remove = Vec::new(); + let mut to_insert: Vec<(ExcerptId, ExcerptRange)> = Vec::new(); + let mut added_a_new_excerpt = false; + let snapshot = self.snapshot(cx); + + let mut next_excerpt_id = + if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() { + last_entry.id.0 + 1 + } else { + 1 + }; + + let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id)); + + let mut excerpts_cursor = snapshot.excerpts.cursor::>(()); + excerpts_cursor.next(); + + loop { + let new = new_iter.peek(); + let existing = if let Some(existing_id) = existing_iter.peek() { + let locator = snapshot.excerpt_locator_for_id(*existing_id); + excerpts_cursor.seek_forward(&Some(locator), Bias::Left); + if let Some(excerpt) = excerpts_cursor.item() { + if excerpt.buffer_id != buffer_snapshot.remote_id() { + to_remove.push(*existing_id); + existing_iter.next(); + continue; + } + Some(( + *existing_id, + excerpt.range.context.to_point(buffer_snapshot), + )) + } else { + None + } + } else { + None + }; + + if let Some((last_id, last)) = to_insert.last_mut() { + if let Some(new) = new + && last.context.end >= new.context.start + { + last.context.end = last.context.end.max(new.context.end); + excerpt_ids.push(*last_id); + new_iter.next(); + continue; + } + if let Some((existing_id, existing_range)) = &existing + && last.context.end >= existing_range.start + { + last.context.end = last.context.end.max(existing_range.end); + to_remove.push(*existing_id); + self.snapshot + .get_mut() + .replaced_excerpts + .insert(*existing_id, *last_id); + existing_iter.next(); + continue; + } + } + + match (new, existing) { + (None, None) => break, + (None, Some((existing_id, _))) => { + existing_iter.next(); + to_remove.push(existing_id); + continue; + } + (Some(_), None) => { + added_a_new_excerpt = true; + let new_id = next_excerpt_id(); + excerpt_ids.push(new_id); + to_insert.push((new_id, new_iter.next().unwrap())); + continue; + } + (Some(new), Some((_, existing_range))) => { + if existing_range.end < new.context.start { + let existing_id = existing_iter.next().unwrap(); + to_remove.push(existing_id); + continue; + } else if existing_range.start > new.context.end { + let new_id = next_excerpt_id(); + excerpt_ids.push(new_id); + to_insert.push((new_id, new_iter.next().unwrap())); + continue; + } + + if existing_range.start == new.context.start + && existing_range.end == new.context.end + { + self.insert_excerpts_with_ids_after( + insert_after, + buffer.clone(), + mem::take(&mut to_insert), + cx, + ); + insert_after = existing_iter.next().unwrap(); + excerpt_ids.push(insert_after); + new_iter.next(); + } else { + let existing_id = existing_iter.next().unwrap(); + let new_id = next_excerpt_id(); + self.snapshot + .get_mut() + .replaced_excerpts + .insert(existing_id, new_id); + to_remove.push(existing_id); + let mut range = new_iter.next().unwrap(); + range.context.start = range.context.start.min(existing_range.start); + range.context.end = range.context.end.max(existing_range.end); + excerpt_ids.push(new_id); + to_insert.push((new_id, range)); + } + } + }; + } + + self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx); + self.remove_excerpts(to_remove, cx); + if excerpt_ids.is_empty() { + self.excerpts_by_path.remove(&path); + } else { + for excerpt_id in &excerpt_ids { + self.paths_by_excerpt.insert(*excerpt_id, path.clone()); + } + self.excerpts_by_path + .insert(path, excerpt_ids.iter().dedup().cloned().collect()); + } + + (excerpt_ids, added_a_new_excerpt) + } +} diff --git a/crates/multi_buffer/src/transaction.rs b/crates/multi_buffer/src/transaction.rs new file mode 100644 index 0000000000000000000000000000000000000000..062d25d8233777190113aaa3e6a7f62396cfd08f --- /dev/null +++ b/crates/multi_buffer/src/transaction.rs @@ -0,0 +1,524 @@ +use gpui::{App, Context, Entity}; +use language::{self, Buffer, TextDimension, TransactionId}; +use std::{ + collections::HashMap, + ops::{Range, Sub}, + time::{Duration, Instant}, +}; +use sum_tree::Bias; +use text::BufferId; + +use crate::BufferState; + +use super::{Event, ExcerptSummary, MultiBuffer}; + +#[derive(Clone)] +pub(super) struct History { + next_transaction_id: TransactionId, + undo_stack: Vec, + redo_stack: Vec, + transaction_depth: usize, + group_interval: Duration, +} + +impl Default for History { + fn default() -> Self { + History { + next_transaction_id: clock::Lamport::MIN, + undo_stack: Vec::new(), + redo_stack: Vec::new(), + transaction_depth: 0, + group_interval: Duration::from_millis(300), + } + } +} + +#[derive(Clone)] +struct Transaction { + id: TransactionId, + buffer_transactions: HashMap, + first_edit_at: Instant, + last_edit_at: Instant, + suppress_grouping: bool, +} + +impl History { + fn start_transaction(&mut self, now: Instant) -> Option { + self.transaction_depth += 1; + if self.transaction_depth == 1 { + let id = self.next_transaction_id.tick(); + self.undo_stack.push(Transaction { + id, + buffer_transactions: Default::default(), + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }); + Some(id) + } else { + None + } + } + + fn end_transaction( + &mut self, + now: Instant, + buffer_transactions: HashMap, + ) -> bool { + assert_ne!(self.transaction_depth, 0); + self.transaction_depth -= 1; + if self.transaction_depth == 0 { + if buffer_transactions.is_empty() { + self.undo_stack.pop(); + false + } else { + self.redo_stack.clear(); + let transaction = self.undo_stack.last_mut().unwrap(); + transaction.last_edit_at = now; + for (buffer_id, transaction_id) in buffer_transactions { + transaction + .buffer_transactions + .entry(buffer_id) + .or_insert(transaction_id); + } + true + } + } else { + false + } + } + + fn push_transaction<'a, T>( + &mut self, + buffer_transactions: T, + now: Instant, + cx: &Context, + ) where + T: IntoIterator, &'a language::Transaction)>, + { + assert_eq!(self.transaction_depth, 0); + let transaction = Transaction { + id: self.next_transaction_id.tick(), + buffer_transactions: buffer_transactions + .into_iter() + .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) + .collect(), + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }; + if !transaction.buffer_transactions.is_empty() { + self.undo_stack.push(transaction); + self.redo_stack.clear(); + } + } + + fn finalize_last_transaction(&mut self) { + if let Some(transaction) = self.undo_stack.last_mut() { + transaction.suppress_grouping = true; + } + } + + fn forget(&mut self, transaction_id: TransactionId) -> Option { + if let Some(ix) = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.undo_stack.remove(ix)) + } else if let Some(ix) = self + .redo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id) + { + Some(self.redo_stack.remove(ix)) + } else { + None + } + } + + fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { + self.undo_stack + .iter() + .find(|transaction| transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter() + .find(|transaction| transaction.id == transaction_id) + }) + } + + fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { + self.undo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + .or_else(|| { + self.redo_stack + .iter_mut() + .find(|transaction| transaction.id == transaction_id) + }) + } + + fn pop_undo(&mut self) -> Option<&mut Transaction> { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction) = self.undo_stack.pop() { + self.redo_stack.push(transaction); + self.redo_stack.last_mut() + } else { + None + } + } + + fn pop_redo(&mut self) -> Option<&mut Transaction> { + assert_eq!(self.transaction_depth, 0); + if let Some(transaction) = self.redo_stack.pop() { + self.undo_stack.push(transaction); + self.undo_stack.last_mut() + } else { + None + } + } + + fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { + let ix = self + .undo_stack + .iter() + .rposition(|transaction| transaction.id == transaction_id)?; + let transaction = self.undo_stack.remove(ix); + self.redo_stack.push(transaction); + self.redo_stack.last() + } + + fn group(&mut self) -> Option { + let mut count = 0; + let mut transactions = self.undo_stack.iter(); + if let Some(mut transaction) = transactions.next_back() { + while let Some(prev_transaction) = transactions.next_back() { + if !prev_transaction.suppress_grouping + && transaction.first_edit_at - prev_transaction.last_edit_at + <= self.group_interval + { + transaction = prev_transaction; + count += 1; + } else { + break; + } + } + } + self.group_trailing(count) + } + + fn group_until(&mut self, transaction_id: TransactionId) { + let mut count = 0; + for transaction in self.undo_stack.iter().rev() { + if transaction.id == transaction_id { + self.group_trailing(count); + break; + } else if transaction.suppress_grouping { + break; + } else { + count += 1; + } + } + } + + fn group_trailing(&mut self, n: usize) -> Option { + let new_len = self.undo_stack.len() - n; + let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); + if let Some(last_transaction) = transactions_to_keep.last_mut() { + if let Some(transaction) = transactions_to_merge.last() { + last_transaction.last_edit_at = transaction.last_edit_at; + } + for to_merge in transactions_to_merge { + for (buffer_id, transaction_id) in &to_merge.buffer_transactions { + last_transaction + .buffer_transactions + .entry(*buffer_id) + .or_insert(*transaction_id); + } + } + } + + self.undo_stack.truncate(new_len); + self.undo_stack.last().map(|t| t.id) + } + + pub(super) fn transaction_depth(&self) -> usize { + self.transaction_depth + } + + pub fn set_group_interval(&mut self, group_interval: Duration) { + self.group_interval = group_interval; + } +} + +impl MultiBuffer { + pub fn start_transaction(&mut self, cx: &mut Context) -> Option { + self.start_transaction_at(Instant::now(), cx) + } + + pub fn start_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); + } + + for BufferState { buffer, .. } in self.buffers.values() { + buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); + } + self.history.start_transaction(now) + } + + pub fn last_transaction_id(&self, cx: &App) -> Option { + if let Some(buffer) = self.as_singleton() { + buffer + .read(cx) + .peek_undo_stack() + .map(|history_entry| history_entry.transaction_id()) + } else { + let last_transaction = self.history.undo_stack.last()?; + Some(last_transaction.id) + } + } + + pub fn end_transaction(&mut self, cx: &mut Context) -> Option { + self.end_transaction_at(Instant::now(), cx) + } + + pub fn end_transaction_at( + &mut self, + now: Instant, + cx: &mut Context, + ) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); + } + + let mut buffer_transactions = HashMap::default(); + for BufferState { buffer, .. } in self.buffers.values() { + if let Some(transaction_id) = + buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) + { + buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); + } + } + + if self.history.end_transaction(now, buffer_transactions) { + let transaction_id = self.history.group().unwrap(); + Some(transaction_id) + } else { + None + } + } + + pub fn edited_ranges_for_transaction( + &self, + transaction_id: TransactionId, + cx: &App, + ) -> Vec> + where + D: TextDimension + Ord + Sub, + { + let Some(transaction) = self.history.transaction(transaction_id) else { + return Vec::new(); + }; + + let mut ranges = Vec::new(); + let snapshot = self.read(cx); + let mut cursor = snapshot.excerpts.cursor::(()); + + for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { + let Some(buffer_state) = self.buffers.get(buffer_id) else { + continue; + }; + + let buffer = buffer_state.buffer.read(cx); + for range in buffer.edited_ranges_for_transaction_id::(*buffer_transaction) { + for excerpt_id in &buffer_state.excerpts { + cursor.seek(excerpt_id, Bias::Left); + if let Some(excerpt) = cursor.item() + && excerpt.locator == *excerpt_id + { + let excerpt_buffer_start = excerpt.range.context.start.summary::(buffer); + let excerpt_buffer_end = excerpt.range.context.end.summary::(buffer); + let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; + if excerpt_range.contains(&range.start) + && excerpt_range.contains(&range.end) + { + let excerpt_start = D::from_text_summary(&cursor.start().text); + + let mut start = excerpt_start; + start.add_assign(&(range.start - excerpt_buffer_start)); + let mut end = excerpt_start; + end.add_assign(&(range.end - excerpt_buffer_start)); + + ranges.push(start..end); + break; + } + } + } + } + } + + ranges.sort_by_key(|range| range.start); + ranges + } + + pub fn merge_transactions( + &mut self, + transaction: TransactionId, + destination: TransactionId, + cx: &mut Context, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.merge_transactions(transaction, destination) + }); + } else if let Some(transaction) = self.history.forget(transaction) + && let Some(destination) = self.history.transaction_mut(destination) + { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(destination_buffer_transaction_id) = + destination.buffer_transactions.get(&buffer_id) + { + if let Some(state) = self.buffers.get(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.merge_transactions( + buffer_transaction_id, + *destination_buffer_transaction_id, + ) + }); + } + } else { + destination + .buffer_transactions + .insert(buffer_id, buffer_transaction_id); + } + } + } + } + + pub fn finalize_last_transaction(&mut self, cx: &mut Context) { + self.history.finalize_last_transaction(); + for BufferState { buffer, .. } in self.buffers.values() { + buffer.update(cx, |buffer, _| { + buffer.finalize_last_transaction(); + }); + } + } + + pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context) + where + T: IntoIterator, &'a language::Transaction)>, + { + self.history + .push_transaction(buffer_transactions, Instant::now(), cx); + self.history.finalize_last_transaction(); + } + + pub fn group_until_transaction( + &mut self, + transaction_id: TransactionId, + cx: &mut Context, + ) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.group_until_transaction(transaction_id) + }); + } else { + self.history.group_until(transaction_id); + } + } + pub fn undo(&mut self, cx: &mut Context) -> Option { + let mut transaction_id = None; + if let Some(buffer) = self.as_singleton() { + transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); + } else { + while let Some(transaction) = self.history.pop_undo() { + let mut undone = false; + for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { + undone |= buffer.update(cx, |buffer, cx| { + let undo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_undo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.undo_to_transaction(undo_to, cx) + }); + } + } + + if undone { + transaction_id = Some(transaction.id); + break; + } + } + } + + if let Some(transaction_id) = transaction_id { + cx.emit(Event::TransactionUndone { transaction_id }); + } + + transaction_id + } + + pub fn redo(&mut self, cx: &mut Context) -> Option { + if let Some(buffer) = self.as_singleton() { + return buffer.update(cx, |buffer, cx| buffer.redo(cx)); + } + + while let Some(transaction) = self.history.pop_redo() { + let mut redone = false; + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions.iter_mut() { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { + redone |= buffer.update(cx, |buffer, cx| { + let redo_to = *buffer_transaction_id; + if let Some(entry) = buffer.peek_redo_stack() { + *buffer_transaction_id = entry.transaction_id(); + } + buffer.redo_to_transaction(redo_to, cx) + }); + } + } + + if redone { + return Some(transaction.id); + } + } + + None + } + + pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { + for (buffer_id, transaction_id) in &transaction.buffer_transactions { + if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { + buffer.update(cx, |buffer, cx| { + buffer.undo_transaction(*transaction_id, cx) + }); + } + } + } + } + + pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { + if let Some(buffer) = self.as_singleton() { + buffer.update(cx, |buffer, _| { + buffer.forget_transaction(transaction_id); + }); + } else if let Some(transaction) = self.history.forget(transaction_id) { + for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { + if let Some(state) = self.buffers.get_mut(&buffer_id) { + state.buffer.update(cx, |buffer, _| { + buffer.forget_transaction(buffer_transaction_id); + }); + } + } + } + } +} diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs index d0be336c9faf2c5834182387307a7775ba00db38..2fa6112dd439a5835891db813dc9ce12cb22809d 100644 --- a/crates/rope/src/chunk.rs +++ b/crates/rope/src/chunk.rs @@ -448,6 +448,19 @@ impl<'a> ChunkSlice<'a> { } } + #[inline(always)] + pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { + if point.row > self.lines().row { + debug_panic!( + "point {:?} extends beyond rows for string {:?}", + point, + self.text + ); + return self.len_utf16(); + } + self.offset_to_offset_utf16(self.point_to_offset(point)) + } + #[inline(always)] pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 5a43e22ea5ef43c5b31aeb63d52dcecdea72f5fe..0195f61dcb30bdc85ae3dbe541fa5fba5f76a2c9 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -440,6 +440,21 @@ impl Rope { }) } + pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { + if point >= self.summary().lines_utf16() { + return self.summary().lines; + } + let mut cursor = self.chunks.cursor::>(()); + cursor.seek(&point, Bias::Left); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(Point::zero(), |chunk| { + chunk + .as_slice() + .offset_to_point(chunk.as_slice().point_utf16_to_offset(overshoot, false)) + }) + } + pub fn point_to_offset(&self, point: Point) -> usize { if point >= self.summary().lines { return self.summary().len; @@ -451,10 +466,27 @@ impl Rope { start.1 + item.map_or(0, |chunk| chunk.as_slice().point_to_offset(overshoot)) } + pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { + if point >= self.summary().lines { + return self.summary().len_utf16; + } + let mut cursor = self.chunks.cursor::>(()); + cursor.seek(&point, Bias::Left); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(OffsetUtf16(0), |chunk| { + chunk.as_slice().point_to_offset_utf16(overshoot) + }) + } + pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { self.point_utf16_to_offset_impl(point, false) } + pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { + self.point_utf16_to_offset_utf16_impl(point, false) + } + pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { self.point_utf16_to_offset_impl(point.0, true) } @@ -473,6 +505,23 @@ impl Rope { }) } + fn point_utf16_to_offset_utf16_impl(&self, point: PointUtf16, clip: bool) -> OffsetUtf16 { + if point >= self.summary().lines_utf16() { + return self.summary().len_utf16; + } + let mut cursor = self + .chunks + .cursor::>(()); + cursor.seek(&point, Bias::Left); + let overshoot = point - cursor.start().0; + cursor.start().1 + + cursor.item().map_or(OffsetUtf16(0), |chunk| { + chunk + .as_slice() + .offset_to_offset_utf16(chunk.as_slice().point_utf16_to_offset(overshoot, clip)) + }) + } + pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { if point.0 >= self.summary().lines_utf16() { return self.summary().lines; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index d30a3dca0d5a3a5809440b816b9491f7f1d940c8..0634212c8ca41de539c9791193321cce77c9263e 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2051,6 +2051,14 @@ impl BufferSnapshot { self.visible_text.point_to_offset(point) } + pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { + self.visible_text.point_to_offset_utf16(point) + } + + pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { + self.visible_text.point_utf16_to_offset_utf16(point) + } + pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { self.visible_text.point_utf16_to_offset(point) } @@ -2083,6 +2091,10 @@ impl BufferSnapshot { self.visible_text.point_to_point_utf16(point) } + pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { + self.visible_text.point_utf16_to_point(point) + } + pub fn version(&self) -> &clock::Global { &self.version } From 55bc679c196e0622449471bf2233ff935ab2b1d4 Mon Sep 17 00:00:00 2001 From: Sean Hagstrom Date: Thu, 23 Oct 2025 11:13:35 -0700 Subject: [PATCH 202/202] docs: Ensure macOS and Linux keybindings are escaped in HTML (#39802) Closes #39654 Release Notes: - Fixed the formatting of macOS and Linux keybindings in the Zed docs to escape the backslash character when templating. --- crates/docs_preprocessor/src/main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 747cb2ecdd8193e6a88f93779d01c95aef54cb70..b614a8251139413f4b316937db1d4e3c0d551df6 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -203,6 +203,10 @@ fn template_big_table_of_actions(book: &mut Book) { }); } +fn format_binding(binding: String) -> String { + binding.replace("\\", "\\\\") +} + fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); @@ -223,7 +227,10 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSetNo default binding
".to_string(); } - format!("{macos_binding}|{linux_binding}") + let formatted_macos_binding = format_binding(macos_binding); + let formatted_linux_binding = format_binding(linux_binding); + + format!("{formatted_macos_binding}|{formatted_linux_binding}") }) .into_owned() });