From 6c5791532ea2dbea26167ed79a832f075707ad9a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:07:36 +0200 Subject: [PATCH 01/35] lsp: Remove Attach enum, default to Shared behaviour (#35248) This should be a no-op PR, behavior-wise. Release Notes: - N/A --- crates/language/src/language.rs | 29 --------- crates/project/src/lsp_store.rs | 63 +++---------------- .../project/src/manifest_tree/server_tree.rs | 23 +++---- 3 files changed, 14 insertions(+), 101 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7cda2b4b5ade9bfe36e595e25f8a7f1f9ce1ff8d..549afc931c8ba52389b633ef4823b98b5cd0e931 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -166,7 +166,6 @@ pub struct CachedLspAdapter { pub reinstall_attempt_count: AtomicU64, cached_binary: futures::lock::Mutex>, manifest_name: OnceLock>, - attach_kind: OnceLock, } impl Debug for CachedLspAdapter { @@ -202,7 +201,6 @@ impl CachedLspAdapter { adapter, cached_binary: Default::default(), reinstall_attempt_count: AtomicU64::new(0), - attach_kind: Default::default(), manifest_name: Default::default(), }) } @@ -288,29 +286,6 @@ impl CachedLspAdapter { .get_or_init(|| self.adapter.manifest_name()) .clone() } - pub fn attach_kind(&self) -> Attach { - *self.attach_kind.get_or_init(|| self.adapter.attach_kind()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Attach { - /// Create a single language server instance per subproject root. - InstancePerRoot, - /// Use one shared language server instance for all subprojects within a project. - Shared, -} - -impl Attach { - pub fn root_path( - &self, - root_subproject_path: (WorktreeId, Arc), - ) -> (WorktreeId, Arc) { - match self { - Attach::InstancePerRoot => root_subproject_path, - Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))), - } - } } /// Determines what gets sent out as a workspace folders content @@ -611,10 +586,6 @@ pub trait LspAdapter: 'static + Send + Sync { Ok(original) } - fn attach_kind(&self) -> Attach { - Attach::Shared - } - /// Determines whether a language server supports workspace folders. /// /// And does not trip over itself in the process. diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index defe056dd8a0430501ad24a2348e080defa3df19..5b523c9a03da1428e96a7377921ca76466c790d4 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -2425,36 +2425,12 @@ impl LocalLspStore { let server_id = server_node.server_id_or_init( |LaunchDisposition { server_name, - attach, + path, settings, }| { - let server_id = match attach { - language::Attach::InstancePerRoot => { - // todo: handle instance per root proper. - if let Some(server_ids) = self - .language_server_ids - .get(&(worktree_id, server_name.clone())) - { - server_ids.iter().cloned().next().unwrap() - } else { - let language_name = language.name(); - let adapter = self.languages - .lsp_adapters(&language_name) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"); - let server_id = self.start_language_server( - &worktree, - delegate.clone(), - adapter, - settings, - cx, - ); - server_id - } - } - language::Attach::Shared => { + let server_id = + { let uri = Url::from_file_path( worktree.read(cx).abs_path().join(&path.path), ); @@ -2489,7 +2465,7 @@ impl LocalLspStore { } else { unreachable!("Language server ID should be available, as it's registered on demand") } - } + }; let lsp_store = self.weak.clone(); let server_name = server_node.name(); @@ -4705,35 +4681,11 @@ impl LspStore { let server_id = node.server_id_or_init( |LaunchDisposition { server_name, - attach, + path, settings, - }| match attach { - language::Attach::InstancePerRoot => { - // todo: handle instance per root proper. - if let Some(server_ids) = local - .language_server_ids - .get(&(worktree_id, server_name.clone())) - { - server_ids.iter().cloned().next().unwrap() - } else { - let adapter = local - .languages - .lsp_adapters(&language) - .into_iter() - .find(|adapter| &adapter.name() == server_name) - .expect("To find LSP adapter"); - let server_id = local.start_language_server( - &worktree, - delegate.clone(), - adapter, - settings, - cx, - ); - server_id - } - } - language::Attach::Shared => { + }| + { let uri = Url::from_file_path( worktree.read(cx).abs_path().join(&path.path), ); @@ -4762,7 +4714,6 @@ impl LspStore { } server_id } - }, ); if let Some(language_server_id) = server_id { diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 0283f06eec0f2859f99bddb0e5be10bb8f4197fa..81cb1c450c4626bfa691c98e88d26536705dfb3d 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -13,10 +13,10 @@ use std::{ sync::{Arc, Weak}, }; -use collections::{HashMap, IndexMap}; +use collections::IndexMap; use gpui::{App, AppContext as _, Entity, Subscription}; use language::{ - Attach, CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, + CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate, language_settings::AllLanguageSettings, }; use lsp::LanguageServerName; @@ -38,7 +38,6 @@ pub(crate) struct ServersForWorktree { pub struct LanguageServerTree { manifest_tree: Entity, pub(crate) instances: BTreeMap, - attach_kind_cache: HashMap, languages: Arc, _subscriptions: Subscription, } @@ -53,7 +52,6 @@ pub struct LanguageServerTreeNode(Weak); #[derive(Debug)] pub(crate) struct LaunchDisposition<'a> { pub(crate) server_name: &'a LanguageServerName, - pub(crate) attach: Attach, pub(crate) path: ProjectPath, pub(crate) settings: Arc, } @@ -62,7 +60,6 @@ impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> { fn from(value: &'a InnerTreeNode) -> Self { LaunchDisposition { server_name: &value.name, - attach: value.attach, path: value.path.clone(), settings: value.settings.clone(), } @@ -105,7 +102,6 @@ impl From> for LanguageServerTreeNode { pub struct InnerTreeNode { id: OnceLock, name: LanguageServerName, - attach: Attach, path: ProjectPath, settings: Arc, } @@ -113,14 +109,12 @@ pub struct InnerTreeNode { impl InnerTreeNode { fn new( name: LanguageServerName, - attach: Attach, path: ProjectPath, settings: impl Into>, ) -> Self { InnerTreeNode { id: Default::default(), name, - attach, path, settings: settings.into(), } @@ -130,8 +124,11 @@ impl InnerTreeNode { /// Determines how the list of adapters to query should be constructed. pub(crate) enum AdapterQuery<'a> { /// Search for roots of all adapters associated with a given language name. + /// Layman: Look for all project roots along the queried path that have any + /// language server associated with this language running. Language(&'a LanguageName), /// Search for roots of adapter with a given name. + /// Layman: Look for all project roots along the queried path that have this server running. Adapter(&'a LanguageServerName), } @@ -147,7 +144,7 @@ impl LanguageServerTree { }), manifest_tree, instances: Default::default(), - attach_kind_cache: Default::default(), + languages, }) } @@ -223,7 +220,6 @@ impl LanguageServerTree { .and_then(|name| roots.get(&name)) .cloned() .unwrap_or_else(|| root_path.clone()); - let attach = adapter.attach_kind(); let inner_node = self .instances @@ -237,7 +233,6 @@ impl LanguageServerTree { ( Arc::new(InnerTreeNode::new( adapter.name(), - attach, root_path.clone(), settings.clone(), )), @@ -379,7 +374,6 @@ pub(crate) struct ServerTreeRebase<'a> { impl<'tree> ServerTreeRebase<'tree> { fn new(new_tree: &'tree mut LanguageServerTree) -> Self { let old_contents = std::mem::take(&mut new_tree.instances); - new_tree.attach_kind_cache.clear(); let all_server_ids = old_contents .values() .flat_map(|nodes| { @@ -446,10 +440,7 @@ impl<'tree> ServerTreeRebase<'tree> { .get(&disposition.path.worktree_id) .and_then(|worktree_nodes| worktree_nodes.roots.get(&disposition.path.path)) .and_then(|roots| roots.get(&disposition.name)) - .filter(|(old_node, _)| { - disposition.attach == old_node.attach - && disposition.settings == old_node.settings - }) + .filter(|(old_node, _)| disposition.settings == old_node.settings) else { return Some(node); }; From 8f952f1b5812abe9fa88b5f60d5df1660c295c1a Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 29 Jul 2025 12:30:38 +0200 Subject: [PATCH 02/35] gpui: Ensure first tab index is selected on first focus (#35247) This fixes an issue with tab indices where we would actually focus the second focus handle on first focus instead of the first one. The test was updated accordingly. Release Notes: - N/A --------- Co-authored-by: Jason Lee --- crates/gpui/examples/tab_stop.rs | 15 ++++++++++++--- crates/gpui/src/tab_stop.rs | 32 ++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/crates/gpui/examples/tab_stop.rs b/crates/gpui/examples/tab_stop.rs index 9c58b52a5e93b154237f8822e6abc86a237c2d02..1f6500f3e63a1000b4e32d6482ef1d1b4867fb8b 100644 --- a/crates/gpui/examples/tab_stop.rs +++ b/crates/gpui/examples/tab_stop.rs @@ -6,6 +6,7 @@ use gpui::{ actions!(example, [Tab, TabPrev]); struct Example { + focus_handle: FocusHandle, items: Vec, message: SharedString, } @@ -20,8 +21,11 @@ impl Example { cx.focus_handle().tab_index(2).tab_stop(true), ]; - window.focus(items.first().unwrap()); + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle); + Self { + focus_handle, items, message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."), } @@ -40,6 +44,10 @@ impl Example { impl Render for Example { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn tab_stop_style(this: T) -> T { + this.border_3().border_color(gpui::blue()) + } + fn button(id: impl Into) -> Stateful
{ div() .id(id) @@ -52,12 +60,13 @@ impl Render for Example { .border_color(gpui::black()) .bg(gpui::black()) .text_color(gpui::white()) - .focus(|this| this.border_color(gpui::blue())) + .focus(tab_stop_style) .shadow_sm() } div() .id("app") + .track_focus(&self.focus_handle) .on_action(cx.listener(Self::on_tab)) .on_action(cx.listener(Self::on_tab_prev)) .size_full() @@ -86,7 +95,7 @@ impl Render for Example { .border_color(gpui::black()) .when( item_handle.tab_stop && item_handle.is_focused(window), - |this| this.border_color(gpui::blue()), + tab_stop_style, ) .map(|this| match item_handle.tab_stop { true => this diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 1aa4cd6d9fbb1815fb8e566a223fc8d85cf304b1..7dde42efed8a138de3a29657683d95c60e27dda0 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -32,20 +32,18 @@ impl TabHandles { self.handles.clear(); } - fn current_index(&self, focused_id: Option<&FocusId>) -> usize { - self.handles - .iter() - .position(|h| Some(&h.id) == focused_id) - .unwrap_or_default() + fn current_index(&self, focused_id: Option<&FocusId>) -> Option { + self.handles.iter().position(|h| Some(&h.id) == focused_id) } pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option { - let ix = self.current_index(focused_id); - - let mut next_ix = ix + 1; - if next_ix + 1 > self.handles.len() { - next_ix = 0; - } + let next_ix = self + .current_index(focused_id) + .and_then(|ix| { + let next_ix = ix + 1; + (next_ix < self.handles.len()).then_some(next_ix) + }) + .unwrap_or_default(); if let Some(next_handle) = self.handles.get(next_ix) { Some(next_handle.clone()) @@ -55,7 +53,7 @@ impl TabHandles { } pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option { - let ix = self.current_index(focused_id); + let ix = self.current_index(focused_id).unwrap_or_default(); let prev_ix; if ix == 0 { prev_ix = self.handles.len().saturating_sub(1); @@ -108,8 +106,14 @@ mod tests { ] ); - // next - assert_eq!(tab.next(None), Some(tab.handles[1].clone())); + // Select first tab index if no handle is currently focused. + assert_eq!(tab.next(None), Some(tab.handles[0].clone())); + // Select last tab index if no handle is currently focused. + assert_eq!( + tab.prev(None), + Some(tab.handles[tab.handles.len() - 1].clone()) + ); + assert_eq!( tab.next(Some(&tab.handles[0].id)), Some(tab.handles[1].clone()) From 9353ba788774c7d82905db9feebf374937487715 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 29 Jul 2025 09:40:59 -0300 Subject: [PATCH 03/35] Fix remaining agent server integration tests (#35222) Release Notes: - N/A --- crates/acp_thread/src/acp_thread.rs | 1 + crates/acp_thread/src/old_acp_support.rs | 6 +++- crates/agent_servers/src/codex.rs | 2 +- crates/agent_servers/src/e2e_tests.rs | 43 ++++++++++++++---------- crates/agent_servers/src/gemini.rs | 1 + 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index d572992c548d24f18be4c8bf82dcf86673c7cac4..72035804101048c24c27f2a0a69908cb5a8998a2 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1597,6 +1597,7 @@ mod tests { name: "test", connection, child_status: io_task, + current_thread: thread_rc, }; AcpThread::new( diff --git a/crates/acp_thread/src/old_acp_support.rs b/crates/acp_thread/src/old_acp_support.rs index 44cd00348fa4dc5de282378f64fed042a7b35439..571023239f1588fbe57666f553a2f416cb82429b 100644 --- a/crates/acp_thread/src/old_acp_support.rs +++ b/crates/acp_thread/src/old_acp_support.rs @@ -7,6 +7,7 @@ use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc}; use ui::App; +use util::ResultExt as _; use crate::{AcpThread, AgentConnection}; @@ -46,7 +47,7 @@ impl acp_old::Client for OldAcpClientDelegate { thread.push_assistant_content_block(thought.into(), true, cx) } }) - .ok(); + .log_err(); })?; Ok(()) @@ -364,6 +365,7 @@ pub struct OldAcpAgentConnection { pub name: &'static str, pub connection: acp_old::AgentConnection, pub child_status: Task>, + pub current_thread: Rc>>, } impl AgentConnection for OldAcpAgentConnection { @@ -383,6 +385,7 @@ impl AgentConnection for OldAcpAgentConnection { } .into_any(), ); + let current_thread = self.current_thread.clone(); cx.spawn(async move |cx| { let result = task.await?; let result = acp_old::InitializeParams::response_from_any(result)?; @@ -396,6 +399,7 @@ impl AgentConnection for OldAcpAgentConnection { let session_id = acp::SessionId("acp-old-no-id".into()); AcpThread::new(self.clone(), project, session_id, cx) }); + current_thread.replace(thread.downgrade()); thread }) }) diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs index b10ce9cf54b75039e768e70ad65d2f0eb318aaf8..d713f0d11cbd3d60f763469dbb4b08a3507f65a5 100644 --- a/crates/agent_servers/src/codex.rs +++ b/crates/agent_servers/src/codex.rs @@ -310,7 +310,7 @@ pub(crate) mod tests { AgentServerCommand { path: cli_path, - args: vec!["mcp".into()], + args: vec![], env: None, } } diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index aca9001c79de2cf60947ec4d67b10ae52e936107..e9c72eabc92b5c5fd6964cb46f0caa5d03180ee2 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -12,7 +12,6 @@ use futures::{FutureExt, StreamExt, channel::mpsc, select}; use gpui::{Entity, TestAppContext}; use indoc::indoc; use project::{FakeFs, Project}; -use serde_json::json; use settings::{Settings, SettingsStore}; use util::path; @@ -27,7 +26,11 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont .unwrap(); thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 2); + assert!( + thread.entries().len() >= 2, + "Expected at least 2 entries. Got: {:?}", + thread.entries() + ); assert!(matches!( thread.entries()[0], AgentThreadEntry::UserMessage(_) @@ -108,19 +111,19 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes } pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { - let fs = init_test(cx).await; - fs.insert_tree( - path!("/private/tmp"), - json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}), - ) - .await; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; + let _fs = init_test(cx).await; + + let tempdir = tempfile::tempdir().unwrap(); + let foo_path = tempdir.path().join("foo"); + std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); + + let project = Project::example([tempdir.path()], &mut cx.to_async()).await; let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; thread .update(cx, |thread, cx| { thread.send_raw( - "Read the '/private/tmp/foo' file and tell me what you see.", + &format!("Read {} and tell me what you see.", foo_path.display()), cx, ) }) @@ -143,6 +146,8 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp .any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) }) ); }); + + drop(tempdir); } pub async fn test_tool_call_with_confirmation( @@ -155,7 +160,7 @@ pub async fn test_tool_call_with_confirmation( let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; let full_turn = thread.update(cx, |thread, cx| { thread.send_raw( - r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#, + r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, cx, ) }); @@ -175,10 +180,10 @@ pub async fn test_tool_call_with_confirmation( ) .await; - let tool_call_id = thread.read_with(cx, |thread, _cx| { + let tool_call_id = thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { id, - content, + label, status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) = &thread @@ -190,7 +195,8 @@ pub async fn test_tool_call_with_confirmation( panic!(); }; - assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch"))); + let label = label.read(cx).source(); + assert!(label.contains("touch"), "Got: {}", label); id.clone() }); @@ -242,7 +248,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await; let full_turn = thread.update(cx, |thread, cx| { thread.send_raw( - r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#, + r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, cx, ) }); @@ -262,10 +268,10 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon ) .await; - thread.read_with(cx, |thread, _cx| { + thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { id, - content, + label, status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) = &thread.entries()[first_tool_call_ix] @@ -273,7 +279,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon panic!("{:?}", thread.entries()[1]); }; - assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch"))); + let label = label.read(cx).source(); + assert!(label.contains("touch"), "Got: {}", label); id.clone() }); diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 8b9fed5777f2ff409170db998cb114e6e7a380e6..a97ff3f462dc96d83130d1cc258af114bb83d6f4 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -107,6 +107,7 @@ impl AgentServer for Gemini { name, connection, child_status, + current_thread: thread_rc, }); Ok(connection) From 5a218d83231647ec22fe8defa0904cdae11e22be Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Jul 2025 18:24:52 +0300 Subject: [PATCH 04/35] Add more data to see which extension got leaked (#35272) Part of https://github.com/zed-industries/zed/issues/35185 Release Notes: - N/A --- crates/extension_host/src/wasm_host.rs | 29 ++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 1f6f5035e32fff3a7d4dff3ff3dd560a029d0c96..d909d06f6b93c5070e6f8fca966778ecefa9f35d 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -777,8 +777,18 @@ impl WasmExtension { } .boxed() })) - .expect("wasm extension channel should not be closed yet"); - return_rx.await.expect("wasm extension channel") + .unwrap_or_else(|_| { + panic!( + "wasm extension channel should not be closed yet, extension {} (id {})", + self.manifest.name, self.manifest.id, + ) + }); + return_rx.await.unwrap_or_else(|_| { + panic!( + "wasm extension channel, extension {} (id {})", + self.manifest.name, self.manifest.id, + ) + }) } } @@ -799,8 +809,19 @@ impl WasmState { } .boxed_local() })) - .expect("main thread message channel should not be closed yet"); - async move { return_rx.await.expect("main thread message channel") } + .unwrap_or_else(|_| { + panic!( + "main thread message channel should not be closed yet, extension {} (id {})", + self.manifest.name, self.manifest.id, + ) + }); + let name = self.manifest.name.clone(); + let id = self.manifest.id.clone(); + async move { + return_rx.await.unwrap_or_else(|_| { + panic!("main thread message channel, extension {name} (id {id})") + }) + } } fn work_dir(&self) -> PathBuf { From 3fc84f8a62977a6e2c732a6536f30cafb11ad55c Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 29 Jul 2025 11:29:12 -0400 Subject: [PATCH 05/35] Comment on source of ctrl-m in keymaps (#35273) Closes https://github.com/zed-industries/zed/issues/23896 Release Notes: - N/A --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index a4f812b2fcf70d44b4ae3dd371d9745755ab1f5e..e36e093e220c7da0a70d3f47f38b51c4d4ba52cc 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -495,7 +495,7 @@ "shift-f12": "editor::GoToImplementation", "alt-ctrl-f12": "editor::GoToTypeDefinitionSplit", "alt-shift-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", + "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains "ctrl-|": "editor::MoveToEnclosingBracket", "ctrl-{": "editor::Fold", "ctrl-}": "editor::UnfoldLines", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index eded8c73e64ae12da04466a654eb2f52fd175bdd..0114e2da1dd74f16fa5cbe54766a07449ae9f056 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -549,7 +549,7 @@ "alt-cmd-f12": "editor::GoToTypeDefinitionSplit", "alt-shift-f12": "editor::FindAllReferences", "cmd-|": "editor::MoveToEnclosingBracket", - "ctrl-m": "editor::MoveToEnclosingBracket", + "ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains "alt-cmd-[": "editor::Fold", "alt-cmd-]": "editor::UnfoldLines", "cmd-k cmd-l": "editor::ToggleFold", From 2fced602b805c1645daf2486fc53035632f75d65 Mon Sep 17 00:00:00 2001 From: devjasperwang Date: Tue, 29 Jul 2025 23:31:54 +0800 Subject: [PATCH 06/35] paths: Fix using relative path as custom_data_dir (#35256) This PR fixes issue of incorrect LSP path args caused by using a relative path when customizing data directory. command: ```bash .\target\debug\zed.exe --user-data-dir=.\target\data ``` before: ```log 2025-07-29T14:17:18+08:00 INFO [lsp] starting language server process. binary path: "F:\\nvm\\nodejs\\node.exe", working directory: "F:\\zed\\target\\data\\config", args: [".\\target\\data\\languages\\json-language-server\\node_modules/vscode-langservers-extracted/bin/vscode-json-language-server", "--stdio"] 2025-07-29T14:17:18+08:00 INFO [project::prettier_store] Installing default prettier and plugins: [("prettier", "3.6.2")] 2025-07-29T14:17:18+08:00 ERROR [lsp] cannot read LSP message headers 2025-07-29T14:17:18+08:00 ERROR [lsp] Shutdown request failure, server json-language-server (id 1): server shut down 2025-07-29T14:17:43+08:00 ERROR [project] Invalid file path provided to LSP request: ".\\target\\data\\config\\settings.json" Thread "main" panicked with "called `Result::unwrap()` on an `Err` value: ()" at crates\project\src\lsp_store.rs:7203:54 https://github.com/zed-industries/zed/blob/cfd5b8ff10cd88a97988292c964689f67301520b/src/crates\project\src\lsp_store.rs#L7203 (may not be uploaded, line may be incorrect if files modified) ``` after: ```log 2025-07-29T14:24:20+08:00 INFO [lsp] starting language server process. binary path: "F:\\nvm\\nodejs\\node.exe", working directory: "F:\\zed\\target\\data\\config", args: ["F:\\zed\\target\\data\\languages\\json-language-server\\node_modules/vscode-langservers-extracted/bin/vscode-json-language-server", "--stdio"] ``` Release Notes: - N/A --- crates/paths/src/paths.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 2f3b18898077bcc455ca8e616a9d550019cd3cbb..47a0f12c0634dbde48d015e4f577519babc67b34 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -35,6 +35,7 @@ pub fn remote_server_dir_relative() -> &'static Path { /// Sets a custom directory for all user data, overriding the default data directory. /// This function must be called before any other path operations that depend on the data directory. +/// The directory's path will be canonicalized to an absolute path by a blocking FS operation. /// The directory will be created if it doesn't exist. /// /// # Arguments @@ -50,13 +51,20 @@ pub fn remote_server_dir_relative() -> &'static Path { /// /// Panics if: /// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`) +/// * The directory's path cannot be canonicalized to an absolute path /// * The directory cannot be created pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() { panic!("set_custom_data_dir called after data_dir or config_dir was initialized"); } CUSTOM_DATA_DIR.get_or_init(|| { - let path = PathBuf::from(dir); + let mut path = PathBuf::from(dir); + if path.is_relative() { + let abs_path = path + .canonicalize() + .expect("failed to canonicalize custom data directory's path to an absolute path"); + path = PathBuf::from(util::paths::SanitizedPath::from(abs_path)) + } std::fs::create_dir_all(&path).expect("failed to create custom data directory"); path }) From a8bdf30259e93be218c6402c2f544f4932b92c68 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 11:45:49 -0400 Subject: [PATCH 07/35] client: Fix typo in the error message (#35275) This PR fixes a typo in the error message for when we fail to parse the Collab URL. Release Notes: - N/A --- crates/client/src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 81bb95b5143b94c0fe71963ae00d7ac3edf93b29..07df7043b5f92eab2eecdb54cf06a07691cd699c 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1138,7 +1138,7 @@ impl Client { .to_str() .map_err(EstablishConnectionError::other)? .to_string(); - Url::parse(&collab_url).with_context(|| format!("parsing colab rpc url {collab_url}")) + Url::parse(&collab_url).with_context(|| format!("parsing collab rpc url {collab_url}")) } } From 511fdaed43e6002778f4bc0693cc5f70552f90b2 Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 29 Jul 2025 17:58:28 +0200 Subject: [PATCH 08/35] Allow searching Windows paths with forward slash (#35198) Release Notes: - Searching windows paths is now possible with a forward slash --- crates/file_finder/src/file_finder.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index a4d61dd56f0b3503b09698aa633cf47bf12389e4..e5ac70bb583be004941eee06476dc9318de1adc4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1404,14 +1404,21 @@ impl PickerDelegate for FileFinderDelegate { } else { let path_position = PathWithPosition::parse_str(&raw_query); + #[cfg(windows)] + let raw_query = raw_query.trim().to_owned().replace("/", "\\"); + #[cfg(not(windows))] + let raw_query = raw_query.trim().to_owned(); + + let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { + None + } else { + // Safe to unwrap as we won't get here when the unwrap in if fails + Some(path_position.path.to_str().unwrap().len()) + }; + let query = FileSearchQuery { - raw_query: raw_query.trim().to_owned(), - file_query_end: if path_position.path.to_str().unwrap_or(raw_query) == raw_query { - None - } else { - // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path_position.path.to_str().unwrap().len()) - }, + raw_query, + file_query_end, path_position, }; From d43f4641748bc17fbfb60ba03b6c4f04fc82817e Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 29 Jul 2025 18:01:07 +0200 Subject: [PATCH 09/35] Fix nightly icon (#35204) Release Notes: - N/A --- .github/workflows/ci.yml | 2 +- script/bundle-windows.ps1 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9ef1531e739aff0dfcf49dff7f5283e9d89ffd2..009fcc8337694a0240e18d03a2f531b1efa36487 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -771,7 +771,7 @@ jobs: timeout-minutes: 120 name: Create a Windows installer runs-on: [self-hosted, Windows, X64] - if: false && (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling')) + 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 }} diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 3aac8700ce91fb946e5446e53689bc9a33a01101..2f751f1d1062d1a5ed33ac088d86d987a0740cd0 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -26,6 +26,7 @@ if ($Help) { Push-Location -Path crates/zed $channel = Get-Content "RELEASE_CHANNEL" $env:ZED_RELEASE_CHANNEL = $channel +$env:RELEASE_CHANNEL = $channel Pop-Location function CheckEnvironmentVariables { From 397b5f930197b470e4323b252300931162ebfe0f Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 29 Jul 2025 18:03:43 +0200 Subject: [PATCH 10/35] Ensure context servers are spawned in the workspace directory (#35271) This fixes an issue where we were not setting the context server working directory at all. Release Notes: - Context servers will now be spawned in the currently active project root. --------- Co-authored-by: Danilo Leal --- crates/agent_servers/src/codex.rs | 2 + crates/context_server/src/client.rs | 3 +- crates/context_server/src/context_server.rs | 16 +++- .../src/transport/stdio_transport.rs | 11 ++- crates/project/src/context_server_store.rs | 82 ++++++++++++++----- crates/project/src/project.rs | 12 ++- 6 files changed, 97 insertions(+), 29 deletions(-) diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs index d713f0d11cbd3d60f763469dbb4b08a3507f65a5..712c3332213f04647940e3d894c18c3385d90ad6 100644 --- a/crates/agent_servers/src/codex.rs +++ b/crates/agent_servers/src/codex.rs @@ -47,6 +47,7 @@ impl AgentServer for Codex { cx: &mut App, ) -> Task>> { let project = project.clone(); + let working_directory = project.read(cx).active_project_directory(cx); cx.spawn(async move |cx| { let settings = cx.read_global(|settings: &SettingsStore, _| { settings.get::(None).codex.clone() @@ -65,6 +66,7 @@ impl AgentServer for Codex { args: command.args, env: command.env, }, + working_directory, ) .into(); ContextServer::start(client.clone(), cx).await?; diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index ff4d79c07d0eccba0e64a2aadec3e3035c9c169f..1eb29bbbf9d61b6139e8d9a1d5fffd2836f55c8a 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -158,6 +158,7 @@ impl Client { pub fn stdio( server_id: ContextServerId, binary: ModelContextServerBinary, + working_directory: &Option, cx: AsyncApp, ) -> Result { log::info!( @@ -172,7 +173,7 @@ impl Client { .map(|name| name.to_string_lossy().to_string()) .unwrap_or_else(String::new); - let transport = Arc::new(StdioTransport::new(binary, &cx)?); + let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?); Self::new(server_id, server_name.into(), transport, cx) } diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index f2517feb27e9ceab2187e0f86bc752e14de5d63f..e76e7972f76a90743b0b34609f4407749660e50f 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -53,7 +53,7 @@ impl std::fmt::Debug for ContextServerCommand { } enum ContextServerTransport { - Stdio(ContextServerCommand), + Stdio(ContextServerCommand, Option), Custom(Arc), } @@ -64,11 +64,18 @@ pub struct ContextServer { } impl ContextServer { - pub fn stdio(id: ContextServerId, command: ContextServerCommand) -> Self { + pub fn stdio( + id: ContextServerId, + command: ContextServerCommand, + working_directory: Option>, + ) -> Self { Self { id, client: RwLock::new(None), - configuration: ContextServerTransport::Stdio(command), + configuration: ContextServerTransport::Stdio( + command, + working_directory.map(|directory| directory.to_path_buf()), + ), } } @@ -90,13 +97,14 @@ impl ContextServer { pub async fn start(self: Arc, cx: &AsyncApp) -> Result<()> { let client = match &self.configuration { - ContextServerTransport::Stdio(command) => Client::stdio( + ContextServerTransport::Stdio(command, working_directory) => Client::stdio( client::ContextServerId(self.id.0.clone()), client::ModelContextServerBinary { executable: Path::new(&command.path).to_path_buf(), args: command.args.clone(), env: command.env.clone(), }, + working_directory, cx.clone(), )?, ContextServerTransport::Custom(transport) => Client::new( diff --git a/crates/context_server/src/transport/stdio_transport.rs b/crates/context_server/src/transport/stdio_transport.rs index 56d0240fa5e86149091c59102d277fca3580a970..443b8c16f160394f4bede9a72315b4e80c652726 100644 --- a/crates/context_server/src/transport/stdio_transport.rs +++ b/crates/context_server/src/transport/stdio_transport.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::pin::Pin; use anyhow::{Context as _, Result}; @@ -22,7 +23,11 @@ pub struct StdioTransport { } impl StdioTransport { - pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result { + pub fn new( + binary: ModelContextServerBinary, + working_directory: &Option, + cx: &AsyncApp, + ) -> Result { let mut command = util::command::new_smol_command(&binary.executable); command .args(&binary.args) @@ -32,6 +37,10 @@ impl StdioTransport { .stderr(std::process::Stdio::piped()) .kill_on_drop(true); + if let Some(working_directory) = working_directory { + command.current_dir(working_directory); + } + let mut server = command.spawn().with_context(|| { format!( "failed to spawn command. (path={:?}, args={:?})", diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index ceec0c0a52b70cb68f0fb41d8c415a13e39e8b85..c96ab4e8f3ba87133d9b64e9701130f5d32adfb9 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -13,6 +13,7 @@ use settings::{Settings as _, SettingsStore}; use util::ResultExt as _; use crate::{ + Project, project_settings::{ContextServerSettings, ProjectSettings}, worktree_store::WorktreeStore, }; @@ -144,6 +145,7 @@ pub struct ContextServerStore { context_server_settings: HashMap, ContextServerSettings>, servers: HashMap, worktree_store: Entity, + project: WeakEntity, registry: Entity, update_servers_task: Option>>, context_server_factory: Option, @@ -161,12 +163,17 @@ pub enum Event { impl EventEmitter for ContextServerStore {} impl ContextServerStore { - pub fn new(worktree_store: Entity, cx: &mut Context) -> Self { + pub fn new( + worktree_store: Entity, + weak_project: WeakEntity, + cx: &mut Context, + ) -> Self { Self::new_internal( true, None, ContextServerDescriptorRegistry::default_global(cx), worktree_store, + weak_project, cx, ) } @@ -184,9 +191,10 @@ impl ContextServerStore { pub fn test( registry: Entity, worktree_store: Entity, + weak_project: WeakEntity, cx: &mut Context, ) -> Self { - Self::new_internal(false, None, registry, worktree_store, cx) + Self::new_internal(false, None, registry, worktree_store, weak_project, cx) } #[cfg(any(test, feature = "test-support"))] @@ -194,6 +202,7 @@ impl ContextServerStore { context_server_factory: ContextServerFactory, registry: Entity, worktree_store: Entity, + weak_project: WeakEntity, cx: &mut Context, ) -> Self { Self::new_internal( @@ -201,6 +210,7 @@ impl ContextServerStore { Some(context_server_factory), registry, worktree_store, + weak_project, cx, ) } @@ -210,6 +220,7 @@ impl ContextServerStore { context_server_factory: Option, registry: Entity, worktree_store: Entity, + weak_project: WeakEntity, cx: &mut Context, ) -> Self { let subscriptions = if maintain_server_loop { @@ -235,6 +246,7 @@ impl ContextServerStore { context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx) .clone(), worktree_store, + project: weak_project, registry, needs_server_update: false, servers: HashMap::default(), @@ -360,7 +372,7 @@ impl ContextServerStore { let configuration = state.configuration(); self.stop_server(&state.server().id(), cx)?; - let new_server = self.create_context_server(id.clone(), configuration.clone())?; + let new_server = self.create_context_server(id.clone(), configuration.clone(), cx); self.run_server(new_server, configuration, cx); } Ok(()) @@ -449,14 +461,33 @@ impl ContextServerStore { &self, id: ContextServerId, configuration: Arc, - ) -> Result> { + cx: &mut Context, + ) -> Arc { + let root_path = self + .project + .read_with(cx, |project, cx| project.active_project_directory(cx)) + .ok() + .flatten() + .or_else(|| { + self.worktree_store.read_with(cx, |store, cx| { + store.visible_worktrees(cx).fold(None, |acc, item| { + if acc.is_none() { + item.read(cx).root_dir() + } else { + acc + } + }) + }) + }); + if let Some(factory) = self.context_server_factory.as_ref() { - Ok(factory(id, configuration)) + factory(id, configuration) } else { - Ok(Arc::new(ContextServer::stdio( + Arc::new(ContextServer::stdio( id, configuration.command().clone(), - ))) + root_path, + )) } } @@ -553,7 +584,7 @@ impl ContextServerStore { let mut servers_to_remove = HashSet::default(); let mut servers_to_stop = HashSet::default(); - this.update(cx, |this, _cx| { + this.update(cx, |this, cx| { for server_id in this.servers.keys() { // All servers that are not in desired_servers should be removed from the store. // This can happen if the user removed a server from the context server settings. @@ -572,14 +603,10 @@ impl ContextServerStore { let existing_config = state.as_ref().map(|state| state.configuration()); if existing_config.as_deref() != Some(&config) || is_stopped { let config = Arc::new(config); - if let Some(server) = this - .create_context_server(id.clone(), config.clone()) - .log_err() - { - servers_to_start.push((server, config)); - if this.servers.contains_key(&id) { - servers_to_stop.insert(id); - } + let server = this.create_context_server(id.clone(), config.clone(), cx); + servers_to_start.push((server, config)); + if this.servers.contains_key(&id) { + servers_to_stop.insert(id); } } } @@ -630,7 +657,12 @@ mod tests { let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); let store = cx.new(|cx| { - ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx) + ContextServerStore::test( + registry.clone(), + project.read(cx).worktree_store(), + project.downgrade(), + cx, + ) }); let server_1_id = ContextServerId(SERVER_1_ID.into()); @@ -705,7 +737,12 @@ mod tests { let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); let store = cx.new(|cx| { - ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx) + ContextServerStore::test( + registry.clone(), + project.read(cx).worktree_store(), + project.downgrade(), + cx, + ) }); let server_1_id = ContextServerId(SERVER_1_ID.into()); @@ -758,7 +795,12 @@ mod tests { let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); let store = cx.new(|cx| { - ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx) + ContextServerStore::test( + registry.clone(), + project.read(cx).worktree_store(), + project.downgrade(), + cx, + ) }); let server_id = ContextServerId(SERVER_1_ID.into()); @@ -842,6 +884,7 @@ mod tests { }), registry.clone(), project.read(cx).worktree_store(), + project.downgrade(), cx, ) }); @@ -1074,6 +1117,7 @@ mod tests { }), registry.clone(), project.read(cx).worktree_store(), + project.downgrade(), cx, ) }); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a4e76ed4756a92c4c721a4ad1d991e69cbac0a4f..6b943216b3d0b4e24c7459c9d77befefd206b0d0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -998,8 +998,9 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); + let weak_self = cx.weak_entity(); let context_server_store = - cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx)); + cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx)); let environment = cx.new(|_| ProjectEnvironment::new(env)); let manifest_tree = ManifestTree::new(worktree_store.clone(), cx); @@ -1167,8 +1168,9 @@ impl Project { cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); + let weak_self = cx.weak_entity(); let context_server_store = - cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx)); + cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx)); let buffer_store = cx.new(|cx| { BufferStore::remote( @@ -1428,8 +1430,6 @@ impl Project { let image_store = cx.new(|cx| { ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx) })?; - let context_server_store = - cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx))?; let environment = cx.new(|_| ProjectEnvironment::new(None))?; @@ -1496,6 +1496,10 @@ impl Project { let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx); + let weak_self = cx.weak_entity(); + let context_server_store = + cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx)); + let mut worktrees = Vec::new(); for worktree in response.payload.worktrees { let worktree = From aa3437e98fe61cc6387a1a993d38431a517c554b Mon Sep 17 00:00:00 2001 From: localcc Date: Tue, 29 Jul 2025 18:03:57 +0200 Subject: [PATCH 11/35] Allow installing from an administrator user (#35202) Release Notes: - N/A --- crates/zed/resources/windows/zed.iss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss index 9d104d1f1540acde9f7045ebbe103caf1ae5605a..51c1dd096ed20e78beb34664283340d30c13e017 100644 --- a/crates/zed/resources/windows/zed.iss +++ b/crates/zed/resources/windows/zed.iss @@ -1245,16 +1245,6 @@ Root: HKCU; Subkey: "Software\Classes\zed\DefaultIcon"; ValueType: "string"; Val Root: HKCU; Subkey: "Software\Classes\zed\shell\open\command"; ValueType: "string"; ValueData: """{app}\Zed.exe"" ""%1""" [Code] -function InitializeSetup(): Boolean; -begin - Result := True; - - if not WizardSilent() and IsAdmin() then begin - MsgBox('This User Installer is not meant to be run as an Administrator.', mbError, MB_OK); - Result := False; - end; -end; - function WizardNotSilent(): Boolean; begin Result := not WizardSilent(); From f9224b1d7486ea43d2ced75597ff0ea6f96d9aa9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 12:53:56 -0400 Subject: [PATCH 12/35] client: Send `User-Agent` header on WebSocket connection requests (#35280) This PR makes it so we send the `User-Agent` header on the WebSocket connection requests when connecting to Collab. We use the user agent set on the parent HTTP client. Release Notes: - N/A --- crates/client/src/client.rs | 8 ++++-- crates/gpui/src/app.rs | 4 +++ crates/http_client/src/http_client.rs | 29 +++++++++++++++++++++ crates/reqwest_client/src/reqwest_client.rs | 13 +++++++-- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 07df7043b5f92eab2eecdb54cf06a07691cd699c..e0f4a70b15e5cada778d4a348a9afc2d0ae95527 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -21,7 +21,7 @@ use futures::{ channel::oneshot, future::BoxFuture, }; use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions}; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; +use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http}; use parking_lot::RwLock; use postage::watch; use proxy::connect_proxy_stream; @@ -1158,6 +1158,7 @@ impl Client { let http = self.http.clone(); let proxy = http.proxy().cloned(); + let user_agent = http.user_agent().cloned(); let credentials = credentials.clone(); let rpc_url = self.rpc_url(http, release_channel); let system_id = self.telemetry.system_id(); @@ -1209,7 +1210,7 @@ impl Client { // We then modify the request to add our desired headers. let request_headers = request.headers_mut(); request_headers.insert( - "Authorization", + http::header::AUTHORIZATION, HeaderValue::from_str(&credentials.authorization_header())?, ); request_headers.insert( @@ -1221,6 +1222,9 @@ impl Client { "x-zed-release-channel", HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?, ); + if let Some(user_agent) = user_agent { + request_headers.insert(http::header::USER_AGENT, user_agent); + } if let Some(system_id) = system_id { request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?); } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 759d33563e0af1be038a98f78712f7b3f18ef327..ded7bae3164ab5b76568290b7335599dd720c320 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2023,6 +2023,10 @@ impl HttpClient for NullHttpClient { .boxed() } + fn user_agent(&self) -> Option<&http_client::http::HeaderValue> { + None + } + fn proxy(&self) -> Option<&Url> { None } diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index eebab86e21222a40f7c1e3c3285b63b523ecfd3b..434bd74fc804a36685ca5a51df26a6de73c14cf1 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -4,6 +4,7 @@ pub mod github; pub use anyhow::{Result, anyhow}; pub use async_body::{AsyncBody, Inner}; use derive_more::Deref; +use http::HeaderValue; pub use http::{self, Method, Request, Response, StatusCode, Uri}; use futures::future::BoxFuture; @@ -39,6 +40,8 @@ impl HttpRequestExt for http::request::Builder { pub trait HttpClient: 'static + Send + Sync { fn type_name(&self) -> &'static str; + fn user_agent(&self) -> Option<&HeaderValue>; + fn send( &self, req: http::Request, @@ -118,6 +121,10 @@ impl HttpClient for HttpClientWithProxy { self.client.send(req) } + fn user_agent(&self) -> Option<&HeaderValue> { + self.client.user_agent() + } + fn proxy(&self) -> Option<&Url> { self.proxy.as_ref() } @@ -135,6 +142,10 @@ impl HttpClient for Arc { self.client.send(req) } + fn user_agent(&self) -> Option<&HeaderValue> { + self.client.user_agent() + } + fn proxy(&self) -> Option<&Url> { self.proxy.as_ref() } @@ -250,6 +261,10 @@ impl HttpClient for Arc { self.client.send(req) } + fn user_agent(&self) -> Option<&HeaderValue> { + self.client.user_agent() + } + fn proxy(&self) -> Option<&Url> { self.client.proxy.as_ref() } @@ -267,6 +282,10 @@ impl HttpClient for HttpClientWithUrl { self.client.send(req) } + fn user_agent(&self) -> Option<&HeaderValue> { + self.client.user_agent() + } + fn proxy(&self) -> Option<&Url> { self.client.proxy.as_ref() } @@ -314,6 +333,10 @@ impl HttpClient for BlockedHttpClient { }) } + fn user_agent(&self) -> Option<&HeaderValue> { + None + } + fn proxy(&self) -> Option<&Url> { None } @@ -334,6 +357,7 @@ type FakeHttpHandler = Box< #[cfg(feature = "test-support")] pub struct FakeHttpClient { handler: FakeHttpHandler, + user_agent: HeaderValue, } #[cfg(feature = "test-support")] @@ -348,6 +372,7 @@ impl FakeHttpClient { client: HttpClientWithProxy { client: Arc::new(Self { handler: Box::new(move |req| Box::pin(handler(req))), + user_agent: HeaderValue::from_static(type_name::()), }), proxy: None, }, @@ -390,6 +415,10 @@ impl HttpClient for FakeHttpClient { future } + fn user_agent(&self) -> Option<&HeaderValue> { + Some(&self.user_agent) + } + fn proxy(&self) -> Option<&Url> { None } diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs index daff20ac4ad244a7491bc8f6a248d6df3e7e99f5..e02768876d4ca276a101def33298caf2171bc968 100644 --- a/crates/reqwest_client/src/reqwest_client.rs +++ b/crates/reqwest_client/src/reqwest_client.rs @@ -20,6 +20,7 @@ static REDACT_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"key=[^&]+") pub struct ReqwestClient { client: reqwest::Client, proxy: Option, + user_agent: Option, handle: tokio::runtime::Handle, } @@ -44,9 +45,11 @@ impl ReqwestClient { Ok(client.into()) } - pub fn proxy_and_user_agent(proxy: Option, agent: &str) -> anyhow::Result { + pub fn proxy_and_user_agent(proxy: Option, user_agent: &str) -> anyhow::Result { + let user_agent = HeaderValue::from_str(user_agent)?; + let mut map = HeaderMap::new(); - map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?); + map.insert(http::header::USER_AGENT, user_agent.clone()); let mut client = Self::builder().default_headers(map); let client_has_proxy; @@ -73,6 +76,7 @@ impl ReqwestClient { .build()?; let mut client: ReqwestClient = client.into(); client.proxy = client_has_proxy.then_some(proxy).flatten(); + client.user_agent = Some(user_agent); Ok(client) } } @@ -96,6 +100,7 @@ impl From for ReqwestClient { client, handle, proxy: None, + user_agent: None, } } } @@ -216,6 +221,10 @@ impl http_client::HttpClient for ReqwestClient { type_name::() } + fn user_agent(&self) -> Option<&HeaderValue> { + self.user_agent.as_ref() + } + fn send( &self, req: http::Request, From 77dc65d8261f38d8c2c8648de26786f253d8de5a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 13:06:27 -0400 Subject: [PATCH 13/35] collab: Attach `User-Agent` to `handle connection` span (#35282) This PR makes it so we attach the value from the `User-Agent` header to the `handle connection` span. We'll start sending this header in https://github.com/zed-industries/zed/pull/35280. Release Notes: - N/A --- crates/collab/src/rpc.rs | 9 +++++++++ crates/collab/src/tests/test_server.rs | 1 + 2 files changed, 10 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 515647f97dc9f52646aad8513eaa37b09f7d3c11..b7e5ce0739c35c8e9ad4dde1816ade6ec153200a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -23,6 +23,7 @@ use anyhow::{Context as _, anyhow, bail}; use async_tungstenite::tungstenite::{ Message as TungsteniteMessage, protocol::CloseFrame as TungsteniteCloseFrame, }; +use axum::headers::UserAgent; use axum::{ Extension, Router, TypedHeader, body::Body, @@ -750,6 +751,7 @@ impl Server { address: String, principal: Principal, zed_version: ZedVersion, + user_agent: Option, geoip_country_code: Option, system_id: Option, send_connection_id: Option>, @@ -762,9 +764,14 @@ impl Server { user_id=field::Empty, login=field::Empty, impersonator=field::Empty, + user_agent=field::Empty, geoip_country_code=field::Empty ); principal.update_span(&span); + if let Some(user_agent) = user_agent { + span.record("user_agent", user_agent); + } + if let Some(country_code) = geoip_country_code.as_ref() { span.record("geoip_country_code", country_code); } @@ -1172,6 +1179,7 @@ pub async fn handle_websocket_request( ConnectInfo(socket_address): ConnectInfo, Extension(server): Extension>, Extension(principal): Extension, + user_agent: Option>, country_code_header: Option>, system_id_header: Option>, ws: WebSocketUpgrade, @@ -1227,6 +1235,7 @@ pub async fn handle_websocket_request( socket_address, principal, version, + user_agent.map(|header| header.to_string()), country_code_header.map(|header| header.to_string()), system_id_header.map(|header| header.to_string()), None, diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index ab84e02b190443787aa0165ada558382a5d08da9..5192db16a7de350aa04650b27b860f7848103af2 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -256,6 +256,7 @@ impl TestServer { ZedVersion(SemanticVersion::new(1, 0, 0)), None, None, + None, Some(connection_id_tx), Executor::Deterministic(cx.background_executor().clone()), None, From 65250fe08d29c27d7414cdea5201550f720a7307 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 29 Jul 2025 11:28:18 -0600 Subject: [PATCH 14/35] cloud provider: Use `CompletionEvent` type from `zed_llm_client` (#35285) Release Notes: - N/A --- crates/language_models/src/provider/cloud.rs | 31 ++++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 09a2ac6e0ab17a69e8bf3c85f871dab35733d1a3..1e6e7b96d00ce240a2919801ac8adb7ccce57142 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -35,8 +35,8 @@ use ui::{TintColor, prelude::*}; use util::{ResultExt as _, maybe}; use zed_llm_client::{ CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, - CompletionRequestStatus, CountTokensBody, CountTokensResponse, EXPIRED_LLM_TOKEN_HEADER_NAME, - ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, + CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, + EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME, }; @@ -1040,15 +1040,8 @@ impl LanguageModel for CloudLanguageModel { } } -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CloudCompletionEvent { - Status(CompletionRequestStatus), - Event(T), -} - fn map_cloud_completion_events( - stream: Pin>> + Send>>, + stream: Pin>> + Send>>, mut map_callback: F, ) -> BoxStream<'static, Result> where @@ -1063,10 +1056,10 @@ where Err(error) => { vec![Err(LanguageModelCompletionError::from(error))] } - Ok(CloudCompletionEvent::Status(event)) => { + Ok(CompletionEvent::Status(event)) => { vec![Ok(LanguageModelCompletionEvent::StatusUpdate(event))] } - Ok(CloudCompletionEvent::Event(event)) => map_callback(event), + Ok(CompletionEvent::Event(event)) => map_callback(event), }) }) .boxed() @@ -1074,9 +1067,9 @@ where fn usage_updated_event( usage: Option, -) -> impl Stream>> { +) -> impl Stream>> { futures::stream::iter(usage.map(|usage| { - Ok(CloudCompletionEvent::Status( + Ok(CompletionEvent::Status( CompletionRequestStatus::UsageUpdated { amount: usage.amount as usize, limit: usage.limit, @@ -1087,9 +1080,9 @@ fn usage_updated_event( fn tool_use_limit_reached_event( tool_use_limit_reached: bool, -) -> impl Stream>> { +) -> impl Stream>> { futures::stream::iter(tool_use_limit_reached.then(|| { - Ok(CloudCompletionEvent::Status( + Ok(CompletionEvent::Status( CompletionRequestStatus::ToolUseLimitReached, )) })) @@ -1098,7 +1091,7 @@ fn tool_use_limit_reached_event( fn response_lines( response: Response, includes_status_messages: bool, -) -> impl Stream>> { +) -> impl Stream>> { futures::stream::try_unfold( (String::new(), BufReader::new(response.into_body())), move |(mut line, mut body)| async move { @@ -1106,9 +1099,9 @@ fn response_lines( Ok(0) => Ok(None), Ok(_) => { let event = if includes_status_messages { - serde_json::from_str::>(&line)? + serde_json::from_str::>(&line)? } else { - CloudCompletionEvent::Event(serde_json::from_str::(&line)?) + CompletionEvent::Event(serde_json::from_str::(&line)?) }; line.clear(); From efa3cc13efa0bf100fcdac315e81dc31433e9c46 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 29 Jul 2025 13:10:51 -0500 Subject: [PATCH 15/35] keymap_ui: Test keystroke input (#35286) Closes #ISSUE Separate out the keystroke input into it's own component and add a bunch of tests for it's core keystroke+modifier event handling logic Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/Cargo.toml | 4 + crates/settings_ui/src/keybindings.rs | 546 +------- .../src/ui_components/keystroke_input.rs | 1165 +++++++++++++++++ crates/settings_ui/src/ui_components/mod.rs | 1 + 4 files changed, 1179 insertions(+), 537 deletions(-) create mode 100644 crates/settings_ui/src/ui_components/keystroke_input.rs diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 25f033469d7b00e1b351c7a0385b2de5bc10d9ad..e8434c1a32ba1bd3d1fc8d10ebbf7ba405e9dbe0 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -48,3 +48,7 @@ workspace.workspace = true [dev-dependencies] db = {"workspace"= true, "features" = ["test-support"]} +fs = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } +project = { workspace = true, features = ["test-support"] } +workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 5ff91246f4de52fa1057c7aef598d88b6b1ed306..70afe1729c6b14a952c1be3ddba5b0a93e5b7ec3 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -11,11 +11,10 @@ use editor::{CompletionProvider, Editor, EditorEvent}; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context, - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, - KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, - ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, - actions, anchored, deferred, div, + Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton, + Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, + TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -35,7 +34,10 @@ use workspace::{ use crate::{ keybindings::persistence::KEYBINDING_EDITORS, - ui_components::table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState}, + ui_components::{ + keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording}, + table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState}, + }, }; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static(""); @@ -72,18 +74,6 @@ actions!( ] ); -actions!( - keystroke_input, - [ - /// Starts recording keystrokes - StartRecording, - /// Stops recording keystrokes - StopRecording, - /// Clears the recorded keystrokes - ClearKeystrokes, - ] -); - pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); cx.set_global(keymap_event_channel); @@ -393,7 +383,7 @@ impl KeymapEditor { let keystroke_editor = cx.new(|cx| { let mut keystroke_editor = KeystrokeInput::new(None, window, cx); - keystroke_editor.search = true; + keystroke_editor.set_search(true); keystroke_editor }); @@ -2979,524 +2969,6 @@ async fn remove_keybinding( Ok(()) } -#[derive(PartialEq, Eq, Debug, Copy, Clone)] -enum CloseKeystrokeResult { - Partial, - Close, - None, -} - -struct KeystrokeInput { - keystrokes: Vec, - placeholder_keystrokes: Option>, - outer_focus_handle: FocusHandle, - inner_focus_handle: FocusHandle, - intercept_subscription: Option, - _focus_subscriptions: [Subscription; 2], - search: bool, - /// Handles tripe escape to stop recording - close_keystrokes: Option>, - close_keystrokes_start: Option, - previous_modifiers: Modifiers, -} - -impl KeystrokeInput { - const KEYSTROKE_COUNT_MAX: usize = 3; - - fn new( - placeholder_keystrokes: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let outer_focus_handle = cx.focus_handle(); - let inner_focus_handle = cx.focus_handle(); - let _focus_subscriptions = [ - cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in), - cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out), - ]; - Self { - keystrokes: Vec::new(), - placeholder_keystrokes, - inner_focus_handle, - outer_focus_handle, - intercept_subscription: None, - _focus_subscriptions, - search: false, - close_keystrokes: None, - close_keystrokes_start: None, - previous_modifiers: Modifiers::default(), - } - } - - fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { - self.keystrokes = keystrokes; - self.keystrokes_changed(cx); - } - - fn dummy(modifiers: Modifiers) -> Keystroke { - return Keystroke { - modifiers, - key: "".to_string(), - key_char: None, - }; - } - - fn keystrokes_changed(&self, cx: &mut Context) { - cx.emit(()); - cx.notify(); - } - - fn key_context() -> KeyContext { - let mut key_context = KeyContext::default(); - key_context.add("KeystrokeInput"); - key_context - } - - fn handle_possible_close_keystroke( - &mut self, - keystroke: &Keystroke, - window: &mut Window, - cx: &mut Context, - ) -> CloseKeystrokeResult { - let Some(keybind_for_close_action) = window - .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context()) - else { - log::trace!("No keybinding to stop recording keystrokes in keystroke input"); - self.close_keystrokes.take(); - self.close_keystrokes_start.take(); - return CloseKeystrokeResult::None; - }; - let action_keystrokes = keybind_for_close_action.keystrokes(); - - if let Some(mut close_keystrokes) = self.close_keystrokes.take() { - let mut index = 0; - - while index < action_keystrokes.len() && index < close_keystrokes.len() { - if !close_keystrokes[index].should_match(&action_keystrokes[index]) { - break; - } - index += 1; - } - if index == close_keystrokes.len() { - if index >= action_keystrokes.len() { - self.close_keystrokes_start.take(); - return CloseKeystrokeResult::None; - } - if keystroke.should_match(&action_keystrokes[index]) { - if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 { - self.stop_recording(&StopRecording, window, cx); - return CloseKeystrokeResult::Close; - } else { - close_keystrokes.push(keystroke.clone()); - self.close_keystrokes = Some(close_keystrokes); - return CloseKeystrokeResult::Partial; - } - } else { - self.close_keystrokes_start.take(); - return CloseKeystrokeResult::None; - } - } - } else if let Some(first_action_keystroke) = action_keystrokes.first() - && keystroke.should_match(first_action_keystroke) - { - self.close_keystrokes = Some(vec![keystroke.clone()]); - return CloseKeystrokeResult::Partial; - } - self.close_keystrokes_start.take(); - return CloseKeystrokeResult::None; - } - - fn on_modifiers_changed( - &mut self, - event: &ModifiersChangedEvent, - _window: &mut Window, - cx: &mut Context, - ) { - let keystrokes_len = self.keystrokes.len(); - - if self.previous_modifiers.modified() - && event.modifiers.is_subset_of(&self.previous_modifiers) - { - self.previous_modifiers &= event.modifiers; - cx.stop_propagation(); - return; - } - - if let Some(last) = self.keystrokes.last_mut() - && last.key.is_empty() - && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX - { - if self.search { - if self.previous_modifiers.modified() { - last.modifiers |= event.modifiers; - self.previous_modifiers |= event.modifiers; - } else { - self.keystrokes.push(Self::dummy(event.modifiers)); - self.previous_modifiers |= event.modifiers; - } - } else if !event.modifiers.modified() { - self.keystrokes.pop(); - } else { - last.modifiers = event.modifiers; - } - - self.keystrokes_changed(cx); - } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { - self.keystrokes.push(Self::dummy(event.modifiers)); - if self.search { - self.previous_modifiers |= event.modifiers; - } - self.keystrokes_changed(cx); - } - cx.stop_propagation(); - } - - fn handle_keystroke( - &mut self, - keystroke: &Keystroke, - window: &mut Window, - cx: &mut Context, - ) { - let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); - if close_keystroke_result != CloseKeystrokeResult::Close { - let key_len = self.keystrokes.len(); - if let Some(last) = self.keystrokes.last_mut() - && last.key.is_empty() - && key_len <= Self::KEYSTROKE_COUNT_MAX - { - if self.search { - last.key = keystroke.key.clone(); - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() - { - self.close_keystrokes_start = Some(self.keystrokes.len() - 1); - } - if self.search { - self.previous_modifiers = keystroke.modifiers; - } - self.keystrokes_changed(cx); - cx.stop_propagation(); - return; - } else { - self.keystrokes.pop(); - } - } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() - { - self.close_keystrokes_start = Some(self.keystrokes.len()); - } - self.keystrokes.push(keystroke.clone()); - if self.search { - self.previous_modifiers = keystroke.modifiers; - } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { - self.keystrokes.push(Self::dummy(keystroke.modifiers)); - } - } else if close_keystroke_result != CloseKeystrokeResult::Partial { - self.clear_keystrokes(&ClearKeystrokes, window, cx); - } - } - self.keystrokes_changed(cx); - cx.stop_propagation(); - } - - fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context) { - if self.intercept_subscription.is_none() { - let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| { - this.handle_keystroke(&event.keystroke, window, cx); - }); - self.intercept_subscription = Some(cx.intercept_keystrokes(listener)) - } - } - - fn on_inner_focus_out( - &mut self, - _event: gpui::FocusOutEvent, - _window: &mut Window, - cx: &mut Context, - ) { - self.intercept_subscription.take(); - cx.notify(); - } - - fn keystrokes(&self) -> &[Keystroke] { - if let Some(placeholders) = self.placeholder_keystrokes.as_ref() - && self.keystrokes.is_empty() - { - return placeholders; - } - if !self.search - && self - .keystrokes - .last() - .map_or(false, |last| last.key.is_empty()) - { - return &self.keystrokes[..self.keystrokes.len() - 1]; - } - return &self.keystrokes; - } - - fn render_keystrokes(&self, is_recording: bool) -> impl Iterator { - let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref() - && self.keystrokes.is_empty() - { - if is_recording { - &[] - } else { - placeholders.as_slice() - } - } else { - &self.keystrokes - }; - keystrokes.iter().map(move |keystroke| { - h_flex().children(ui::render_keystroke( - keystroke, - Some(Color::Default), - Some(rems(0.875).into()), - ui::PlatformStyle::platform(), - false, - )) - }) - } - - fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context) { - window.focus(&self.inner_focus_handle); - self.clear_keystrokes(&ClearKeystrokes, window, cx); - self.previous_modifiers = window.modifiers(); - cx.stop_propagation(); - } - - fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context) { - if !self.inner_focus_handle.is_focused(window) { - return; - } - window.focus(&self.outer_focus_handle); - if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() - && close_keystrokes_start < self.keystrokes.len() - { - self.keystrokes.drain(close_keystrokes_start..); - } - self.close_keystrokes.take(); - cx.notify(); - } - - fn clear_keystrokes( - &mut self, - _: &ClearKeystrokes, - _window: &mut Window, - cx: &mut Context, - ) { - self.keystrokes.clear(); - self.keystrokes_changed(cx); - } -} - -impl EventEmitter<()> for KeystrokeInput {} - -impl Focusable for KeystrokeInput { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.outer_focus_handle.clone() - } -} - -impl Render for KeystrokeInput { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let colors = cx.theme().colors(); - let is_focused = self.outer_focus_handle.contains_focused(window, cx); - let is_recording = self.inner_focus_handle.is_focused(window); - - let horizontal_padding = rems_from_px(64.); - - let recording_bg_color = colors - .editor_background - .blend(colors.text_accent.opacity(0.1)); - - let recording_pulse = |color: Color| { - Icon::new(IconName::Circle) - .size(IconSize::Small) - .color(Color::Error) - .with_animation( - "recording-pulse", - Animation::new(std::time::Duration::from_secs(2)) - .repeat() - .with_easing(gpui::pulsating_between(0.4, 0.8)), - { - let color = color.color(cx); - move |this, delta| this.color(Color::Custom(color.opacity(delta))) - }, - ) - }; - - let recording_indicator = h_flex() - .h_4() - .pr_1() - .gap_0p5() - .border_1() - .border_color(colors.border) - .bg(colors - .editor_background - .blend(colors.text_accent.opacity(0.1))) - .rounded_sm() - .child(recording_pulse(Color::Error)) - .child( - Label::new("REC") - .size(LabelSize::XSmall) - .weight(FontWeight::SEMIBOLD) - .color(Color::Error), - ); - - let search_indicator = h_flex() - .h_4() - .pr_1() - .gap_0p5() - .border_1() - .border_color(colors.border) - .bg(colors - .editor_background - .blend(colors.text_accent.opacity(0.1))) - .rounded_sm() - .child(recording_pulse(Color::Accent)) - .child( - Label::new("SEARCH") - .size(LabelSize::XSmall) - .weight(FontWeight::SEMIBOLD) - .color(Color::Accent), - ); - - let record_icon = if self.search { - IconName::MagnifyingGlass - } else { - IconName::PlayFilled - }; - - h_flex() - .id("keystroke-input") - .track_focus(&self.outer_focus_handle) - .py_2() - .px_3() - .gap_2() - .min_h_10() - .w_full() - .flex_1() - .justify_between() - .rounded_lg() - .overflow_hidden() - .map(|this| { - if is_recording { - this.bg(recording_bg_color) - } else { - this.bg(colors.editor_background) - } - }) - .border_1() - .border_color(colors.border_variant) - .when(is_focused, |parent| { - parent.border_color(colors.border_focused) - }) - .key_context(Self::key_context()) - .on_action(cx.listener(Self::start_recording)) - .on_action(cx.listener(Self::clear_keystrokes)) - .child( - h_flex() - .w(horizontal_padding) - .gap_0p5() - .justify_start() - .flex_none() - .when(is_recording, |this| { - this.map(|this| { - if self.search { - this.child(search_indicator) - } else { - this.child(recording_indicator) - } - }) - }), - ) - .child( - h_flex() - .id("keystroke-input-inner") - .track_focus(&self.inner_focus_handle) - .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) - .size_full() - .when(!self.search, |this| { - this.focus(|mut style| { - style.border_color = Some(colors.border_focused); - style - }) - }) - .w_full() - .min_w_0() - .justify_center() - .flex_wrap() - .gap(ui::DynamicSpacing::Base04.rems(cx)) - .children(self.render_keystrokes(is_recording)), - ) - .child( - h_flex() - .w(horizontal_padding) - .gap_0p5() - .justify_end() - .flex_none() - .map(|this| { - if is_recording { - this.child( - IconButton::new("stop-record-btn", IconName::StopFilled) - .shape(ui::IconButtonShape::Square) - .map(|this| { - this.tooltip(Tooltip::for_action_title( - if self.search { - "Stop Searching" - } else { - "Stop Recording" - }, - &StopRecording, - )) - }) - .icon_color(Color::Error) - .on_click(cx.listener(|this, _event, window, cx| { - this.stop_recording(&StopRecording, window, cx); - })), - ) - } else { - this.child( - IconButton::new("record-btn", record_icon) - .shape(ui::IconButtonShape::Square) - .map(|this| { - this.tooltip(Tooltip::for_action_title( - if self.search { - "Start Searching" - } else { - "Start Recording" - }, - &StartRecording, - )) - }) - .when(!is_focused, |this| this.icon_color(Color::Muted)) - .on_click(cx.listener(|this, _event, window, cx| { - this.start_recording(&StartRecording, window, cx); - })), - ) - } - }) - .child( - IconButton::new("clear-btn", IconName::Delete) - .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::for_action_title( - "Clear Keystrokes", - &ClearKeystrokes, - )) - .when(!is_recording || !is_focused, |this| { - this.icon_color(Color::Muted) - }) - .on_click(cx.listener(|this, _event, window, cx| { - this.clear_keystrokes(&ClearKeystrokes, window, cx); - })), - ), - ) - } -} - fn collect_contexts_from_assets() -> Vec { let mut keymap_assets = vec![ util::asset_str::(settings::DEFAULT_KEYMAP_PATH), diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs new file mode 100644 index 0000000000000000000000000000000000000000..08ffe3575bcf1365add16f8afbcce370baaf48f2 --- /dev/null +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -0,0 +1,1165 @@ +use gpui::{ + Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, actions, +}; +use ui::{ + ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, + ParentElement as _, Render, Styled as _, Tooltip, Window, prelude::*, +}; + +actions!( + keystroke_input, + [ + /// Starts recording keystrokes + StartRecording, + /// Stops recording keystrokes + StopRecording, + /// Clears the recorded keystrokes + ClearKeystrokes, + ] +); + +const KEY_CONTEXT_VALUE: &'static str = "KeystrokeInput"; + +enum CloseKeystrokeResult { + Partial, + Close, + None, +} + +impl PartialEq for CloseKeystrokeResult { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (CloseKeystrokeResult::Partial, CloseKeystrokeResult::Partial) + | (CloseKeystrokeResult::Close, CloseKeystrokeResult::Close) + | (CloseKeystrokeResult::None, CloseKeystrokeResult::None) + ) + } +} + +pub struct KeystrokeInput { + keystrokes: Vec, + placeholder_keystrokes: Option>, + outer_focus_handle: FocusHandle, + inner_focus_handle: FocusHandle, + intercept_subscription: Option, + _focus_subscriptions: [Subscription; 2], + search: bool, + /// Handles triple escape to stop recording + close_keystrokes: Option>, + close_keystrokes_start: Option, + previous_modifiers: Modifiers, + #[cfg(test)] + recording: bool, +} + +impl KeystrokeInput { + const KEYSTROKE_COUNT_MAX: usize = 3; + + pub fn new( + placeholder_keystrokes: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let outer_focus_handle = cx.focus_handle(); + let inner_focus_handle = cx.focus_handle(); + let _focus_subscriptions = [ + cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in), + cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out), + ]; + Self { + keystrokes: Vec::new(), + placeholder_keystrokes, + inner_focus_handle, + outer_focus_handle, + intercept_subscription: None, + _focus_subscriptions, + search: false, + close_keystrokes: None, + close_keystrokes_start: None, + previous_modifiers: Modifiers::default(), + #[cfg(test)] + recording: false, + } + } + + pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { + self.keystrokes = keystrokes; + self.keystrokes_changed(cx); + } + + pub fn set_search(&mut self, search: bool) { + self.search = search; + } + + pub fn keystrokes(&self) -> &[Keystroke] { + if let Some(placeholders) = self.placeholder_keystrokes.as_ref() + && self.keystrokes.is_empty() + { + return placeholders; + } + if !self.search + && self + .keystrokes + .last() + .map_or(false, |last| last.key.is_empty()) + { + return &self.keystrokes[..self.keystrokes.len() - 1]; + } + return &self.keystrokes; + } + + fn dummy(modifiers: Modifiers) -> Keystroke { + return Keystroke { + modifiers, + key: "".to_string(), + key_char: None, + }; + } + + fn keystrokes_changed(&self, cx: &mut Context) { + cx.emit(()); + cx.notify(); + } + + fn key_context() -> KeyContext { + let mut key_context = KeyContext::default(); + key_context.add(KEY_CONTEXT_VALUE); + key_context + } + + fn determine_stop_recording_binding(window: &mut Window) -> Option { + if cfg!(test) { + Some(gpui::KeyBinding::new( + "escape escape escape", + StopRecording, + Some(KEY_CONTEXT_VALUE), + )) + } else { + window.highest_precedence_binding_for_action_in_context( + &StopRecording, + Self::key_context(), + ) + } + } + + fn handle_possible_close_keystroke( + &mut self, + keystroke: &Keystroke, + window: &mut Window, + cx: &mut Context, + ) -> CloseKeystrokeResult { + let Some(keybind_for_close_action) = Self::determine_stop_recording_binding(window) else { + log::trace!("No keybinding to stop recording keystrokes in keystroke input"); + self.close_keystrokes.take(); + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; + }; + let action_keystrokes = keybind_for_close_action.keystrokes(); + + if let Some(mut close_keystrokes) = self.close_keystrokes.take() { + let mut index = 0; + + while index < action_keystrokes.len() && index < close_keystrokes.len() { + if !close_keystrokes[index].should_match(&action_keystrokes[index]) { + break; + } + index += 1; + } + if index == close_keystrokes.len() { + if index >= action_keystrokes.len() { + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; + } + if keystroke.should_match(&action_keystrokes[index]) { + if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 { + self.stop_recording(&StopRecording, window, cx); + return CloseKeystrokeResult::Close; + } else { + close_keystrokes.push(keystroke.clone()); + self.close_keystrokes = Some(close_keystrokes); + return CloseKeystrokeResult::Partial; + } + } else { + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; + } + } + } else if let Some(first_action_keystroke) = action_keystrokes.first() + && keystroke.should_match(first_action_keystroke) + { + self.close_keystrokes = Some(vec![keystroke.clone()]); + return CloseKeystrokeResult::Partial; + } + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; + } + + fn on_modifiers_changed( + &mut self, + event: &ModifiersChangedEvent, + _window: &mut Window, + cx: &mut Context, + ) { + let keystrokes_len = self.keystrokes.len(); + + if self.previous_modifiers.modified() + && event.modifiers.is_subset_of(&self.previous_modifiers) + { + self.previous_modifiers &= event.modifiers; + cx.stop_propagation(); + return; + } + + if let Some(last) = self.keystrokes.last_mut() + && last.key.is_empty() + && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX + { + if self.search { + if self.previous_modifiers.modified() { + last.modifiers |= event.modifiers; + self.previous_modifiers |= event.modifiers; + } else { + self.keystrokes.push(Self::dummy(event.modifiers)); + self.previous_modifiers |= event.modifiers; + } + } else if !event.modifiers.modified() { + self.keystrokes.pop(); + } else { + last.modifiers = event.modifiers; + } + + self.keystrokes_changed(cx); + } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { + self.keystrokes.push(Self::dummy(event.modifiers)); + if self.search { + self.previous_modifiers |= event.modifiers; + } + self.keystrokes_changed(cx); + } + cx.stop_propagation(); + } + + fn handle_keystroke( + &mut self, + keystroke: &Keystroke, + window: &mut Window, + cx: &mut Context, + ) { + let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); + if close_keystroke_result != CloseKeystrokeResult::Close { + let key_len = self.keystrokes.len(); + if let Some(last) = self.keystrokes.last_mut() + && last.key.is_empty() + && key_len <= Self::KEYSTROKE_COUNT_MAX + { + if self.search { + last.key = keystroke.key.clone(); + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.close_keystrokes_start = Some(self.keystrokes.len() - 1); + } + if self.search { + self.previous_modifiers = keystroke.modifiers; + } + self.keystrokes_changed(cx); + cx.stop_propagation(); + return; + } else { + self.keystrokes.pop(); + } + } + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.close_keystrokes_start = Some(self.keystrokes.len()); + } + self.keystrokes.push(keystroke.clone()); + if self.search { + self.previous_modifiers = keystroke.modifiers; + } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX + && keystroke.modifiers.modified() + { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); + } + } else if close_keystroke_result != CloseKeystrokeResult::Partial { + self.clear_keystrokes(&ClearKeystrokes, window, cx); + } + } + self.keystrokes_changed(cx); + cx.stop_propagation(); + } + + fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context) { + if self.intercept_subscription.is_none() { + let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| { + this.handle_keystroke(&event.keystroke, window, cx); + }); + self.intercept_subscription = Some(cx.intercept_keystrokes(listener)) + } + } + + fn on_inner_focus_out( + &mut self, + _event: gpui::FocusOutEvent, + _window: &mut Window, + cx: &mut Context, + ) { + self.intercept_subscription.take(); + cx.notify(); + } + + fn render_keystrokes(&self, is_recording: bool) -> impl Iterator { + let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref() + && self.keystrokes.is_empty() + { + if is_recording { + &[] + } else { + placeholders.as_slice() + } + } else { + &self.keystrokes + }; + keystrokes.iter().map(move |keystroke| { + h_flex().children(ui::render_keystroke( + keystroke, + Some(Color::Default), + Some(rems(0.875).into()), + ui::PlatformStyle::platform(), + false, + )) + }) + } + + pub fn start_recording( + &mut self, + _: &StartRecording, + window: &mut Window, + cx: &mut Context, + ) { + window.focus(&self.inner_focus_handle); + self.clear_keystrokes(&ClearKeystrokes, window, cx); + self.previous_modifiers = window.modifiers(); + #[cfg(test)] + { + self.recording = true; + } + cx.stop_propagation(); + } + + pub fn stop_recording( + &mut self, + _: &StopRecording, + window: &mut Window, + cx: &mut Context, + ) { + if !self.is_recording(window) { + return; + } + window.focus(&self.outer_focus_handle); + if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() + && close_keystrokes_start < self.keystrokes.len() + { + self.keystrokes.drain(close_keystrokes_start..); + } + self.close_keystrokes.take(); + #[cfg(test)] + { + self.recording = false; + } + cx.notify(); + } + + pub fn clear_keystrokes( + &mut self, + _: &ClearKeystrokes, + _window: &mut Window, + cx: &mut Context, + ) { + self.keystrokes.clear(); + self.keystrokes_changed(cx); + } + + fn is_recording(&self, window: &Window) -> bool { + #[cfg(test)] + { + if true { + // in tests, we just need a simple bool that is toggled on start and stop recording + return self.recording; + } + } + // however, in the real world, checking if the inner focus handle is focused + // is a much more reliable check, as the intercept keystroke handlers are installed + // on focus of the inner focus handle, thereby ensuring our recording state does + // not get de-synced + return self.inner_focus_handle.is_focused(window); + } +} + +impl EventEmitter<()> for KeystrokeInput {} + +impl Focusable for KeystrokeInput { + fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle { + self.outer_focus_handle.clone() + } +} + +impl Render for KeystrokeInput { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.theme().colors(); + let is_focused = self.outer_focus_handle.contains_focused(window, cx); + let is_recording = self.is_recording(window); + + let horizontal_padding = rems_from_px(64.); + + let recording_bg_color = colors + .editor_background + .blend(colors.text_accent.opacity(0.1)); + + let recording_pulse = |color: Color| { + Icon::new(IconName::Circle) + .size(IconSize::Small) + .color(Color::Error) + .with_animation( + "recording-pulse", + Animation::new(std::time::Duration::from_secs(2)) + .repeat() + .with_easing(gpui::pulsating_between(0.4, 0.8)), + { + let color = color.color(cx); + move |this, delta| this.color(Color::Custom(color.opacity(delta))) + }, + ) + }; + + let recording_indicator = h_flex() + .h_4() + .pr_1() + .gap_0p5() + .border_1() + .border_color(colors.border) + .bg(colors + .editor_background + .blend(colors.text_accent.opacity(0.1))) + .rounded_sm() + .child(recording_pulse(Color::Error)) + .child( + Label::new("REC") + .size(LabelSize::XSmall) + .weight(FontWeight::SEMIBOLD) + .color(Color::Error), + ); + + let search_indicator = h_flex() + .h_4() + .pr_1() + .gap_0p5() + .border_1() + .border_color(colors.border) + .bg(colors + .editor_background + .blend(colors.text_accent.opacity(0.1))) + .rounded_sm() + .child(recording_pulse(Color::Accent)) + .child( + Label::new("SEARCH") + .size(LabelSize::XSmall) + .weight(FontWeight::SEMIBOLD) + .color(Color::Accent), + ); + + let record_icon = if self.search { + IconName::MagnifyingGlass + } else { + IconName::PlayFilled + }; + + h_flex() + .id("keystroke-input") + .track_focus(&self.outer_focus_handle) + .py_2() + .px_3() + .gap_2() + .min_h_10() + .w_full() + .flex_1() + .justify_between() + .rounded_lg() + .overflow_hidden() + .map(|this| { + if is_recording { + this.bg(recording_bg_color) + } else { + this.bg(colors.editor_background) + } + }) + .border_1() + .border_color(colors.border_variant) + .when(is_focused, |parent| { + parent.border_color(colors.border_focused) + }) + .key_context(Self::key_context()) + .on_action(cx.listener(Self::start_recording)) + .on_action(cx.listener(Self::clear_keystrokes)) + .child( + h_flex() + .w(horizontal_padding) + .gap_0p5() + .justify_start() + .flex_none() + .when(is_recording, |this| { + this.map(|this| { + if self.search { + this.child(search_indicator) + } else { + this.child(recording_indicator) + } + }) + }), + ) + .child( + h_flex() + .id("keystroke-input-inner") + .track_focus(&self.inner_focus_handle) + .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) + .size_full() + .when(!self.search, |this| { + this.focus(|mut style| { + style.border_color = Some(colors.border_focused); + style + }) + }) + .w_full() + .min_w_0() + .justify_center() + .flex_wrap() + .gap(ui::DynamicSpacing::Base04.rems(cx)) + .children(self.render_keystrokes(is_recording)), + ) + .child( + h_flex() + .w(horizontal_padding) + .gap_0p5() + .justify_end() + .flex_none() + .map(|this| { + if is_recording { + this.child( + IconButton::new("stop-record-btn", IconName::StopFilled) + .shape(IconButtonShape::Square) + .map(|this| { + this.tooltip(Tooltip::for_action_title( + if self.search { + "Stop Searching" + } else { + "Stop Recording" + }, + &StopRecording, + )) + }) + .icon_color(Color::Error) + .on_click(cx.listener(|this, _event, window, cx| { + this.stop_recording(&StopRecording, window, cx); + })), + ) + } else { + this.child( + IconButton::new("record-btn", record_icon) + .shape(IconButtonShape::Square) + .map(|this| { + this.tooltip(Tooltip::for_action_title( + if self.search { + "Start Searching" + } else { + "Start Recording" + }, + &StartRecording, + )) + }) + .when(!is_focused, |this| this.icon_color(Color::Muted)) + .on_click(cx.listener(|this, _event, window, cx| { + this.start_recording(&StartRecording, window, cx); + })), + ) + } + }) + .child( + IconButton::new("clear-btn", IconName::Delete) + .shape(IconButtonShape::Square) + .tooltip(Tooltip::for_action_title( + "Clear Keystrokes", + &ClearKeystrokes, + )) + .when(!is_recording || !is_focused, |this| { + this.icon_color(Color::Muted) + }) + .on_click(cx.listener(|this, _event, window, cx| { + this.clear_keystrokes(&ClearKeystrokes, window, cx); + })), + ), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fs::FakeFs; + use gpui::{Entity, TestAppContext, VisualTestContext}; + use project::Project; + use settings::SettingsStore; + use workspace::Workspace; + + pub struct KeystrokeInputTestHelper { + input: Entity, + current_modifiers: Modifiers, + cx: VisualTestContext, + } + + impl KeystrokeInputTestHelper { + /// Creates a new test helper with default settings + pub fn new(mut cx: VisualTestContext) -> Self { + let input = cx.new_window_entity(|window, cx| KeystrokeInput::new(None, window, cx)); + + let mut helper = Self { + input, + current_modifiers: Modifiers::default(), + cx, + }; + + helper.start_recording(); + helper + } + + /// Sets search mode on the input + pub fn with_search_mode(&mut self, search: bool) -> &mut Self { + self.input.update(&mut self.cx, |input, _| { + input.set_search(search); + }); + self + } + + /// Sends a keystroke event based on string description + /// Examples: "a", "ctrl-a", "cmd-shift-z", "escape" + pub fn send_keystroke(&mut self, keystroke_input: &str) -> &mut Self { + self.expect_is_recording(true); + let keystroke_str = if keystroke_input.ends_with('-') { + format!("{}_", keystroke_input) + } else { + keystroke_input.to_string() + }; + + let mut keystroke = Keystroke::parse(&keystroke_str) + .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_input)); + + // Remove the dummy key if we added it for modifier-only keystrokes + if keystroke_input.ends_with('-') && keystroke_str.ends_with("_") { + keystroke.key = "".to_string(); + } + + // Combine current modifiers with keystroke modifiers + keystroke.modifiers |= self.current_modifiers; + + self.input.update_in(&mut self.cx, |input, window, cx| { + input.handle_keystroke(&keystroke, window, cx); + }); + + // Don't update current_modifiers for keystrokes with actual keys + if keystroke.key.is_empty() { + self.current_modifiers = keystroke.modifiers; + } + self + } + + /// Sends a modifier change event based on string description + /// Examples: "+ctrl", "-ctrl", "+cmd+shift", "-all" + pub fn send_modifiers(&mut self, modifiers: &str) -> &mut Self { + self.expect_is_recording(true); + let new_modifiers = if modifiers == "-all" { + Modifiers::default() + } else { + self.parse_modifier_change(modifiers) + }; + + let event = ModifiersChangedEvent { + modifiers: new_modifiers, + capslock: gpui::Capslock::default(), + }; + + self.input.update_in(&mut self.cx, |input, window, cx| { + input.on_modifiers_changed(&event, window, cx); + }); + + self.current_modifiers = new_modifiers; + self + } + + /// Sends multiple events in sequence + /// Each event string is either a keystroke or modifier change + pub fn send_events(&mut self, events: &[&str]) -> &mut Self { + self.expect_is_recording(true); + for event in events { + if event.starts_with('+') || event.starts_with('-') { + self.send_modifiers(event); + } else { + self.send_keystroke(event); + } + } + self + } + + /// Verifies that the keystrokes match the expected strings + #[track_caller] + pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + let expected_keystrokes: Result, _> = expected + .iter() + .map(|s| { + let keystroke_str = if s.ends_with('-') { + format!("{}_", s) + } else { + s.to_string() + }; + + let mut keystroke = Keystroke::parse(&keystroke_str)?; + + // Remove the dummy key if we added it for modifier-only keystrokes + if s.ends_with('-') && keystroke_str.ends_with("_") { + keystroke.key = "".to_string(); + } + + Ok(keystroke) + }) + .collect(); + + let expected_keystrokes = expected_keystrokes + .unwrap_or_else(|e: anyhow::Error| panic!("Invalid expected keystroke: {}", e)); + + let actual = self + .input + .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); + assert_eq!( + actual.len(), + expected_keystrokes.len(), + "Keystroke count mismatch. Expected: {:?}, Actual: {:?}", + expected_keystrokes + .iter() + .map(|k| k.unparse()) + .collect::>(), + actual.iter().map(|k| k.unparse()).collect::>() + ); + + for (i, (actual, expected)) in actual.iter().zip(expected_keystrokes.iter()).enumerate() + { + assert_eq!( + actual.unparse(), + expected.unparse(), + "Keystroke {} mismatch. Expected: '{}', Actual: '{}'", + i, + expected.unparse(), + actual.unparse() + ); + } + self + } + + /// Verifies that there are no keystrokes + #[track_caller] + pub fn expect_empty(&mut self) -> &mut Self { + self.expect_keystrokes(&[]) + } + + /// Starts recording keystrokes + #[track_caller] + pub fn start_recording(&mut self) -> &mut Self { + self.expect_is_recording(false); + self.input.update_in(&mut self.cx, |input, window, cx| { + input.start_recording(&StartRecording, window, cx); + }); + self + } + + /// Stops recording keystrokes + pub fn stop_recording(&mut self) -> &mut Self { + self.expect_is_recording(true); + self.input.update_in(&mut self.cx, |input, window, cx| { + input.stop_recording(&StopRecording, window, cx); + }); + self + } + + /// Clears all keystrokes + pub fn clear_keystrokes(&mut self) -> &mut Self { + self.input.update_in(&mut self.cx, |input, window, cx| { + input.clear_keystrokes(&ClearKeystrokes, window, cx); + }); + self + } + + /// Verifies the recording state + #[track_caller] + pub fn expect_is_recording(&mut self, expected: bool) -> &mut Self { + let actual = self + .input + .update_in(&mut self.cx, |input, window, _| input.is_recording(window)); + assert_eq!( + actual, expected, + "Recording state mismatch. Expected: {}, Actual: {}", + expected, actual + ); + self + } + + /// Parses modifier change strings like "+ctrl", "-shift", "+cmd+alt" + fn parse_modifier_change(&self, modifiers_str: &str) -> Modifiers { + let mut modifiers = self.current_modifiers; + + if let Some(to_add) = modifiers_str.strip_prefix('+') { + // Add modifiers + for modifier in to_add.split('+') { + match modifier { + "ctrl" | "control" => modifiers.control = true, + "alt" | "option" => modifiers.alt = true, + "shift" => modifiers.shift = true, + "cmd" | "command" => modifiers.platform = true, + "fn" | "function" => modifiers.function = true, + _ => panic!("Unknown modifier: {}", modifier), + } + } + } else if let Some(to_remove) = modifiers_str.strip_prefix('-') { + // Remove modifiers + for modifier in to_remove.split('+') { + match modifier { + "ctrl" | "control" => modifiers.control = false, + "alt" | "option" => modifiers.alt = false, + "shift" => modifiers.shift = false, + "cmd" | "command" => modifiers.platform = false, + "fn" | "function" => modifiers.function = false, + _ => panic!("Unknown modifier: {}", modifier), + } + } + } + + modifiers + } + } + + async fn init_test(cx: &mut TestAppContext) -> KeystrokeInputTestHelper { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + project::Project::init_settings(cx); + workspace::init_settings(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.clone(), window, cx)); + let cx = VisualTestContext::from_window(*workspace, cx); + KeystrokeInputTestHelper::new(cx) + } + + #[gpui::test] + async fn test_basic_keystroke_input(cx: &mut TestAppContext) { + init_test(cx) + .await + .send_keystroke("a") + .clear_keystrokes() + .expect_empty(); + } + + #[gpui::test] + async fn test_modifier_handling(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "a", "-ctrl"]) + .expect_keystrokes(&["ctrl-a"]); + } + + #[gpui::test] + async fn test_multiple_modifiers(cx: &mut TestAppContext) { + init_test(cx) + .await + .send_keystroke("cmd-shift-z") + .expect_keystrokes(&["cmd-shift-z", "cmd-shift-"]); + } + + #[gpui::test] + async fn test_search_mode_behavior(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+cmd", "shift-f", "-cmd"]) + // In search mode, when completing a modifier-only keystroke with a key, + // only the original modifiers are preserved, not the keystroke's modifiers + .expect_keystrokes(&["cmd-f"]); + } + + #[gpui::test] + async fn test_keystroke_limit(cx: &mut TestAppContext) { + init_test(cx) + .await + .send_keystroke("a") + .send_keystroke("b") + .send_keystroke("c") + .expect_keystrokes(&["a", "b", "c"]) // At max limit + .send_keystroke("d") + .expect_empty(); // Should clear when exceeding limit + } + + #[gpui::test] + async fn test_modifier_release_all(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl+shift", "a", "-all"]) + .expect_keystrokes(&["ctrl-shift-a"]); + } + + #[gpui::test] + async fn test_search_new_modifiers_not_added_until_all_released(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl+shift", "a", "-ctrl"]) + .expect_keystrokes(&["ctrl-shift-a"]) + .send_events(&["+ctrl"]) + .expect_keystrokes(&["ctrl-shift-a", "ctrl-shift-"]); + } + + #[gpui::test] + async fn test_previous_modifiers_no_effect_when_not_search(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["+ctrl+shift", "a", "-all"]) + .expect_keystrokes(&["ctrl-shift-a"]); + } + + #[gpui::test] + async fn test_keystroke_limit_overflow_non_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["a", "b", "c", "d"]) // 4 keystrokes, exceeds limit of 3 + .expect_empty(); // Should clear when exceeding limit + } + + #[gpui::test] + async fn test_complex_modifier_sequences(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "+shift", "+alt", "a", "-ctrl", "-shift", "-alt"]) + .expect_keystrokes(&["ctrl-shift-alt-a"]); + } + + #[gpui::test] + async fn test_modifier_only_keystrokes_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "+shift", "-ctrl", "-shift"]) + .expect_keystrokes(&["ctrl-shift-"]); // Modifier-only sequences create modifier-only keystrokes + } + + #[gpui::test] + async fn test_modifier_only_keystrokes_non_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["+ctrl", "+shift", "-ctrl", "-shift"]) + .expect_empty(); // Modifier-only sequences get filtered in non-search mode + } + + #[gpui::test] + async fn test_rapid_modifier_changes(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "-ctrl", "+shift", "-shift", "+alt", "a", "-alt"]) + .expect_keystrokes(&["ctrl-", "shift-", "alt-a"]); + } + + #[gpui::test] + async fn test_clear_keystrokes_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "a", "-ctrl", "b"]) + .expect_keystrokes(&["ctrl-a", "b"]) + .clear_keystrokes() + .expect_empty(); + } + + #[gpui::test] + async fn test_non_search_mode_modifier_key_sequence(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["+ctrl", "a"]) + .expect_keystrokes(&["ctrl-a", "ctrl-"]) + .send_events(&["-ctrl"]) + .expect_keystrokes(&["ctrl-a"]); // Non-search mode filters trailing empty keystrokes + } + + #[gpui::test] + async fn test_all_modifiers_at_once(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl+shift+alt+cmd", "a", "-all"]) + .expect_keystrokes(&["ctrl-shift-alt-cmd-a"]); + } + + #[gpui::test] + async fn test_keystrokes_at_exact_limit(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["a", "b", "c"]) // exactly 3 keystrokes (at limit) + .expect_keystrokes(&["a", "b", "c"]) + .send_events(&["d"]) // should clear when exceeding + .expect_empty(); + } + + #[gpui::test] + async fn test_function_modifier_key(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+fn", "f1", "-fn"]) + .expect_keystrokes(&["fn-f1"]); + } + + #[gpui::test] + async fn test_start_stop_recording(cx: &mut TestAppContext) { + init_test(cx) + .await + .send_events(&["a", "b"]) + .expect_keystrokes(&["a", "b"]) // start_recording clears existing keystrokes + .stop_recording() + .expect_is_recording(false) + .start_recording() + .send_events(&["c"]) + .expect_keystrokes(&["c"]); + } + + #[gpui::test] + async fn test_modifier_sequence_with_interruption(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "+shift", "a", "-shift", "b", "-ctrl"]) + .expect_keystrokes(&["ctrl-shift-a", "ctrl-b"]); + } + + #[gpui::test] + async fn test_empty_key_sequence_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&[]) // No events at all + .expect_empty(); + } + + #[gpui::test] + async fn test_modifier_sequence_completion_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "+shift", "-shift", "a", "-ctrl"]) + .expect_keystrokes(&["ctrl-shift-a"]); + } + + #[gpui::test] + async fn test_triple_escape_stops_recording_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["a", "escape", "escape", "escape"]) + .expect_keystrokes(&["a"]) // Triple escape removes final escape, stops recording + .expect_is_recording(false); + } + + #[gpui::test] + async fn test_triple_escape_stops_recording_non_search_mode(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["a", "escape", "escape", "escape"]) + .expect_keystrokes(&["a"]); // Triple escape stops recording but only removes final escape + } + + #[gpui::test] + async fn test_triple_escape_at_keystroke_limit(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["a", "b", "c", "escape", "escape", "escape"]) // 6 keystrokes total, exceeds limit + .expect_keystrokes(&["a", "b", "c"]); // Triple escape stops recording and removes escapes, leaves original keystrokes + } + + #[gpui::test] + async fn test_interrupted_escape_sequence(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["escape", "escape", "a", "escape"]) // Partial escape sequence interrupted by 'a' + .expect_keystrokes(&["escape", "escape", "a"]); // Escape sequence interrupted by 'a', no close triggered + } + + #[gpui::test] + async fn test_interrupted_escape_sequence_within_limit(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["escape", "escape", "a"]) // Partial escape sequence interrupted by 'a' (3 keystrokes, at limit) + .expect_keystrokes(&["escape", "escape", "a"]); // Should not trigger close, interruption resets escape detection + } + + #[gpui::test] + async fn test_partial_escape_sequence_no_close(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["escape", "escape"]) // Only 2 escapes, not enough to close + .expect_keystrokes(&["escape", "escape"]) + .expect_is_recording(true); // Should remain in keystrokes, no close triggered + } + + #[gpui::test] + async fn test_recording_state_after_triple_escape(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["a", "escape", "escape", "escape"]) + .expect_keystrokes(&["a"]) // Triple escape stops recording, removes final escape + .expect_is_recording(false); + } + + #[gpui::test] + async fn test_triple_escape_mixed_with_other_keystrokes(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["a", "escape", "b", "escape", "escape"]) // Mixed sequence, should not trigger close + .expect_keystrokes(&["a", "escape", "b"]); // No complete triple escape sequence, stays at limit + } + + #[gpui::test] + async fn test_triple_escape_only(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["escape", "escape", "escape"]) // Pure triple escape sequence + .expect_empty(); + } +} diff --git a/crates/settings_ui/src/ui_components/mod.rs b/crates/settings_ui/src/ui_components/mod.rs index 13971b0a5df8e3b188de1df94faab3df94aa86da..5d6463a61a21afd5208b75af0362f6f7956f5e56 100644 --- a/crates/settings_ui/src/ui_components/mod.rs +++ b/crates/settings_ui/src/ui_components/mod.rs @@ -1 +1,2 @@ +pub mod keystroke_input; pub mod table; From 902c17ac1a1c5d012c9ba0a7675e7f1ed1b98de2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 29 Jul 2025 14:15:17 -0400 Subject: [PATCH 16/35] Add Zed badge to README.md (#35287) Release Notes: - N/A --- README.md | 2 ++ assets/badge/v0.json | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 assets/badge/v0.json diff --git a/README.md b/README.md index 4c794efc3de3f26fb1e5dbf943f6c7379174791a..9ea7b81de06b4b0bcc78627d7992629769d2e047 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Zed +[![zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev) + [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). diff --git a/assets/badge/v0.json b/assets/badge/v0.json new file mode 100644 index 0000000000000000000000000000000000000000..4b3bbf45ca5547f994b07a50579831f3379aaa48 --- /dev/null +++ b/assets/badge/v0.json @@ -0,0 +1,8 @@ +{ + "label": "", + "message": "zed", + "logoSvg": "", + "logoWidth": 16, + "labelColor": "grey", + "color": "#261230" +} From 72f8fa6d1e4d0a09a54e3a25d6be333bd692ed08 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 29 Jul 2025 14:24:10 -0400 Subject: [PATCH 17/35] Adjust Zed badge (#35290) - Inline badges - Set label background fill color to black - Uppercase Zed text - Remove gray padding Release Notes: - N/A --- README.md | 3 +-- assets/badge/v0.json | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9ea7b81de06b4b0bcc78627d7992629769d2e047..38547c1ca441b918b773d8b1a884a1e3f48c785f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Zed -[![zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev) - +[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev) [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). diff --git a/assets/badge/v0.json b/assets/badge/v0.json index 4b3bbf45ca5547f994b07a50579831f3379aaa48..3ff95d33787bbe75ae32ae20a11b3cfd0cb4a8b2 100644 --- a/assets/badge/v0.json +++ b/assets/badge/v0.json @@ -1,8 +1,8 @@ { "label": "", "message": "zed", - "logoSvg": "", + "logoSvg": "", "logoWidth": 16, - "labelColor": "grey", + "labelColor": "black", "color": "#261230" } From 7878eacc7348d23468370b24b1412b78d86c967e Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Tue, 29 Jul 2025 15:00:41 -0400 Subject: [PATCH 18/35] python: Use a single workspace folder for basedpyright (#35292) Treat the new basedpyright adapter the same as pyright was treated in #35243. Release Notes: - N/A --- crates/languages/src/python.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 4a0cc7078b3e4097ba8ff77b8c546528ce9848c3..0524c02fd5b95c4d8ccc2fbcbd2286a53a900fa2 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1625,6 +1625,10 @@ impl LspAdapter for BasedPyrightLspAdapter { fn manifest_name(&self) -> Option { Some(SharedString::new_static("pyproject.toml").into()) } + + fn workspace_folders_content(&self) -> WorkspaceFoldersContent { + WorkspaceFoldersContent::WorktreeRoot + } } #[cfg(test)] From 397314232451ba589aacb6a2053c8ab36d19dfdd Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 29 Jul 2025 15:09:31 -0400 Subject: [PATCH 19/35] Adjust Zed badge (#35294) - Make right side background white - Fix Zed casing Release Notes: - N/A --- assets/badge/v0.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/badge/v0.json b/assets/badge/v0.json index 3ff95d33787bbe75ae32ae20a11b3cfd0cb4a8b2..c7d18bb42b71f2d57696ce56b8211e0395afab9d 100644 --- a/assets/badge/v0.json +++ b/assets/badge/v0.json @@ -1,8 +1,8 @@ { "label": "", - "message": "zed", + "message": "Zed", "logoSvg": "", "logoWidth": 16, "labelColor": "black", - "color": "#261230" + "color": "white" } From 1501ae001356fbf16490083f9f1bd3ad93d022b5 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Tue, 29 Jul 2025 22:24:34 +0200 Subject: [PATCH 20/35] Upgrade rodio to 0.21 (#34368) Hi all, We just released [Rodio 0.21](https://github.com/RustAudio/rodio/blob/master/CHANGELOG.md) :partying_face: with quite some breaking changes. This should take care of those for zed. I tested it by hopping in and out some of the zed channels, sound seems to still work. Given zed uses tracing I also took the liberty of enabling the tracing feature for rodio. edit: We changed the default wav decoder from hound to symphonia. The latter has a slightly more restrictive license however that should be no issue here (as the audio crate uses the GPL) Release Notes: - N/A --- Cargo.lock | 171 +++++++++++++----------------- crates/audio/Cargo.toml | 2 +- crates/audio/src/assets.rs | 9 +- crates/audio/src/audio.rs | 14 +-- tooling/workspace-hack/Cargo.toml | 6 -- 5 files changed, 85 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ab4a85c7d28cbed4cfba91b93210751a783a7a4..5e35202e900d216c9f0a088b3e86c4e03be05223 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3684,17 +3684,6 @@ dependencies = [ "libm", ] -[[package]] -name = "coreaudio-rs" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - [[package]] name = "coreaudio-rs" version = "0.12.1" @@ -3752,29 +3741,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpal" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" -dependencies = [ - "alsa", - "core-foundation-sys", - "coreaudio-rs 0.11.3", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2", - "ndk 0.8.0", - "ndk-context", - "oboe", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.54.0", -] - [[package]] name = "cpal" version = "0.16.0" @@ -3788,7 +3754,7 @@ dependencies = [ "js-sys", "libc", "mach2", - "ndk 0.9.0", + "ndk", "ndk-context", "num-derive", "num-traits", @@ -5367,6 +5333,12 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "extension" version = "0.1.0" @@ -7742,12 +7714,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "html5ever" version = "0.27.0" @@ -9595,7 +9561,7 @@ dependencies = [ "core-foundation 0.10.0", "core-video", "coreaudio-rs 0.12.1", - "cpal 0.16.0", + "cpal", "futures 0.3.31", "gpui", "gpui_tokio", @@ -10366,20 +10332,6 @@ dependencies = [ "workspace-hack", ] -[[package]] -name = "ndk" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" -dependencies = [ - "bitflags 2.9.0", - "jni-sys", - "log", - "ndk-sys 0.5.0+25.2.9519653", - "num_enum", - "thiserror 1.0.69", -] - [[package]] name = "ndk" version = "0.9.0" @@ -10389,7 +10341,7 @@ dependencies = [ "bitflags 2.9.0", "jni-sys", "log", - "ndk-sys 0.6.0+11769913", + "ndk-sys", "num_enum", "thiserror 1.0.69", ] @@ -10400,15 +10352,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-sys" -version = "0.5.0+25.2.9519653" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" -dependencies = [ - "jni-sys", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -10978,29 +10921,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk 0.8.0", - "ndk-context", - "num-derive", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - [[package]] name = "ollama" version = "0.1.0" @@ -13780,12 +13700,15 @@ dependencies = [ [[package]] name = "rodio" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ - "cpal 0.15.3", - "hound", + "cpal", + "dasp_sample", + "num-rational", + "symphonia", + "tracing", ] [[package]] @@ -15806,6 +15729,66 @@ dependencies = [ "zeno", ] +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-codec-pcm", + "symphonia-core", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "syn" version = "1.0.109" @@ -19693,14 +19676,12 @@ dependencies = [ "cc", "chrono", "cipher", - "clang-sys", "clap", "clap_builder", "codespan-reporting 0.12.0", "concurrent-queue", "core-foundation 0.9.4", "core-foundation-sys", - "coreaudio-sys", "cranelift-codegen", "crc32fast", "crossbeam-epoch", diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index 960aaf8e08d864f7bf3b1883951d0f7d22ad56ed..d857a3eb2f6c112b9d6a9851715718047f72ccbf 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -18,6 +18,6 @@ collections.workspace = true derive_more.workspace = true gpui.workspace = true parking_lot.workspace = true -rodio = { version = "0.20.0", default-features = false, features = ["wav"] } +rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs index 02da79dc24f067795b6636fc7fa031bce95cf935..fd5c935d875960f4fd9bf30494301f4811b22448 100644 --- a/crates/audio/src/assets.rs +++ b/crates/audio/src/assets.rs @@ -3,12 +3,9 @@ use std::{io::Cursor, sync::Arc}; use anyhow::{Context as _, Result}; use collections::HashMap; use gpui::{App, AssetSource, Global}; -use rodio::{ - Decoder, Source, - source::{Buffered, SamplesConverter}, -}; +use rodio::{Decoder, Source, source::Buffered}; -type Sound = Buffered>>, f32>>; +type Sound = Buffered>>>; pub struct SoundRegistry { cache: Arc>>, @@ -48,7 +45,7 @@ impl SoundRegistry { .with_context(|| format!("No asset available for path {path}"))?? .into_owned(); let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.convert_samples::().buffered(); + let source = Decoder::new(cursor)?.buffered(); self.cache.lock().insert(name.to_string(), source.clone()); diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index e7b9a59e8f281e9fb19481b118990b07c439448f..44baa16aa20a3e4b7651744974cfc085dcde7fb1 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,7 +1,7 @@ use assets::SoundRegistry; use derive_more::{Deref, DerefMut}; use gpui::{App, AssetSource, BorrowAppContext, Global}; -use rodio::{OutputStream, OutputStreamHandle}; +use rodio::{OutputStream, OutputStreamBuilder}; use util::ResultExt; mod assets; @@ -37,8 +37,7 @@ impl Sound { #[derive(Default)] pub struct Audio { - _output_stream: Option, - output_handle: Option, + output_handle: Option, } #[derive(Deref, DerefMut)] @@ -51,11 +50,9 @@ impl Audio { Self::default() } - fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> { + fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { - let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip(); - self.output_handle = output_handle; - self._output_stream = _output_stream; + self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); } self.output_handle.as_ref() @@ -69,7 +66,7 @@ impl Audio { cx.update_global::(|this, cx| { let output_handle = this.ensure_output_exists()?; let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; - output_handle.play_raw(source).log_err()?; + output_handle.mixer().add(source); Some(()) }); } @@ -80,7 +77,6 @@ impl Audio { } cx.update_global::(|this, _| { - this._output_stream.take(); this.output_handle.take(); }); } diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 10264540262bfd021577a954bf8933a2554ca222..e5123d5ab3955e7e30e0b4fd2d98b8985e7fb8e7 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -284,7 +284,6 @@ winnow = { version = "0.7", features = ["simd"] } codespan-reporting = { version = "0.12" } core-foundation = { version = "0.9" } core-foundation-sys = { version = "0.8" } -coreaudio-sys = { version = "0.2", default-features = false, features = ["audio_toolbox", "audio_unit", "core_audio", "core_midi", "open_al"] } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } @@ -310,11 +309,9 @@ tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } [target.x86_64-apple-darwin.build-dependencies] -clang-sys = { version = "1", default-features = false, features = ["clang_11_0", "runtime"] } codespan-reporting = { version = "0.12" } core-foundation = { version = "0.9" } core-foundation-sys = { version = "0.8" } -coreaudio-sys = { version = "0.2", default-features = false, features = ["audio_toolbox", "audio_unit", "core_audio", "core_midi", "open_al"] } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } @@ -344,7 +341,6 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti codespan-reporting = { version = "0.12" } core-foundation = { version = "0.9" } core-foundation-sys = { version = "0.8" } -coreaudio-sys = { version = "0.2", default-features = false, features = ["audio_toolbox", "audio_unit", "core_audio", "core_midi", "open_al"] } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } @@ -370,11 +366,9 @@ tokio-stream = { version = "0.1", features = ["fs"] } tower = { version = "0.5", default-features = false, features = ["timeout", "util"] } [target.aarch64-apple-darwin.build-dependencies] -clang-sys = { version = "1", default-features = false, features = ["clang_11_0", "runtime"] } codespan-reporting = { version = "0.12" } core-foundation = { version = "0.9" } core-foundation-sys = { version = "0.8" } -coreaudio-sys = { version = "0.2", default-features = false, features = ["audio_toolbox", "audio_unit", "core_audio", "core_midi", "open_al"] } foldhash = { version = "0.1", default-features = false, features = ["std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-features = false, features = ["std"] } gimli = { version = "0.31", default-features = false, features = ["read", "std", "write"] } From 5fa212183ace0388735c7aa05e9bc3955a7970a5 Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Tue, 29 Jul 2025 14:22:53 -0700 Subject: [PATCH 21/35] Fix animations in the component preview (#33673) Fixes #33869 The Animation page in the Component Preview had a few issues. * The animations only ran once, so you couldn't watch animations below the fold. * The offset math was wrong, so some animated elements were rendered outside of their parent container. * The "animate in from right" elements were defined with an initial `.left()` offset, which overrode the animation behavior. I made fixes to address these issues. In particular, every time you click the active list item, it renders the preview again (which causes the animations to run again). Before: https://github.com/user-attachments/assets/a1fa2e3f-653c-4b83-a6ed-c55ca9c78ad4 After: https://github.com/user-attachments/assets/3623bbbc-9047-4443-b7f3-96bd92f582bf Release Notes: - N/A --- crates/ui/src/styles/animation.rs | 18 +++++++++--------- crates/zed/src/zed/component_preview.rs | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs index 50c4e0eb0daf6d0868c5ab76db5374d695863f99..0649bee1f82b666a5fd187fa84aeba45ede36f8a 100644 --- a/crates/ui/src/styles/animation.rs +++ b/crates/ui/src/styles/animation.rs @@ -109,7 +109,7 @@ impl Component for Animation { fn preview(_window: &mut Window, _cx: &mut App) -> Option { let container_size = 128.0; let element_size = 32.0; - let left_offset = element_size - container_size / 2.0; + let offset = container_size / 2.0 - element_size / 2.0; Some( v_flex() .gap_6() @@ -129,7 +129,7 @@ impl Component for Animation { .id("animate-in-from-bottom") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .left(px(offset)) .rounded_md() .bg(gpui::red()) .animate_in(AnimationDirection::FromBottom, false), @@ -148,7 +148,7 @@ impl Component for Animation { .id("animate-in-from-top") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .left(px(offset)) .rounded_md() .bg(gpui::blue()) .animate_in(AnimationDirection::FromTop, false), @@ -167,7 +167,7 @@ impl Component for Animation { .id("animate-in-from-left") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .top(px(offset)) .rounded_md() .bg(gpui::green()) .animate_in(AnimationDirection::FromLeft, false), @@ -186,7 +186,7 @@ impl Component for Animation { .id("animate-in-from-right") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .top(px(offset)) .rounded_md() .bg(gpui::yellow()) .animate_in(AnimationDirection::FromRight, false), @@ -211,7 +211,7 @@ impl Component for Animation { .id("fade-animate-in-from-bottom") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .left(px(offset)) .rounded_md() .bg(gpui::red()) .animate_in(AnimationDirection::FromBottom, true), @@ -230,7 +230,7 @@ impl Component for Animation { .id("fade-animate-in-from-top") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .left(px(offset)) .rounded_md() .bg(gpui::blue()) .animate_in(AnimationDirection::FromTop, true), @@ -249,7 +249,7 @@ impl Component for Animation { .id("fade-animate-in-from-left") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .top(px(offset)) .rounded_md() .bg(gpui::green()) .animate_in(AnimationDirection::FromLeft, true), @@ -268,7 +268,7 @@ impl Component for Animation { .id("fade-animate-in-from-right") .absolute() .size(px(element_size)) - .left(px(left_offset)) + .top(px(offset)) .rounded_md() .bg(gpui::yellow()) .animate_in(AnimationDirection::FromRight, true), diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index 670793cff3816231d8f7a2e5c946643dbeaa3453..2e57152c62246505506c41b11895c7a596cb58dd 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -105,6 +105,7 @@ enum PreviewPage { struct ComponentPreview { active_page: PreviewPage, active_thread: Option>, + reset_key: usize, component_list: ListState, component_map: HashMap, components: Vec, @@ -188,6 +189,7 @@ impl ComponentPreview { let mut component_preview = Self { active_page, active_thread: None, + reset_key: 0, component_list, component_map: component_registry.component_map(), components: sorted_components, @@ -265,8 +267,13 @@ impl ComponentPreview { } fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context) { - self.active_page = page; - cx.emit(ItemEvent::UpdateTab); + if self.active_page == page { + // Force the current preview page to render again + self.reset_key = self.reset_key.wrapping_add(1); + } else { + self.active_page = page; + cx.emit(ItemEvent::UpdateTab); + } cx.notify(); } @@ -690,6 +697,7 @@ impl ComponentPreview { component.clone(), self.workspace.clone(), self.active_thread.clone(), + self.reset_key, )) .into_any_element() } else { @@ -1041,6 +1049,7 @@ pub struct ComponentPreviewPage { component: ComponentMetadata, workspace: WeakEntity, active_thread: Option>, + reset_key: usize, } impl ComponentPreviewPage { @@ -1048,6 +1057,7 @@ impl ComponentPreviewPage { component: ComponentMetadata, workspace: WeakEntity, active_thread: Option>, + reset_key: usize, // languages: Arc ) -> Self { Self { @@ -1055,6 +1065,7 @@ impl ComponentPreviewPage { component, workspace, active_thread, + reset_key, } } @@ -1155,6 +1166,7 @@ impl ComponentPreviewPage { }; v_flex() + .id(("component-preview", self.reset_key)) .size_full() .flex_1() .px_12() From 85b712c04e77cb3500facc0cd67836c5c3fdb719 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 29 Jul 2025 16:24:57 -0500 Subject: [PATCH 22/35] keymap_ui: Clear close keystroke capture on timeout (#35289) Closes #ISSUE Introduces a mechanism whereby keystrokes that have a post-fix which matches the prefix of the stop recording binding can still be entered. The solution is to introduce a (as of right now) 300ms timeout before the close keystroke state is wiped. Previously, with the default stop recording binding `esc esc esc`, searching or entering a binding ending in esc was not possible without using the mouse. `e.g.` entering keystroke `ctrl-g esc` and then attempting to hit `esc` three times would stop recording on the penultimate `esc` press and the final `esc` would not be intercepted. Now with the timeout, it is possible to enter `ctrl-g esc`, pause for a moment, then hit `esc esc esc` and end the recording with the keystroke input state being `ctrl-g esc`. I arrived at 300ms for this delay as it was long enough that I didn't run into it very often when trying to escape, but short enough that a natural pause will almost always work as expected. Release Notes: - Keymap Editor: Added a short timeout to the stop recording keybind handling in the keystroke input, so that it is now possible using the default bindings as an example (custom bindings should work as well) to search for/enter a binding ending with `escape` (with no modifier), pause for a moment, then hit `escape escape escape` to stop recording and search for/enter a keystroke ending with `escape`. --- .../src/ui_components/keystroke_input.rs | 180 +++++++++++++----- 1 file changed, 136 insertions(+), 44 deletions(-) diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 08ffe3575bcf1365add16f8afbcce370baaf48f2..a34d0a2bbd113e1434f9f4d1d924fd76897e2cf9 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - Keystroke, Modifiers, ModifiersChangedEvent, Subscription, actions, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -21,6 +21,9 @@ actions!( const KEY_CONTEXT_VALUE: &'static str = "KeystrokeInput"; +const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration = + std::time::Duration::from_millis(300); + enum CloseKeystrokeResult { Partial, Close, @@ -46,10 +49,19 @@ pub struct KeystrokeInput { intercept_subscription: Option, _focus_subscriptions: [Subscription; 2], search: bool, - /// Handles triple escape to stop recording + /// The sequence of close keystrokes being typed close_keystrokes: Option>, close_keystrokes_start: Option, previous_modifiers: Modifiers, + /// In order to support inputting keystrokes that end with a prefix of the + /// close keybind keystrokes, we clear the close keystroke capture info + /// on a timeout after a close keystroke is pressed + /// + /// e.g. if close binding is `esc esc esc` and user wants to search for + /// `ctrl-g esc`, after entering the `ctrl-g esc`, hitting `esc` twice would + /// stop recording because of the sequence of three escapes making it + /// impossible to search for anything ending in `esc` + clear_close_keystrokes_timer: Option>, #[cfg(test)] recording: bool, } @@ -79,6 +91,7 @@ impl KeystrokeInput { close_keystrokes: None, close_keystrokes_start: None, previous_modifiers: Modifiers::default(), + clear_close_keystrokes_timer: None, #[cfg(test)] recording: false, } @@ -144,6 +157,34 @@ impl KeystrokeInput { } } + fn upsert_close_keystrokes_start(&mut self, start: usize, cx: &mut Context) { + if self.close_keystrokes_start.is_some() { + return; + } + self.close_keystrokes_start = Some(start); + self.update_clear_close_keystrokes_timer(cx); + } + + fn update_clear_close_keystrokes_timer(&mut self, cx: &mut Context) { + self.clear_close_keystrokes_timer = Some(cx.spawn(async |this, cx| { + cx.background_executor() + .timer(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT) + .await; + this.update(cx, |this, _cx| { + this.end_close_keystrokes_capture(); + }) + .ok(); + })); + } + + /// Interrupt the capture of close keystrokes, but do not clear the close keystrokes + /// from the input + fn end_close_keystrokes_capture(&mut self) -> Option { + self.close_keystrokes.take(); + self.clear_close_keystrokes_timer.take(); + return self.close_keystrokes_start.take(); + } + fn handle_possible_close_keystroke( &mut self, keystroke: &Keystroke, @@ -152,8 +193,7 @@ impl KeystrokeInput { ) -> CloseKeystrokeResult { let Some(keybind_for_close_action) = Self::determine_stop_recording_binding(window) else { log::trace!("No keybinding to stop recording keystrokes in keystroke input"); - self.close_keystrokes.take(); - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; }; let action_keystrokes = keybind_for_close_action.keystrokes(); @@ -169,20 +209,20 @@ impl KeystrokeInput { } if index == close_keystrokes.len() { if index >= action_keystrokes.len() { - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; } if keystroke.should_match(&action_keystrokes[index]) { - if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 { - self.stop_recording(&StopRecording, window, cx); + close_keystrokes.push(keystroke.clone()); + if close_keystrokes.len() == action_keystrokes.len() { return CloseKeystrokeResult::Close; } else { - close_keystrokes.push(keystroke.clone()); self.close_keystrokes = Some(close_keystrokes); + self.update_clear_close_keystrokes_timer(cx); return CloseKeystrokeResult::Partial; } } else { - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; } } @@ -192,7 +232,7 @@ impl KeystrokeInput { self.close_keystrokes = Some(vec![keystroke.clone()]); return CloseKeystrokeResult::Partial; } - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; } @@ -248,36 +288,22 @@ impl KeystrokeInput { cx: &mut Context, ) { let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); - if close_keystroke_result != CloseKeystrokeResult::Close { - let key_len = self.keystrokes.len(); - if let Some(last) = self.keystrokes.last_mut() - && last.key.is_empty() - && key_len <= Self::KEYSTROKE_COUNT_MAX - { - if self.search { - last.key = keystroke.key.clone(); - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() - { - self.close_keystrokes_start = Some(self.keystrokes.len() - 1); - } - if self.search { - self.previous_modifiers = keystroke.modifiers; - } - self.keystrokes_changed(cx); - cx.stop_propagation(); - return; - } else { - self.keystrokes.pop(); - } - } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + if close_keystroke_result == CloseKeystrokeResult::Close { + self.stop_recording(&StopRecording, window, cx); + return; + } + let key_len = self.keystrokes.len(); + if let Some(last) = self.keystrokes.last_mut() + && last.key.is_empty() + && key_len <= Self::KEYSTROKE_COUNT_MAX + { + if self.search { + last.key = keystroke.key.clone(); if close_keystroke_result == CloseKeystrokeResult::Partial && self.close_keystrokes_start.is_none() { - self.close_keystrokes_start = Some(self.keystrokes.len()); + self.upsert_close_keystrokes_start(self.keystrokes.len() - 1, cx); } - self.keystrokes.push(keystroke.clone()); if self.search { self.previous_modifiers = keystroke.modifiers; } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX @@ -285,9 +311,29 @@ impl KeystrokeInput { { self.keystrokes.push(Self::dummy(keystroke.modifiers)); } - } else if close_keystroke_result != CloseKeystrokeResult::Partial { - self.clear_keystrokes(&ClearKeystrokes, window, cx); + self.keystrokes_changed(cx); + cx.stop_propagation(); + return; + } else { + self.keystrokes.pop(); + } + } + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.upsert_close_keystrokes_start(self.keystrokes.len(), cx); } + self.keystrokes.push(keystroke.clone()); + if self.search { + self.previous_modifiers = keystroke.modifiers; + } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX + && keystroke.modifiers.modified() + { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); + } + } else if close_keystroke_result != CloseKeystrokeResult::Partial { + self.clear_keystrokes(&ClearKeystrokes, window, cx); } self.keystrokes_changed(cx); cx.stop_propagation(); @@ -365,8 +411,9 @@ impl KeystrokeInput { && close_keystrokes_start < self.keystrokes.len() { self.keystrokes.drain(close_keystrokes_start..); + self.keystrokes_changed(cx); } - self.close_keystrokes.take(); + self.end_close_keystrokes_capture(); #[cfg(test)] { self.recording = false; @@ -645,6 +692,7 @@ mod tests { /// Sends a keystroke event based on string description /// Examples: "a", "ctrl-a", "cmd-shift-z", "escape" + #[track_caller] pub fn send_keystroke(&mut self, keystroke_input: &str) -> &mut Self { self.expect_is_recording(true); let keystroke_str = if keystroke_input.ends_with('-') { @@ -677,6 +725,7 @@ mod tests { /// Sends a modifier change event based on string description /// Examples: "+ctrl", "-ctrl", "+cmd+shift", "-all" + #[track_caller] pub fn send_modifiers(&mut self, modifiers: &str) -> &mut Self { self.expect_is_recording(true); let new_modifiers = if modifiers == "-all" { @@ -700,6 +749,7 @@ mod tests { /// Sends multiple events in sequence /// Each event string is either a keystroke or modifier change + #[track_caller] pub fn send_events(&mut self, events: &[&str]) -> &mut Self { self.expect_is_recording(true); for event in events { @@ -712,9 +762,8 @@ mod tests { self } - /// Verifies that the keystrokes match the expected strings #[track_caller] - pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + fn expect_keystrokes_equal(actual: &[Keystroke], expected: &[&str]) { let expected_keystrokes: Result, _> = expected .iter() .map(|s| { @@ -738,9 +787,6 @@ mod tests { let expected_keystrokes = expected_keystrokes .unwrap_or_else(|e: anyhow::Error| panic!("Invalid expected keystroke: {}", e)); - let actual = self - .input - .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); assert_eq!( actual.len(), expected_keystrokes.len(), @@ -763,6 +809,25 @@ mod tests { actual.unparse() ); } + } + + /// Verifies that the keystrokes match the expected strings + #[track_caller] + pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + let actual = self + .input + .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); + Self::expect_keystrokes_equal(&actual, expected); + self + } + + #[track_caller] + pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + let actual = self + .input + .read_with(&mut self.cx, |input, _| input.close_keystrokes.clone()) + .unwrap_or_default(); + Self::expect_keystrokes_equal(&actual, expected); self } @@ -813,6 +878,18 @@ mod tests { self } + pub async fn wait_for_close_keystroke_capture_end(&mut self) -> &mut Self { + let task = self.input.update_in(&mut self.cx, |input, _, _| { + input.clear_close_keystrokes_timer.take() + }); + let task = task.expect("No close keystroke capture end timer task"); + self.cx + .executor() + .advance_clock(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT); + task.await; + self + } + /// Parses modifier change strings like "+ctrl", "-shift", "+cmd+alt" fn parse_modifier_change(&self, modifiers_str: &str) -> Modifiers { let mut modifiers = self.current_modifiers; @@ -1162,4 +1239,19 @@ mod tests { .send_events(&["escape", "escape", "escape"]) // Pure triple escape sequence .expect_empty(); } + + #[gpui::test] + async fn test_end_close_keystroke_capture(cx: &mut TestAppContext) { + init_test(cx) + .await + .send_events(&["+ctrl", "g", "-ctrl", "escape"]) + .expect_keystrokes(&["ctrl-g", "escape"]) + .wait_for_close_keystroke_capture_end() + .await + .send_events(&["escape", "escape"]) + .expect_keystrokes(&["ctrl-g", "escape", "escape"]) + .expect_close_keystrokes(&["escape", "escape"]) + .send_keystroke("escape") + .expect_keystrokes(&["ctrl-g", "escape"]); + } } From c110f7801516a1948ade4a51213f1fc8ea7f8efc Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Tue, 29 Jul 2025 23:26:30 +0200 Subject: [PATCH 23/35] gpui: Implement support for wlr layer shell (#32651) I was interested in potentially using gpui for a hobby project, but needed [layer shell](https://wayland.app/protocols/wlr-layer-shell-unstable-v1) support for it. Turns out gpui's (excellent!) architecture made that super easy to implement, so I went ahead and did it. Layer shell is a window role used for notification windows, lock screens, docks, backgrounds, etc. Supporting it in gpui opens the door to implementing applications like that using the framework. If this turns out interesting enough to merge - I'm also happy to provide a follow-up PR (when I have the time to) to implement some of the desirable window options for layer shell surfaces, such as: - namespace (currently always `""`) - keyboard interactivity (currently always `OnDemand`, which mimics normal keyboard interactivity) - anchor, exclusive zone, margins - popups Release Notes: - Added support for wayland layer shell surfaces in gpui --------- Co-authored-by: Mikayla Maki --- Cargo.lock | 38 ++- crates/gpui/Cargo.toml | 4 + crates/gpui/src/platform.rs | 4 + .../gpui/src/platform/linux/wayland/client.rs | 29 ++ .../gpui/src/platform/linux/wayland/window.rs | 295 +++++++++++++----- crates/gpui/src/platform/mac/window.rs | 4 +- 6 files changed, 296 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e35202e900d216c9f0a088b3e86c4e03be05223..7f09342879281a79d37b2c50adc23affef8024ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7354,8 +7354,9 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-cursor", - "wayland-protocols", + "wayland-protocols 0.31.2", "wayland-protocols-plasma", + "wayland-protocols-wlr", "windows 0.61.1", "windows-core 0.61.0", "windows-numerics", @@ -18369,9 +18370,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" dependencies = [ "cc", "downcast-rs", @@ -18383,9 +18384,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.8" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" dependencies = [ "bitflags 2.9.0", "rustix 0.38.44", @@ -18416,6 +18417,18 @@ dependencies = [ "wayland-scanner", ] +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + [[package]] name = "wayland-protocols-plasma" version = "0.2.0" @@ -18425,7 +18438,20 @@ dependencies = [ "bitflags 2.9.0", "wayland-backend", "wayland-client", - "wayland-protocols", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.9.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.8", "wayland-scanner", ] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 680111a6ce7860a1b29ec36f9429c42d83972363..4023ddf2dced9191b76432daf7a5ba948cc06038 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -47,6 +47,7 @@ wayland = [ "wayland-cursor", "wayland-protocols", "wayland-protocols-plasma", + "wayland-protocols-wlr", "filedescriptor", "xkbcommon", "open", @@ -193,6 +194,9 @@ wayland-protocols = { version = "0.31.2", features = [ wayland-protocols-plasma = { version = "0.2.0", features = [ "client", ], optional = true } +wayland-protocols-wlr = { version = "0.3.8", features = [ + "client" +], optional = true} # X11 as-raw-xcb-connection = { version = "1", optional = true } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1e72d2386807b83b2f71e5d89309f8e75eb8132b..febf294e485111682e1513655f7e1fe64fee7ab5 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1216,6 +1216,10 @@ pub enum WindowKind { /// A window that appears above all other windows, usually used for alerts or popups /// use sparingly! PopUp, + /// An overlay such as a notification window, a launcher, ... + /// + /// Only supported on wayland + Overlay, } /// The appearance of the window, as defined by the operating system. diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 72e4477ecf697a9f6443dffb80e0637202d3b848..33b22e7ce5cb67150deed7cd9a1a881a6385a044 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -61,6 +61,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{ }; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager}; +use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1}; use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1; use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode}; @@ -114,6 +115,7 @@ pub struct Globals { pub fractional_scale_manager: Option, pub decoration_manager: Option, + pub layer_shell: Option, pub blur_manager: Option, pub text_input_manager: Option, pub executor: ForegroundExecutor, @@ -151,6 +153,7 @@ impl Globals { viewporter: globals.bind(&qh, 1..=1, ()).ok(), fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(), decoration_manager: globals.bind(&qh, 1..=1, ()).ok(), + layer_shell: globals.bind(&qh, 1..=1, ()).ok(), blur_manager: globals.bind(&qh, 1..=1, ()).ok(), text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), executor, @@ -929,6 +932,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer); delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion); delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1); delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1); +delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager); delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3); delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur); @@ -1074,6 +1078,31 @@ impl Dispatch for WaylandClientStatePtr { } } +impl Dispatch for WaylandClientStatePtr { + fn event( + this: &mut Self, + _: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + event: ::Event, + surface_id: &ObjectId, + _: &Connection, + _: &QueueHandle, + ) { + let client = this.get_client(); + let mut state = client.borrow_mut(); + let Some(window) = get_window(&mut state, surface_id) else { + return; + }; + drop(state); + + let should_close = window.handle_layersurface_event(event); + + if should_close { + // The close logic will be handled in drop_window() + window.close(); + } + } +} + impl Dispatch for WaylandClientStatePtr { fn event( _: &mut Self, diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 2b2207e22c86fc25e6387581bb92b9c304f4bc9d..33c908d1b2618c3685beae4f0e99161f9a13c1d4 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -1,3 +1,6 @@ +use blade_graphics as gpu; +use collections::HashMap; +use futures::channel::oneshot::Receiver; use std::{ cell::{Ref, RefCell, RefMut}, ffi::c_void, @@ -6,9 +9,14 @@ use std::{ sync::Arc, }; -use blade_graphics as gpu; -use collections::HashMap; -use futures::channel::oneshot::Receiver; +use crate::{ + Capslock, + platform::{ + PlatformAtlas, PlatformInputHandler, PlatformWindow, + blade::{BladeContext, BladeRenderer, BladeSurfaceConfig}, + linux::wayland::{display::WaylandDisplay, serial::SerialKind}, + }, +}; use raw_window_handle as rwh; use wayland_backend::client::ObjectId; @@ -20,6 +28,8 @@ use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1 use wayland_protocols::xdg::shell::client::xdg_surface; use wayland_protocols::xdg::shell::client::xdg_toplevel::{self}; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur; +use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::Layer; +use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1; use crate::scene::Scene; use crate::{ @@ -27,15 +37,7 @@ use crate::{ PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions, ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations, - WindowParams, px, size, -}; -use crate::{ - Capslock, - platform::{ - PlatformAtlas, PlatformInputHandler, PlatformWindow, - blade::{BladeContext, BladeRenderer, BladeSurfaceConfig}, - linux::wayland::{display::WaylandDisplay, serial::SerialKind}, - }, + WindowKind, WindowParams, px, size, }; #[derive(Default)] @@ -81,14 +83,12 @@ struct InProgressConfigure { } pub struct WaylandWindowState { - xdg_surface: xdg_surface::XdgSurface, + surface_state: WaylandSurfaceState, acknowledged_first_configure: bool, pub surface: wl_surface::WlSurface, - decoration: Option, app_id: Option, appearance: WindowAppearance, blur: Option, - toplevel: xdg_toplevel::XdgToplevel, viewport: Option, outputs: HashMap, display: Option<(ObjectId, Output)>, @@ -114,6 +114,78 @@ pub struct WaylandWindowState { client_inset: Option, } +pub enum WaylandSurfaceState { + Xdg(WaylandXdgSurfaceState), + LayerShell(WaylandLayerSurfaceState), +} + +pub struct WaylandXdgSurfaceState { + xdg_surface: xdg_surface::XdgSurface, + toplevel: xdg_toplevel::XdgToplevel, + decoration: Option, +} + +pub struct WaylandLayerSurfaceState { + layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, +} + +impl WaylandSurfaceState { + fn ack_configure(&self, serial: u32) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + xdg_surface.ack_configure(serial); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + layer_surface.ack_configure(serial); + } + } + } + + fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> { + if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self { + decoration.as_ref() + } else { + None + } + } + + fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> { + if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self { + Some(toplevel) + } else { + None + } + } + + fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => { + xdg_surface.set_window_geometry(x, y, width, height); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => { + // cannot set window position of a layer surface + layer_surface.set_size(width as u32, height as u32); + } + } + } + + fn destroy(&mut self) { + match self { + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { + xdg_surface, + toplevel, + decoration: _decoration, + }) => { + toplevel.destroy(); + xdg_surface.destroy(); + } + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => { + layer_surface.destroy(); + } + } + } +} + #[derive(Clone)] pub struct WaylandWindowStatePtr { state: Rc>, @@ -124,9 +196,7 @@ impl WaylandWindowState { pub(crate) fn new( handle: AnyWindowHandle, surface: wl_surface::WlSurface, - xdg_surface: xdg_surface::XdgSurface, - toplevel: xdg_toplevel::XdgToplevel, - decoration: Option, + surface_state: WaylandSurfaceState, appearance: WindowAppearance, viewport: Option, client: WaylandClientStatePtr, @@ -156,13 +226,11 @@ impl WaylandWindowState { }; Ok(Self { - xdg_surface, + surface_state, acknowledged_first_configure: false, surface, - decoration, app_id: None, blur: None, - toplevel, viewport, globals, outputs: HashMap::default(), @@ -235,17 +303,16 @@ impl Drop for WaylandWindow { let client = state.client.clone(); state.renderer.destroy(); - if let Some(decoration) = &state.decoration { + if let Some(decoration) = &state.surface_state.decoration() { decoration.destroy(); } if let Some(blur) = &state.blur { blur.release(); } - state.toplevel.destroy(); + state.surface_state.destroy(); if let Some(viewport) = &state.viewport { viewport.destroy(); } - state.xdg_surface.destroy(); state.surface.destroy(); let state_ptr = self.0.clone(); @@ -279,27 +346,65 @@ impl WaylandWindow { appearance: WindowAppearance, ) -> anyhow::Result<(Self, ObjectId)> { let surface = globals.compositor.create_surface(&globals.qh, ()); - let xdg_surface = globals - .wm_base - .get_xdg_surface(&surface, &globals.qh, surface.id()); - let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); - if let Some(size) = params.window_min_size { - toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32); - } + let surface_state = match (params.kind, globals.layer_shell.as_ref()) { + // Matching on layer_shell here means that if kind is Overlay, but the compositor doesn't support layer_shell, + // we end up defaulting to xdg_surface anyway + (WindowKind::Overlay, Some(layer_shell)) => { + let layer_surface = layer_shell.get_layer_surface( + &surface, + None, + Layer::Overlay, + "".to_string(), + &globals.qh, + surface.id(), + ); + + let width = params.bounds.size.width.0; + let height = params.bounds.size.height.0; + layer_surface.set_size(width as u32, height as u32); + layer_surface.set_keyboard_interactivity( + zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand, + ); + + WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) + } + _ => { + let xdg_surface = + globals + .wm_base + .get_xdg_surface(&surface, &globals.qh, surface.id()); + + let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id()); + + if let Some(size) = params.window_min_size { + toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32); + } + + // Attempt to set up window decorations based on the requested configuration + let decoration = globals + .decoration_manager + .as_ref() + .map(|decoration_manager| { + decoration_manager.get_toplevel_decoration( + &toplevel, + &globals.qh, + surface.id(), + ) + }); + + WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { + xdg_surface, + toplevel, + decoration, + }) + } + }; if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() { fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id()); } - // Attempt to set up window decorations based on the requested configuration - let decoration = globals - .decoration_manager - .as_ref() - .map(|decoration_manager| { - decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id()) - }); - let viewport = globals .viewporter .as_ref() @@ -309,9 +414,7 @@ impl WaylandWindow { state: Rc::new(RefCell::new(WaylandWindowState::new( handle, surface.clone(), - xdg_surface, - toplevel, - decoration, + surface_state, appearance, viewport, client, @@ -403,7 +506,7 @@ impl WaylandWindowStatePtr { } } let mut state = self.state.borrow_mut(); - state.xdg_surface.ack_configure(serial); + state.surface_state.ack_configure(serial); let window_geometry = inset_by_tiling( state.bounds.map_origin(|_| px(0.0)), @@ -413,7 +516,7 @@ impl WaylandWindowStatePtr { .map(|v| v.0 as i32) .map_size(|v| if v <= 0 { 1 } else { v }); - state.xdg_surface.set_window_geometry( + state.surface_state.set_geometry( window_geometry.origin.x, window_geometry.origin.y, window_geometry.size.width, @@ -578,6 +681,42 @@ impl WaylandWindowStatePtr { } } + pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool { + match event { + zwlr_layer_surface_v1::Event::Configure { + width, + height, + serial, + } => { + let mut size = if width == 0 || height == 0 { + None + } else { + Some(size(px(width as f32), px(height as f32))) + }; + + let mut state = self.state.borrow_mut(); + state.in_progress_configure = Some(InProgressConfigure { + size, + fullscreen: false, + maximized: false, + resizing: false, + tiling: Tiling::default(), + }); + drop(state); + + // just do the same thing we'd do as an xdg_surface + self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial }); + + false + } + zwlr_layer_surface_v1::Event::Closed => { + // unlike xdg, we don't have a choice here: the surface is closing. + true + } + _ => false, + } + } + #[allow(clippy::mutable_key_type)] pub fn handle_surface_event( &self, @@ -840,7 +979,7 @@ impl PlatformWindow for WaylandWindow { let state_ptr = self.0.clone(); let dp_size = size.to_device_pixels(self.scale_factor()); - state.xdg_surface.set_window_geometry( + state.surface_state.set_geometry( state.bounds.origin.x.0 as i32, state.bounds.origin.y.0 as i32, dp_size.width.0, @@ -934,12 +1073,16 @@ impl PlatformWindow for WaylandWindow { } fn set_title(&mut self, title: &str) { - self.borrow().toplevel.set_title(title.to_string()); + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_title(title.to_string()); + } } fn set_app_id(&mut self, app_id: &str) { let mut state = self.borrow_mut(); - state.toplevel.set_app_id(app_id.to_owned()); + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_app_id(app_id.to_owned()); + } state.app_id = Some(app_id.to_owned()); } @@ -950,24 +1093,30 @@ impl PlatformWindow for WaylandWindow { } fn minimize(&self) { - self.borrow().toplevel.set_minimized(); + if let Some(toplevel) = self.borrow().surface_state.toplevel() { + toplevel.set_minimized(); + } } fn zoom(&self) { let state = self.borrow(); - if !state.maximized { - state.toplevel.set_maximized(); - } else { - state.toplevel.unset_maximized(); + if let Some(toplevel) = state.surface_state.toplevel() { + if !state.maximized { + toplevel.set_maximized(); + } else { + toplevel.unset_maximized(); + } } } fn toggle_fullscreen(&self) { - let mut state = self.borrow_mut(); - if !state.fullscreen { - state.toplevel.set_fullscreen(None); - } else { - state.toplevel.unset_fullscreen(); + let mut state = self.borrow(); + if let Some(toplevel) = state.surface_state.toplevel() { + if !state.fullscreen { + toplevel.set_fullscreen(None); + } else { + toplevel.unset_fullscreen(); + } } } @@ -1032,27 +1181,33 @@ impl PlatformWindow for WaylandWindow { fn show_window_menu(&self, position: Point) { let state = self.borrow(); let serial = state.client.get_serial(SerialKind::MousePress); - state.toplevel.show_window_menu( - &state.globals.seat, - serial, - position.x.0 as i32, - position.y.0 as i32, - ); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.show_window_menu( + &state.globals.seat, + serial, + position.x.0 as i32, + position.y.0 as i32, + ); + } } fn start_window_move(&self) { let state = self.borrow(); let serial = state.client.get_serial(SerialKind::MousePress); - state.toplevel._move(&state.globals.seat, serial); + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel._move(&state.globals.seat, serial); + } } fn start_window_resize(&self, edge: crate::ResizeEdge) { let state = self.borrow(); - state.toplevel.resize( - &state.globals.seat, - state.client.get_serial(SerialKind::MousePress), - edge.to_xdg(), - ) + if let Some(toplevel) = state.surface_state.toplevel() { + toplevel.resize( + &state.globals.seat, + state.client.get_serial(SerialKind::MousePress), + edge.to_xdg(), + ) + } } fn window_decorations(&self) -> Decorations { @@ -1068,7 +1223,7 @@ impl PlatformWindow for WaylandWindow { fn request_decorations(&self, decorations: WindowDecorations) { let mut state = self.borrow_mut(); state.decorations = decorations; - if let Some(decoration) = state.decoration.as_ref() { + if let Some(decoration) = state.surface_state.decoration() { decoration.set_mode(decorations.to_xdg()); update_window(state); } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index aedf131909a6956e9a4501b107c81ce242b80a49..f01d33147b6995e17a136ec456c09b7359973e7f 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -559,7 +559,7 @@ impl MacWindow { } let native_window: id = match kind { - WindowKind::Normal => msg_send![WINDOW_CLASS, alloc], + WindowKind::Normal | WindowKind::Overlay => msg_send![WINDOW_CLASS, alloc], WindowKind::PopUp => { style_mask |= NSWindowStyleMaskNonactivatingPanel; msg_send![PANEL_CLASS, alloc] @@ -711,7 +711,7 @@ impl MacWindow { native_window.makeFirstResponder_(native_view); match kind { - WindowKind::Normal => { + WindowKind::Normal | WindowKind::Overlay => { native_window.setLevel_(NSNormalWindowLevel); native_window.setAcceptsMouseMovedEvents_(YES); } From 3378f02b7ee005d8116bbd17b99cb3196ed09d9e Mon Sep 17 00:00:00 2001 From: marius851000 Date: Tue, 29 Jul 2025 23:45:46 +0200 Subject: [PATCH 24/35] Fix link to panic location on GitHub (#35162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I had a panic, and it reported ``https://github.com/zed-industries/zed/blob/24c2a465bbbbb1be28259abef2f98d52184ff446/src/crates/assistant_tools/src/edit_agent.rs#L686 (may not be uploaded, line may be incorrect if files modified)`` The `/src` part seems superfluous, and result in a link that don’t work (unlike `https://github.com/zed-industries/zed/blob/24c2a465bbbbb1be28259abef2f98d52184ff446/src/crates/assistant_tools/src/edit_agent.rs#L686`). I don’t know why it originally worked (of if it even actually originally worked properly), but there seems to be no reason to keep that `/src`. Release Notes: - N/A --- crates/zed/src/reliability.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index ccbe57e7b3903e9e5b380ad0c0323be65864397d..d7f1473288f734f8be6d12698bbcf9c00bdcedd9 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -63,7 +63,7 @@ pub fn init_panic_hook( location.column(), match app_commit_sha.as_ref() { Some(commit_sha) => format!( - "https://github.com/zed-industries/zed/blob/{}/src/{}#L{} \ + "https://github.com/zed-industries/zed/blob/{}/{}#L{} \ (may not be uploaded, line may be incorrect if files modified)\n", commit_sha.full(), location.file(), From 48e085a5236b993390a31fb4304512d187b1145f Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 29 Jul 2025 17:54:58 -0400 Subject: [PATCH 25/35] onboarding ui: Add editing page to onboarding page (#35298) I added buttons for inlay values, showing the mini map, git blame, and controlling the UI/Editor Font/Font size. The only thing left for this page is some UI clean up and adding buttons for setting import from VSCode/cursor. I also added Numeric Stepper as a component preview. Current state: image Release Notes: - N/A --- Cargo.lock | 3 + crates/editor/src/editor.rs | 2 +- crates/onboarding/Cargo.toml | 5 +- crates/onboarding/src/editing_page.rs | 287 ++++++++++++++++++++ crates/onboarding/src/onboarding.rs | 10 +- crates/ui/src/components/numeric_stepper.rs | 33 ++- 6 files changed, 331 insertions(+), 9 deletions(-) create mode 100644 crates/onboarding/src/editing_page.rs diff --git a/Cargo.lock b/Cargo.lock index 7f09342879281a79d37b2c50adc23affef8024ca..f171901e299192436283ba016023e24a75e19940 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10942,9 +10942,12 @@ dependencies = [ "anyhow", "command_palette_hooks", "db", + "editor", "feature_flags", "fs", "gpui", + "language", + "project", "settings", "theme", "ui", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3c877873a0a1bb9f61391b58584dc5e9261cd5fa..a2f231014455f79ada6b236e64c744a91e12bf30 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use display_map::*; pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; pub use editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, - ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowScrollbar, + ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, }; use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; pub use editor_settings_controls::*; diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 6ec8f8b162c5b8d62817e81893217e6d22a5d7f2..da009b4e4efefcc7c4d47e3544c6f20e8f1d53eb 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -18,12 +18,15 @@ default = [] anyhow.workspace = true command_palette_hooks.workspace = true db.workspace = true +editor.workspace = true feature_flags.workspace = true fs.workspace = true gpui.workspace = true +language.workspace = true +project.workspace = true settings.workspace = true theme.workspace = true ui.workspace = true -workspace.workspace = true workspace-hack.workspace = true +workspace.workspace = true zed_actions.workspace = true diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs new file mode 100644 index 0000000000000000000000000000000000000000..c07d8fef4d1c29034af740eee37cae34d6865228 --- /dev/null +++ b/crates/onboarding/src/editing_page.rs @@ -0,0 +1,287 @@ +use editor::{EditorSettings, ShowMinimap}; +use fs::Fs; +use gpui::{App, IntoElement, Pixels, Window}; +use language::language_settings::AllLanguageSettings; +use project::project_settings::ProjectSettings; +use settings::{Settings as _, update_settings_file}; +use theme::{FontFamilyCache, FontFamilyName, ThemeSettings}; +use ui::{ + ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize, NumericStepper, + ParentElement, SharedString, Styled, SwitchColor, SwitchField, ToggleButtonGroup, + ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px, v_flex, +}; + +fn read_show_mini_map(cx: &App) -> ShowMinimap { + editor::EditorSettings::get_global(cx).minimap.show +} + +fn write_show_mini_map(show: ShowMinimap, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |editor_settings, _| { + editor_settings.minimap.get_or_insert_default().show = Some(show); + }); +} + +fn read_inlay_hints(cx: &App) -> bool { + AllLanguageSettings::get_global(cx) + .defaults + .inlay_hints + .enabled +} + +fn write_inlay_hints(enabled: bool, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |all_language_settings, cx| { + all_language_settings + .defaults + .inlay_hints + .get_or_insert_with(|| { + AllLanguageSettings::get_global(cx) + .clone() + .defaults + .inlay_hints + }) + .enabled = enabled; + }); +} + +fn read_git_blame(cx: &App) -> bool { + ProjectSettings::get_global(cx).git.inline_blame_enabled() +} + +fn set_git_blame(enabled: bool, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |project_settings, _| { + project_settings + .git + .inline_blame + .get_or_insert_default() + .enabled = enabled; + }); +} + +fn write_ui_font_family(font: SharedString, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |theme_settings, _| { + theme_settings.ui_font_family = Some(FontFamilyName(font.into())); + }); +} + +fn write_ui_font_size(size: Pixels, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |theme_settings, _| { + theme_settings.ui_font_size = Some(size.into()); + }); +} + +fn write_buffer_font_size(size: Pixels, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |theme_settings, _| { + theme_settings.buffer_font_size = Some(size.into()); + }); +} + +fn write_buffer_font_family(font_family: SharedString, cx: &mut App) { + let fs = ::global(cx); + + update_settings_file::(fs, cx, move |theme_settings, _| { + theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into())); + }); +} + +pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement { + let theme_settings = ThemeSettings::get_global(cx); + let ui_font_size = theme_settings.ui_font_size(cx); + let font_family = theme_settings.buffer_font.family.clone(); + let buffer_font_size = theme_settings.buffer_font_size(cx); + + v_flex() + .gap_4() + .child(Label::new("Import Settings").size(LabelSize::Large)) + .child( + Label::new("Automatically pull your settings from other editors.") + .size(LabelSize::Small), + ) + .child( + h_flex() + .child(IconButton::new( + "import-vs-code-settings", + ui::IconName::Code, + )) + .child(IconButton::new( + "import-cursor-settings", + ui::IconName::CursorIBeam, + )), + ) + .child(Label::new("Popular Settings").size(LabelSize::Large)) + .child( + h_flex() + .gap_4() + .justify_between() + .child( + v_flex() + .justify_between() + .gap_1() + .child(Label::new("UI Font")) + .child( + h_flex() + .justify_between() + .gap_2() + .child(div().min_w(px(120.)).child(DropdownMenu::new( + "ui-font-family", + theme_settings.ui_font.family.clone(), + ContextMenu::build(window, cx, |mut menu, _, cx| { + let font_family_cache = FontFamilyCache::global(cx); + + for font_name in font_family_cache.list_font_families(cx) { + menu = menu.custom_entry( + { + let font_name = font_name.clone(); + move |_window, _cx| { + Label::new(font_name.clone()) + .into_any_element() + } + }, + { + let font_name = font_name.clone(); + move |_window, cx| { + write_ui_font_family(font_name.clone(), cx); + } + }, + ) + } + + menu + }), + ))) + .child(NumericStepper::new( + "ui-font-size", + ui_font_size.to_string(), + move |_, _, cx| { + write_ui_font_size(ui_font_size - px(1.), cx); + }, + move |_, _, cx| { + write_ui_font_size(ui_font_size + px(1.), cx); + }, + )), + ), + ) + .child( + v_flex() + .justify_between() + .gap_1() + .child(Label::new("Editor Font")) + .child( + h_flex() + .justify_between() + .gap_2() + .child(DropdownMenu::new( + "buffer-font-family", + font_family, + ContextMenu::build(window, cx, |mut menu, _, cx| { + let font_family_cache = FontFamilyCache::global(cx); + + for font_name in font_family_cache.list_font_families(cx) { + menu = menu.custom_entry( + { + let font_name = font_name.clone(); + move |_window, _cx| { + Label::new(font_name.clone()) + .into_any_element() + } + }, + { + let font_name = font_name.clone(); + move |_window, cx| { + write_buffer_font_family( + font_name.clone(), + cx, + ); + } + }, + ) + } + + menu + }), + )) + .child(NumericStepper::new( + "buffer-font-size", + buffer_font_size.to_string(), + move |_, _, cx| { + write_buffer_font_size(buffer_font_size - px(1.), cx); + }, + move |_, _, cx| { + write_buffer_font_size(buffer_font_size + px(1.), cx); + }, + )), + ), + ), + ) + .child( + h_flex() + .justify_between() + .child(Label::new("Mini Map")) + .child( + ToggleButtonGroup::single_row( + "onboarding-show-mini-map", + [ + ToggleButtonSimple::new("Auto", |_, _, cx| { + write_show_mini_map(ShowMinimap::Auto, cx); + }), + ToggleButtonSimple::new("Always", |_, _, cx| { + write_show_mini_map(ShowMinimap::Always, cx); + }), + ToggleButtonSimple::new("Never", |_, _, cx| { + write_show_mini_map(ShowMinimap::Never, cx); + }), + ], + ) + .selected_index(match read_show_mini_map(cx) { + ShowMinimap::Auto => 0, + ShowMinimap::Always => 1, + ShowMinimap::Never => 2, + }) + .style(ToggleButtonGroupStyle::Outlined) + .button_width(ui::rems_from_px(64.)), + ), + ) + .child( + SwitchField::new( + "onboarding-enable-inlay-hints", + "Inlay Hints", + "See parameter names for function and method calls inline.", + if read_inlay_hints(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + write_inlay_hints(toggle_state == &ToggleState::Selected, cx); + }, + ) + .color(SwitchColor::Accent), + ) + .child( + SwitchField::new( + "onboarding-git-blame-switch", + "Git Blame", + "See who committed each line on a given file.", + if read_git_blame(cx) { + ui::ToggleState::Selected + } else { + ui::ToggleState::Unselected + }, + |toggle_state, _, cx| { + set_git_blame(toggle_state == &ToggleState::Selected, cx); + }, + ) + .color(SwitchColor::Accent), + ) +} diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index b675ed2dd77d803d587f9365f8e0f283b7b9fd97..cc0c47ca71a391198ae3711ae4027e7c2a714779 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -21,6 +21,7 @@ use workspace::{ open_new, with_active_or_new_workspace, }; +mod editing_page; mod welcome; pub struct OnBoardingFeatureFlag {} @@ -246,7 +247,9 @@ impl Onboarding { fn render_page(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { match self.selected_page { SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(), - SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(), + SelectedPage::Editing => { + crate::editing_page::render_editing_page(window, cx).into_any_element() + } SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(), } } @@ -281,11 +284,6 @@ impl Onboarding { ) } - fn render_editing_page(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - // div().child("editing page") - "Right" - } - fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().child("ai setup page") } diff --git a/crates/ui/src/components/numeric_stepper.rs b/crates/ui/src/components/numeric_stepper.rs index f9e6e88f01f64fb7c78e98410178b101383f9be4..05d368f42727170257d284b189c44b5c3a6b948d 100644 --- a/crates/ui/src/components/numeric_stepper.rs +++ b/crates/ui/src/components/numeric_stepper.rs @@ -2,7 +2,7 @@ use gpui::ClickEvent; use crate::{IconButtonShape, prelude::*}; -#[derive(IntoElement)] +#[derive(IntoElement, RegisterComponent)] pub struct NumericStepper { id: ElementId, value: SharedString, @@ -93,3 +93,34 @@ impl RenderOnce for NumericStepper { ) } } + +impl Component for NumericStepper { + fn scope() -> ComponentScope { + ComponentScope::Input + } + + fn name() -> &'static str { + "NumericStepper" + } + + fn sort_name() -> &'static str { + Self::name() + } + + fn description() -> Option<&'static str> { + Some("A button used to increment or decrement a numeric value. ") + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + Some( + div() + .child(NumericStepper::new( + "numeric-stepper-component-preview", + "10", + move |_, _, _| {}, + move |_, _, _| {}, + )) + .into_any_element(), + ) + } +} From 9f69b538692156fd04e564d85ef7e2d9984ba403 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 29 Jul 2025 17:04:00 -0500 Subject: [PATCH 26/35] keymap_ui: Additional cleanup (#35299) Closes #ISSUE Additional cleanup and testing for the keystroke input including - Focused testing of the "previous modifiers" logic in search mode - Not merging unmodified keystrokes into previous modifier only bindings (extension of #35208) - Fixing a bug where input would overflow in search mode when entering only modifiers - Additional testing logic to ensure keystrokes updated events are always emitted correctly Release Notes: - N/A *or* Added/Fixed/Improved ... --- .../src/ui_components/keystroke_input.rs | 273 +++++++++++++----- 1 file changed, 202 insertions(+), 71 deletions(-) diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index a34d0a2bbd113e1434f9f4d1d924fd76897e2cf9..03d27d0ab9576df3ef3031d7a25121a5fcf6d905 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -239,46 +239,48 @@ impl KeystrokeInput { fn on_modifiers_changed( &mut self, event: &ModifiersChangedEvent, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) { + cx.stop_propagation(); let keystrokes_len = self.keystrokes.len(); if self.previous_modifiers.modified() && event.modifiers.is_subset_of(&self.previous_modifiers) { self.previous_modifiers &= event.modifiers; - cx.stop_propagation(); return; } + self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() && last.key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { + if !self.search && !event.modifiers.modified() { + self.keystrokes.pop(); + return; + } if self.search { if self.previous_modifiers.modified() { last.modifiers |= event.modifiers; - self.previous_modifiers |= event.modifiers; } else { self.keystrokes.push(Self::dummy(event.modifiers)); - self.previous_modifiers |= event.modifiers; } - } else if !event.modifiers.modified() { - self.keystrokes.pop(); + self.previous_modifiers |= event.modifiers; } else { last.modifiers = event.modifiers; + return; } - - self.keystrokes_changed(cx); } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { self.keystrokes.push(Self::dummy(event.modifiers)); if self.search { self.previous_modifiers |= event.modifiers; } - self.keystrokes_changed(cx); } - cx.stop_propagation(); + if keystrokes_len >= Self::KEYSTROKE_COUNT_MAX { + self.clear_keystrokes(&ClearKeystrokes, window, cx); + } } fn handle_keystroke( @@ -287,56 +289,47 @@ impl KeystrokeInput { window: &mut Window, cx: &mut Context, ) { + cx.stop_propagation(); + let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); if close_keystroke_result == CloseKeystrokeResult::Close { self.stop_recording(&StopRecording, window, cx); return; } - let key_len = self.keystrokes.len(); - if let Some(last) = self.keystrokes.last_mut() + + let mut keystroke = keystroke.clone(); + if let Some(last) = self.keystrokes.last() && last.key.is_empty() - && key_len <= Self::KEYSTROKE_COUNT_MAX + && (!self.search || self.previous_modifiers.modified()) { - if self.search { - last.key = keystroke.key.clone(); - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() - { - self.upsert_close_keystrokes_start(self.keystrokes.len() - 1, cx); - } - if self.search { - self.previous_modifiers = keystroke.modifiers; - } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.modifiers.modified() - { - self.keystrokes.push(Self::dummy(keystroke.modifiers)); - } - self.keystrokes_changed(cx); - cx.stop_propagation(); + let key = keystroke.key.clone(); + keystroke = last.clone(); + keystroke.key = key; + self.keystrokes.pop(); + } + + if close_keystroke_result == CloseKeystrokeResult::Partial { + self.upsert_close_keystrokes_start(self.keystrokes.len(), cx); + if self.keystrokes.len() >= Self::KEYSTROKE_COUNT_MAX { return; - } else { - self.keystrokes.pop(); } } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() - { - self.upsert_close_keystrokes_start(self.keystrokes.len(), cx); - } - self.keystrokes.push(keystroke.clone()); - if self.search { - self.previous_modifiers = keystroke.modifiers; - } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.modifiers.modified() - { - self.keystrokes.push(Self::dummy(keystroke.modifiers)); - } - } else if close_keystroke_result != CloseKeystrokeResult::Partial { + + if self.keystrokes.len() >= Self::KEYSTROKE_COUNT_MAX { self.clear_keystrokes(&ClearKeystrokes, window, cx); + return; } + + self.keystrokes.push(keystroke.clone()); self.keystrokes_changed(cx); - cx.stop_propagation(); + + if self.search { + self.previous_modifiers = keystroke.modifiers; + return; + } + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); + } } fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context) { @@ -429,6 +422,7 @@ impl KeystrokeInput { ) { self.keystrokes.clear(); self.keystrokes_changed(cx); + self.end_close_keystrokes_capture(); } fn is_recording(&self, window: &Window) -> bool { @@ -657,6 +651,7 @@ mod tests { use super::*; use fs::FakeFs; use gpui::{Entity, TestAppContext, VisualTestContext}; + use itertools::Itertools as _; use project::Project; use settings::SettingsStore; use workspace::Workspace; @@ -712,7 +707,7 @@ mod tests { // Combine current modifiers with keystroke modifiers keystroke.modifiers |= self.current_modifiers; - self.input.update_in(&mut self.cx, |input, window, cx| { + self.update_input(|input, window, cx| { input.handle_keystroke(&keystroke, window, cx); }); @@ -739,7 +734,7 @@ mod tests { capslock: gpui::Capslock::default(), }; - self.input.update_in(&mut self.cx, |input, window, cx| { + self.update_input(|input, window, cx| { input.on_modifiers_changed(&event, window, cx); }); @@ -857,10 +852,14 @@ mod tests { } /// Clears all keystrokes + #[track_caller] pub fn clear_keystrokes(&mut self) -> &mut Self { + let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx); self.input.update_in(&mut self.cx, |input, window, cx| { input.clear_keystrokes(&ClearKeystrokes, window, cx); }); + KeystrokeUpdateTracker::finish(change_tracker, &self.cx); + self.current_modifiers = Default::default(); self } @@ -891,37 +890,103 @@ mod tests { } /// Parses modifier change strings like "+ctrl", "-shift", "+cmd+alt" + #[track_caller] fn parse_modifier_change(&self, modifiers_str: &str) -> Modifiers { let mut modifiers = self.current_modifiers; + assert!(!modifiers_str.is_empty(), "Empty modifier string"); + + let value; + let split_char; + let remaining; if let Some(to_add) = modifiers_str.strip_prefix('+') { - // Add modifiers - for modifier in to_add.split('+') { - match modifier { - "ctrl" | "control" => modifiers.control = true, - "alt" | "option" => modifiers.alt = true, - "shift" => modifiers.shift = true, - "cmd" | "command" => modifiers.platform = true, - "fn" | "function" => modifiers.function = true, - _ => panic!("Unknown modifier: {}", modifier), - } - } - } else if let Some(to_remove) = modifiers_str.strip_prefix('-') { - // Remove modifiers - for modifier in to_remove.split('+') { - match modifier { - "ctrl" | "control" => modifiers.control = false, - "alt" | "option" => modifiers.alt = false, - "shift" => modifiers.shift = false, - "cmd" | "command" => modifiers.platform = false, - "fn" | "function" => modifiers.function = false, - _ => panic!("Unknown modifier: {}", modifier), - } + value = true; + split_char = '+'; + remaining = to_add; + } else { + let to_remove = modifiers_str + .strip_prefix('-') + .expect("Modifier string must start with '+' or '-'"); + value = false; + split_char = '-'; + remaining = to_remove; + } + + for modifier in remaining.split(split_char) { + match modifier { + "ctrl" | "control" => modifiers.control = value, + "alt" | "option" => modifiers.alt = value, + "shift" => modifiers.shift = value, + "cmd" | "command" | "platform" => modifiers.platform = value, + "fn" | "function" => modifiers.function = value, + _ => panic!("Unknown modifier: {}", modifier), } } modifiers } + + #[track_caller] + fn update_input( + &mut self, + cb: impl FnOnce(&mut KeystrokeInput, &mut Window, &mut Context) -> R, + ) -> R { + let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx); + let result = self.input.update_in(&mut self.cx, cb); + KeystrokeUpdateTracker::finish(change_tracker, &self.cx); + return result; + } + } + + struct KeystrokeUpdateTracker { + initial_keystrokes: Vec, + _subscription: Subscription, + input: Entity, + received_keystrokes_updated: bool, + } + + impl KeystrokeUpdateTracker { + fn new(input: Entity, cx: &mut VisualTestContext) -> Entity { + cx.new(|cx| Self { + initial_keystrokes: input.read_with(cx, |input, _| input.keystrokes.clone()), + _subscription: cx.subscribe(&input, |this: &mut Self, _, _, _| { + this.received_keystrokes_updated = true; + }), + input, + received_keystrokes_updated: false, + }) + } + #[track_caller] + fn finish(this: Entity, cx: &VisualTestContext) { + let (received_keystrokes_updated, initial_keystrokes_str, updated_keystrokes_str) = + this.read_with(cx, |this, cx| { + let updated_keystrokes = this + .input + .read_with(cx, |input, _| input.keystrokes.clone()); + let initial_keystrokes_str = keystrokes_str(&this.initial_keystrokes); + let updated_keystrokes_str = keystrokes_str(&updated_keystrokes); + ( + this.received_keystrokes_updated, + initial_keystrokes_str, + updated_keystrokes_str, + ) + }); + if received_keystrokes_updated { + assert_ne!( + initial_keystrokes_str, updated_keystrokes_str, + "Received keystrokes_updated event, expected different keystrokes" + ); + } else { + assert_eq!( + initial_keystrokes_str, updated_keystrokes_str, + "Received no keystrokes_updated event, expected same keystrokes" + ); + } + + fn keystrokes_str(ks: &[Keystroke]) -> String { + ks.iter().map(|ks| ks.unparse()).join(" ") + } + } } async fn init_test(cx: &mut TestAppContext) -> KeystrokeInputTestHelper { @@ -1254,4 +1319,70 @@ mod tests { .send_keystroke("escape") .expect_keystrokes(&["ctrl-g", "escape"]); } + + #[gpui::test] + async fn test_search_previous_modifiers_are_sticky(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl+alt", "-ctrl", "j"]) + .expect_keystrokes(&["ctrl-alt-j"]); + } + + #[gpui::test] + async fn test_previous_modifiers_can_be_entered_separately(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "-ctrl"]) + .expect_keystrokes(&["ctrl-"]) + .send_events(&["+alt", "-alt"]) + .expect_keystrokes(&["ctrl-", "alt-"]); + } + + #[gpui::test] + async fn test_previous_modifiers_reset_on_key(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl+alt", "-ctrl", "+shift"]) + .expect_keystrokes(&["ctrl-shift-alt-"]) + .send_keystroke("j") + .expect_keystrokes(&["ctrl-shift-alt-j"]) + .send_keystroke("i") + .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i"]) + .send_events(&["-shift-alt", "+cmd"]) + .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i", "cmd-"]); + } + + #[gpui::test] + async fn test_previous_modifiers_reset_on_release_all(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl+alt", "-ctrl", "+shift"]) + .expect_keystrokes(&["ctrl-shift-alt-"]) + .send_events(&["-all", "j"]) + .expect_keystrokes(&["ctrl-shift-alt-", "j"]); + } + + #[gpui::test] + async fn test_search_repeat_modifiers(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(true) + .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"]) + .expect_keystrokes(&["ctrl-", "alt-", "shift-"]) + .send_events(&["+cmd"]) + .expect_empty(); + } + + #[gpui::test] + async fn test_not_search_repeat_modifiers(cx: &mut TestAppContext) { + init_test(cx) + .await + .with_search_mode(false) + .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"]) + .expect_empty(); + } } From d2d116cb02b42d0f20bbde15dd5412dca602fcc6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 18:34:04 -0400 Subject: [PATCH 27/35] collab: Remove `GET /user` endpoint (#35301) This PR removes the `GET /user` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api.rs | 43 --------------------------- crates/collab/src/api/contributors.rs | 12 ++++++-- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 5cb26eb50703c8ca7bcf7b493709ac4f80a3a6df..050a15dd4e549426fbc9dd76a7688df1e5b476c9 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -100,7 +100,6 @@ impl std::fmt::Display for SystemIdHeader { pub fn routes(rpc_server: Arc) -> Router<(), Body> { Router::new() - .route("/user", get(update_or_create_authenticated_user)) .route("/users/look_up", get(look_up_user)) .route("/users/:id/access_tokens", post(create_access_token)) .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens)) @@ -145,48 +144,6 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } -#[derive(Debug, Deserialize)] -struct AuthenticatedUserParams { - github_user_id: i32, - github_login: String, - github_email: Option, - github_name: Option, - github_user_created_at: chrono::DateTime, -} - -#[derive(Debug, Serialize)] -struct AuthenticatedUserResponse { - user: User, - metrics_id: String, - feature_flags: Vec, -} - -async fn update_or_create_authenticated_user( - Query(params): Query, - Extension(app): Extension>, -) -> Result> { - let initial_channel_id = app.config.auto_join_channel_id; - - let user = app - .db - .update_or_create_user_by_github_account( - ¶ms.github_login, - params.github_user_id, - params.github_email.as_deref(), - params.github_name.as_deref(), - params.github_user_created_at, - initial_channel_id, - ) - .await?; - let metrics_id = app.db.get_user_metrics_id(user.id).await?; - let feature_flags = app.db.get_user_flags(user.id).await?; - Ok(Json(AuthenticatedUserResponse { - user, - metrics_id, - feature_flags, - })) -} - #[derive(Debug, Deserialize)] struct LookUpUserParams { identifier: String, diff --git a/crates/collab/src/api/contributors.rs b/crates/collab/src/api/contributors.rs index 9296c1d4282078d73dccfe40536fc59102ec248d..8cfef0ad7e717614e23c3cf9d04852c976f1f55f 100644 --- a/crates/collab/src/api/contributors.rs +++ b/crates/collab/src/api/contributors.rs @@ -8,7 +8,6 @@ use axum::{ use chrono::{NaiveDateTime, SecondsFormat}; use serde::{Deserialize, Serialize}; -use crate::api::AuthenticatedUserParams; use crate::db::ContributorSelector; use crate::{AppState, Result}; @@ -104,9 +103,18 @@ impl RenovateBot { } } +#[derive(Debug, Deserialize)] +struct AddContributorBody { + github_user_id: i32, + github_login: String, + github_email: Option, + github_name: Option, + github_user_created_at: chrono::DateTime, +} + async fn add_contributor( Extension(app): Extension>, - extract::Json(params): extract::Json, + extract::Json(params): extract::Json, ) -> Result<()> { let initial_channel_id = app.config.auto_join_channel_id; app.db From f0927faf6151211ecb5b14faf7c9c3fdca66abb8 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 18:45:00 -0400 Subject: [PATCH 28/35] collab: Add kill switches for syncing data to and from Stripe (#35304) This PR adds two kill switches for syncing data to and from Stripe using Collab. The `cloud-stripe-events-polling` and `cloud-stripe-usage-meters-sync` feature flags control whether we use Cloud for polling Stripe events and updating Stripe meters, respectively. When we're ready to hand off the syncing to Cloud we can enable the feature flag to do so. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 1cb20173c12759f4c07d976c16f108a81fd3b3e5..9d0c617ab9268ad5c8e86899a81fb35d50f90457 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -87,6 +87,14 @@ async fn poll_stripe_events( stripe_client: &Arc, real_stripe_client: &stripe::Client, ) -> anyhow::Result<()> { + let feature_flags = app.db.list_feature_flags().await?; + let sync_events_using_cloud = feature_flags + .iter() + .any(|flag| flag.flag == "cloud-stripe-events-polling" && flag.enabled_for_all); + if sync_events_using_cloud { + return Ok(()); + } + fn event_type_to_string(event_type: EventType) -> String { // Calling `to_string` on `stripe::EventType` members gives us a quoted string, // so we need to unquote it. @@ -569,6 +577,14 @@ async fn sync_model_request_usage_with_stripe( llm_db: &Arc, stripe_billing: &Arc, ) -> anyhow::Result<()> { + let feature_flags = app.db.list_feature_flags().await?; + let sync_model_request_usage_using_cloud = feature_flags + .iter() + .any(|flag| flag.flag == "cloud-stripe-usage-meters-sync" && flag.enabled_for_all); + if sync_model_request_usage_using_cloud { + return Ok(()); + } + log::info!("Stripe usage sync: Starting"); let started_at = Utc::now(); From 0be83f1c671097360f3b1a93a9de8288b6be31b2 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 30 Jul 2025 00:46:17 +0200 Subject: [PATCH 29/35] emmet: Bump to 0.0.4 (#35305) This PR bumps the emmet extension to version 0.0.4. Includes: - https://github.com/zed-industries/zed/pull/33865 - https://github.com/zed-industries/zed/pull/32208 - https://github.com/zed-industries/zed/pull/15177 Note that this intentionally does NOT include a change in the `extension.toml`: The version was bumped incorrectly once in https://github.com/zed-industries/zed/pull/32208 in both the `extension.toml` as well as the `Cargo.lock` but not in the `Cargo.toml`. After that, https://github.com/zed-industries/zed/pull/33667 only removed the changes in the `Cargo.lock` but didn't revert the change in the `extension.toml` file. Hence, the version in the `extension.toml` is already at `0.0.4` Release Notes: - N/A --- Cargo.lock | 2 +- extensions/emmet/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f171901e299192436283ba016023e24a75e19940..1cd9e521f1afebbaae765f0a13f67864865a5e7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20367,7 +20367,7 @@ dependencies = [ [[package]] name = "zed_emmet" -version = "0.0.3" +version = "0.0.4" dependencies = [ "zed_extension_api 0.1.0", ] diff --git a/extensions/emmet/Cargo.toml b/extensions/emmet/Cargo.toml index db8aaaae41ea353fc8623be485e6e48ecdfcfab1..9d72a6c5c4df38cfe1e9203641a3a2273a40084a 100644 --- a/extensions/emmet/Cargo.toml +++ b/extensions/emmet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zed_emmet" -version = "0.0.3" +version = "0.0.4" edition.workspace = true publish.workspace = true license = "Apache-2.0" From 57766199cfcf9fba1f920db9958ff45ac4fff1dd Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 30 Jul 2025 00:47:04 +0200 Subject: [PATCH 30/35] ui: Clean up toggle button group component (#35303) This change cleans up the toggle button component a bit by utilizing const parameters instead and also removes some clones by consuming the values where possible instead. Release Notes: - N/A --- .../ui/src/components/button/toggle_button.rs | 236 +++++++----------- 1 file changed, 93 insertions(+), 143 deletions(-) diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs index c6cf7ac62cd43b8e3e8611d81175a9397e0ad88f..30683e60f3c4a5a4d2aae2c734c5ab96c55ac641 100644 --- a/crates/ui/src/components/button/toggle_button.rs +++ b/crates/ui/src/components/button/toggle_button.rs @@ -291,14 +291,18 @@ impl Component for ToggleButton { } } +pub struct ButtonConfiguration { + label: SharedString, + icon: Option, + on_click: Box, +} + mod private { - pub trait Sealed {} + pub trait ToggleButtonStyle {} } -pub trait ButtonBuilder: 'static + private::Sealed { - fn label(&self) -> impl Into; - fn icon(&self) -> Option; - fn on_click(self) -> Box; +pub trait ButtonBuilder: 'static + private::ToggleButtonStyle { + fn into_configuration(self) -> ButtonConfiguration; } pub struct ToggleButtonSimple { @@ -318,19 +322,15 @@ impl ToggleButtonSimple { } } -impl private::Sealed for ToggleButtonSimple {} +impl private::ToggleButtonStyle for ToggleButtonSimple {} impl ButtonBuilder for ToggleButtonSimple { - fn label(&self) -> impl Into { - self.label.clone() - } - - fn icon(&self) -> Option { - None - } - - fn on_click(self) -> Box { - self.on_click + fn into_configuration(self) -> ButtonConfiguration { + ButtonConfiguration { + label: self.label, + icon: None, + on_click: self.on_click, + } } } @@ -354,58 +354,14 @@ impl ToggleButtonWithIcon { } } -impl private::Sealed for ToggleButtonWithIcon {} +impl private::ToggleButtonStyle for ToggleButtonWithIcon {} impl ButtonBuilder for ToggleButtonWithIcon { - fn label(&self) -> impl Into { - self.label.clone() - } - - fn icon(&self) -> Option { - Some(self.icon) - } - - fn on_click(self) -> Box { - self.on_click - } -} - -struct ToggleButtonRow { - items: Vec, - index_offset: usize, - last_item_idx: usize, - is_last_row: bool, -} - -impl ToggleButtonRow { - fn new(items: Vec, index_offset: usize, is_last_row: bool) -> Self { - Self { - index_offset, - last_item_idx: index_offset + items.len() - 1, - is_last_row, - items, - } - } -} - -enum ToggleButtonGroupRows { - Single(Vec), - Multiple(Vec, Vec), -} - -impl ToggleButtonGroupRows { - fn items(self) -> impl IntoIterator> { - match self { - ToggleButtonGroupRows::Single(items) => { - vec![ToggleButtonRow::new(items, 0, true)] - } - ToggleButtonGroupRows::Multiple(first_row, second_row) => { - let row_len = first_row.len(); - vec![ - ToggleButtonRow::new(first_row, 0, false), - ToggleButtonRow::new(second_row, row_len, true), - ] - } + fn into_configuration(self) -> ButtonConfiguration { + ButtonConfiguration { + label: self.label, + icon: Some(self.icon), + on_click: self.on_click, } } } @@ -418,48 +374,42 @@ pub enum ToggleButtonGroupStyle { } #[derive(IntoElement)] -pub struct ToggleButtonGroup +pub struct ToggleButtonGroup where T: ButtonBuilder, { - group_name: SharedString, - rows: ToggleButtonGroupRows, + group_name: &'static str, + rows: [[T; COLS]; ROWS], style: ToggleButtonGroupStyle, button_width: Rems, selected_index: usize, } -impl ToggleButtonGroup { - pub fn single_row( - group_name: impl Into, - buttons: impl IntoIterator, - ) -> Self { +impl ToggleButtonGroup { + pub fn single_row(group_name: &'static str, buttons: [T; COLS]) -> Self { Self { - group_name: group_name.into(), - rows: ToggleButtonGroupRows::Single(Vec::from_iter(buttons)), + group_name, + rows: [buttons], style: ToggleButtonGroupStyle::Transparent, button_width: rems_from_px(100.), selected_index: 0, } } +} - pub fn multiple_rows( - group_name: impl Into, - first_row: [T; ROWS], - second_row: [T; ROWS], - ) -> Self { +impl ToggleButtonGroup { + pub fn two_rows(group_name: &'static str, first_row: [T; COLS], second_row: [T; COLS]) -> Self { Self { - group_name: group_name.into(), - rows: ToggleButtonGroupRows::Multiple( - Vec::from_iter(first_row), - Vec::from_iter(second_row), - ), + group_name, + rows: [first_row, second_row], style: ToggleButtonGroupStyle::Transparent, button_width: rems_from_px(100.), selected_index: 0, } } +} +impl ToggleButtonGroup { pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self { self.style = style; self @@ -476,60 +426,56 @@ impl ToggleButtonGroup { } } -impl RenderOnce for ToggleButtonGroup { +impl RenderOnce + for ToggleButtonGroup +{ fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let rows = self.rows.items().into_iter().map(|row| { - ( - row.items - .into_iter() - .enumerate() - .map(move |(index, item)| (index + row.index_offset, row.last_item_idx, item)) - .map(|(index, last_item_idx, item)| { - ( - ButtonLike::new((self.group_name.clone(), index)) - .when(index == self.selected_index, |this| { - this.toggle_state(true) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - }) - .rounding(None) - .when(self.style == ToggleButtonGroupStyle::Filled, |button| { - button.style(ButtonStyle::Filled) - }) - .child( - h_flex() - .min_w(self.button_width) - .gap_1p5() - .justify_center() - .when_some(item.icon(), |this, icon| { - this.child(Icon::new(icon).size(IconSize::XSmall).map( - |this| { - if index == self.selected_index { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - }, - )) - }) - .child( - Label::new(item.label()) - .when(index == self.selected_index, |this| { - this.color(Color::Accent) - }), - ), - ) - .on_click(item.on_click()), - index == last_item_idx, - ) - }), - row.is_last_row, - ) + let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| { + row.into_iter().enumerate().map(move |(index, button)| { + let ButtonConfiguration { + label, + icon, + on_click, + } = button.into_configuration(); + + ButtonLike::new((self.group_name, row_index * COLS + index)) + .when(index == self.selected_index, |this| { + this.toggle_state(true) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + }) + .rounding(None) + .when(self.style == ToggleButtonGroupStyle::Filled, |button| { + button.style(ButtonStyle::Filled) + }) + .child( + h_flex() + .min_w(self.button_width) + .gap_1p5() + .justify_center() + .when_some(icon, |this, icon| { + this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| { + if index == self.selected_index { + this.color(Color::Accent) + } else { + this.color(Color::Muted) + } + })) + }) + .child( + Label::new(label).when(index == self.selected_index, |this| { + this.color(Color::Accent) + }), + ), + ) + .on_click(on_click) + .into_any_element() + }) }); + let border_color = cx.theme().colors().border.opacity(0.6); let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined || self.style == ToggleButtonGroupStyle::Filled; let is_transparent = self.style == ToggleButtonGroupStyle::Transparent; - let border_color = cx.theme().colors().border.opacity(0.6); v_flex() .rounded_md() @@ -541,13 +487,15 @@ impl RenderOnce for ToggleButtonGroup { this.border_1().border_color(border_color) } }) - .children(rows.map(|(items, last_row)| { + .children(entries.enumerate().map(|(row_index, row)| { + let last_row = row_index == ROWS - 1; h_flex() .when(!is_outlined_or_filled, |this| this.gap_px()) .when(is_outlined_or_filled && !last_row, |this| { this.border_b_1().border_color(border_color) }) - .children(items.map(|(item, last_item)| { + .children(row.enumerate().map(|(item_index, item)| { + let last_item = item_index == COLS - 1; div() .when(is_outlined_or_filled && !last_item, |this| { this.border_r_1().border_color(border_color) @@ -566,7 +514,9 @@ component::__private::inventory::submit! { component::ComponentFn::new(register_toggle_button_group) } -impl Component for ToggleButtonGroup { +impl Component + for ToggleButtonGroup +{ fn name() -> &'static str { "ToggleButtonGroup" } @@ -628,7 +578,7 @@ impl Component for ToggleButtonGroup { ), single_example( "Multiple Row Group", - ToggleButtonGroup::multiple_rows( + ToggleButtonGroup::two_rows( "multiple_row_test", [ ToggleButtonSimple::new("First", |_, _, _| {}), @@ -647,7 +597,7 @@ impl Component for ToggleButtonGroup { ), single_example( "Multiple Row Group with Icons", - ToggleButtonGroup::multiple_rows( + ToggleButtonGroup::two_rows( "multiple_row_test_icons", [ ToggleButtonWithIcon::new( @@ -736,7 +686,7 @@ impl Component for ToggleButtonGroup { ), single_example( "Multiple Row Group", - ToggleButtonGroup::multiple_rows( + ToggleButtonGroup::two_rows( "multiple_row_test", [ ToggleButtonSimple::new("First", |_, _, _| {}), @@ -756,7 +706,7 @@ impl Component for ToggleButtonGroup { ), single_example( "Multiple Row Group with Icons", - ToggleButtonGroup::multiple_rows( + ToggleButtonGroup::two_rows( "multiple_row_test", [ ToggleButtonWithIcon::new( @@ -846,7 +796,7 @@ impl Component for ToggleButtonGroup { ), single_example( "Multiple Row Group", - ToggleButtonGroup::multiple_rows( + ToggleButtonGroup::two_rows( "multiple_row_test", [ ToggleButtonSimple::new("First", |_, _, _| {}), @@ -866,7 +816,7 @@ impl Component for ToggleButtonGroup { ), single_example( "Multiple Row Group with Icons", - ToggleButtonGroup::multiple_rows( + ToggleButtonGroup::two_rows( "multiple_row_test", [ ToggleButtonWithIcon::new( From 3824751e617fd75c917196bbf70767cedb4074de Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 29 Jul 2025 18:01:03 -0500 Subject: [PATCH 31/35] Add meta description tag to docs pages (#35112) Closes #ISSUE Adds basic frontmatter support to `.md` files in docs. The only supported keys currently are `description` which becomes a `` tag, and `title` which becomes a normal `title` tag, with the title contents prefixed with the subject of the file. An example of the syntax can be found in `git.md`, as well as below ```md --- title: Some more detailed title for this page description: A page-specific description --- # Editor ``` The above will be transformed into (with non-relevant tags removed) ```html Editor | Some more detailed title for this page

Editor

``` If no front-matter is provided, or If one or both keys aren't provided, the title and description will be set based on the `default-title` and `default-description` keys in `book.toml` respectively. ## Implementation details Unfortunately, `mdbook` does not support post-processing like it does pre-processing, and only supports defining one description to put in the meta tag per book rather than per file. So in order to apply post-processing (necessary to modify the html head tags) the global book description is set to a marker value `#description#` and the html renderer is replaced with a sub-command of `docs_preprocessor` that wraps the builtin `html` renderer and applies post-processing to the `html` files, replacing the marker value and the `(.*)` with the contents of the front-matter if there is one. ## Known limitations The front-matter parsing is extremely simple, which avoids needing to take on an additional dependency, or implement full yaml parsing. * Double quotes and multi-line values are not supported, i.e. Keys and values must be entirely on the same line, with no double quotes around the value. The following will not work: ```md --- title: Some Multi-line Title --- ``` * The front-matter must be at the top of the file, with only white-space preceding it * The contents of the title and description will not be html-escaped. They should be simple ascii text with no unicode or emoji characters Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Katie Greer --- Cargo.lock | 2 +- crates/docs_preprocessor/Cargo.toml | 8 +- crates/docs_preprocessor/src/main.rs | 246 ++++++++++++++++++++++----- crates/zlog/src/sink.rs | 23 +++ crates/zlog/src/zlog.rs | 2 +- docs/README.md | 61 +++++++ docs/book.toml | 20 ++- docs/src/git.md | 5 + docs/theme/index.hbs | 2 +- 9 files changed, 319 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1cd9e521f1afebbaae765f0a13f67864865a5e7f..d91c5d5ecad5a0b5aa3534177b3fba62184e3f60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4758,7 +4758,6 @@ name = "docs_preprocessor" version = "0.1.0" dependencies = [ "anyhow", - "clap", "command_palette", "gpui", "mdbook", @@ -4769,6 +4768,7 @@ dependencies = [ "util", "workspace-hack", "zed", + "zlog", ] [[package]] diff --git a/crates/docs_preprocessor/Cargo.toml b/crates/docs_preprocessor/Cargo.toml index a0df669abe6036859e2f6c73a26541ed1fc25767..a9eff17fa1c8eba38cfe3dc6748b45a30d591e95 100644 --- a/crates/docs_preprocessor/Cargo.toml +++ b/crates/docs_preprocessor/Cargo.toml @@ -7,17 +7,17 @@ license = "GPL-3.0-or-later" [dependencies] anyhow.workspace = true -clap.workspace = true +command_palette.workspace = true +gpui.workspace = true mdbook = "0.4.40" +regex.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true -regex.workspace = true util.workspace = true workspace-hack.workspace = true zed.workspace = true -gpui.workspace = true -command_palette.workspace = true +zlog.workspace = true [lints] workspace = true diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 8eeeb6f0c5a105e186bdeac3e83807e50db721ea..1448f4cb52369b5142856faabd67c0f9c3271220 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -1,14 +1,15 @@ -use anyhow::Result; -use clap::{Arg, ArgMatches, Command}; +use anyhow::{Context, Result}; use mdbook::BookItem; use mdbook::book::{Book, Chapter}; use mdbook::preprocess::CmdPreprocessor; use regex::Regex; use settings::KeymapFile; -use std::collections::HashSet; +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; use std::io::{self, Read}; use std::process; use std::sync::LazyLock; +use util::paths::PathExt; static KEYMAP_MACOS: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap") @@ -20,60 +21,68 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); -pub fn make_app() -> Command { - Command::new("zed-docs-preprocessor") - .about("Preprocesses Zed Docs content to provide rich action & keybinding support and more") - .subcommand( - Command::new("supports") - .arg(Arg::new("renderer").required(true)) - .about("Check whether a renderer is supported by this preprocessor"), - ) -} +const FRONT_MATTER_COMMENT: &'static str = ""; fn main() -> Result<()> { - let matches = make_app().get_matches(); + zlog::init(); + zlog::init_output_stderr(); // call a zed:: function so everything in `zed` crate is linked and // all actions in the actual app are registered zed::stdout_is_a_pty(); - - if let Some(sub_args) = matches.subcommand_matches("supports") { - handle_supports(sub_args); - } else { - handle_preprocessing()?; + let args = std::env::args().skip(1).collect::>(); + + match args.get(0).map(String::as_str) { + Some("supports") => { + let renderer = args.get(1).expect("Required argument"); + let supported = renderer != "not-supported"; + if supported { + process::exit(0); + } else { + process::exit(1); + } + } + Some("postprocess") => handle_postprocessing()?, + _ => handle_preprocessing()?, } Ok(()) } #[derive(Debug, Clone, PartialEq, Eq, Hash)] -enum Error { +enum PreprocessorError { ActionNotFound { action_name: String }, DeprecatedActionUsed { used: String, should_be: String }, + InvalidFrontmatterLine(String), } -impl Error { +impl PreprocessorError { fn new_for_not_found_action(action_name: String) -> Self { for action in &*ALL_ACTIONS { for alias in action.deprecated_aliases { if alias == &action_name { - return Error::DeprecatedActionUsed { + return PreprocessorError::DeprecatedActionUsed { used: action_name.clone(), should_be: action.name.to_string(), }; } } } - Error::ActionNotFound { + PreprocessorError::ActionNotFound { action_name: action_name.to_string(), } } } -impl std::fmt::Display for Error { +impl std::fmt::Display for PreprocessorError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Error::ActionNotFound { action_name } => write!(f, "Action not found: {}", action_name), - Error::DeprecatedActionUsed { used, should_be } => write!( + PreprocessorError::InvalidFrontmatterLine(line) => { + write!(f, "Invalid frontmatter line: {}", line) + } + PreprocessorError::ActionNotFound { action_name } => { + write!(f, "Action not found: {}", action_name) + } + PreprocessorError::DeprecatedActionUsed { used, should_be } => write!( f, "Deprecated action used: {} should be {}", used, should_be @@ -89,8 +98,9 @@ fn handle_preprocessing() -> Result<()> { let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?; - let mut errors = HashSet::::new(); + let mut errors = HashSet::::new(); + handle_frontmatter(&mut book, &mut errors); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); @@ -108,19 +118,41 @@ fn handle_preprocessing() -> Result<()> { Ok(()) } -fn handle_supports(sub_args: &ArgMatches) -> ! { - let renderer = sub_args - .get_one::("renderer") - .expect("Required argument"); - let supported = renderer != "not-supported"; - if supported { - process::exit(0); - } else { - process::exit(1); - } +fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) { + let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap(); + for_each_chapter_mut(book, |chapter| { + let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| { + let frontmatter = caps[1].trim(); + let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']); + let mut metadata = HashMap::::default(); + for line in frontmatter.lines() { + let Some((name, value)) = line.split_once(':') else { + errors.insert(PreprocessorError::InvalidFrontmatterLine(format!( + "{}: {}", + chapter_breadcrumbs(&chapter), + line + ))); + continue; + }; + let name = name.trim(); + let value = value.trim(); + metadata.insert(name.to_string(), value.to_string()); + } + FRONT_MATTER_COMMENT.replace( + "{}", + &serde_json::to_string(&metadata).expect("Failed to serialize metadata"), + ) + }); + match new_content { + Cow::Owned(content) => { + chapter.content = content; + } + Cow::Borrowed(_) => {} + } + }); } -fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { +fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { @@ -128,7 +160,9 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { +fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#action (.*?)\}").unwrap(); for_each_chapter_mut(book, |chapter| { @@ -152,7 +186,9 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet) { .replace_all(&chapter.content, |caps: ®ex::Captures| { let name = caps[1].trim(); let Some(action) = find_action_by_name(name) else { - errors.insert(Error::new_for_not_found_action(name.to_string())); + errors.insert(PreprocessorError::new_for_not_found_action( + name.to_string(), + )); return String::new(); }; format!("{}", &action.human_name) @@ -217,6 +253,13 @@ fn name_for_action(action_as_str: String) -> String { .unwrap_or(action_as_str) } +fn chapter_breadcrumbs(chapter: &Chapter) -> String { + let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1); + breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str)); + breadcrumbs.push(chapter.name.as_str()); + format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > ")) +} + fn load_keymap(asset_path: &str) -> Result { let content = util::asset_str::(asset_path); KeymapFile::parse(content.as_ref()) @@ -254,3 +297,126 @@ fn dump_all_gpui_actions() -> Vec { return actions; } + +fn handle_postprocessing() -> Result<()> { + let logger = zlog::scoped!("render"); + let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?; + let output = ctx + .config + .get_mut("output") + .expect("has output") + .as_table_mut() + .expect("output is table"); + let zed_html = output.remove("zed-html").expect("zed-html output defined"); + let default_description = zed_html + .get("default-description") + .expect("Default description not found") + .as_str() + .expect("Default description not a string") + .to_string(); + let default_title = zed_html + .get("default-title") + .expect("Default title not found") + .as_str() + .expect("Default title not a string") + .to_string(); + + output.insert("html".to_string(), zed_html); + mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?; + let ignore_list = ["toc.html"]; + + let root_dir = ctx.destination.clone(); + let mut files = Vec::with_capacity(128); + let mut queue = Vec::with_capacity(64); + queue.push(root_dir.clone()); + while let Some(dir) = queue.pop() { + for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? { + let Ok(entry) = entry else { + continue; + }; + let file_type = entry.file_type().context("Failed to determine file type")?; + if file_type.is_dir() { + queue.push(entry.path()); + } + if file_type.is_file() + && matches!( + entry.path().extension().and_then(std::ffi::OsStr::to_str), + Some("html") + ) + { + if ignore_list.contains(&&*entry.file_name().to_string_lossy()) { + zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy()); + } else { + files.push(entry.path()); + } + } + } + } + + zlog::info!(logger => "Processing {} `.html` files", files.len()); + let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap(); + for file in files { + let contents = std::fs::read_to_string(&file)?; + let mut meta_description = None; + let mut meta_title = None; + let contents = meta_regex.replace(&contents, |caps: ®ex::Captures| { + let metadata: HashMap = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata"); + for (kind, content) in metadata { + match kind.as_str() { + "description" => { + meta_description = Some(content); + } + "title" => { + meta_title = Some(content); + } + _ => { + zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir)); + } + } + } + String::new() + }); + let meta_description = meta_description.as_ref().unwrap_or_else(|| { + zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir)); + &default_description + }); + let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir)); + let meta_title = meta_title.as_ref().unwrap_or_else(|| { + zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir)); + &default_title + }); + let meta_title = format!("{} | {}", page_title, meta_title); + zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir)); + let contents = contents.replace("#description#", meta_description); + let contents = TITLE_REGEX + .replace(&contents, |_: ®ex::Captures| { + format!("{}", meta_title) + }) + .to_string(); + // let contents = contents.replace("#title#", &meta_title); + std::fs::write(file, contents)?; + } + return Ok(()); + + fn pretty_path<'a>( + path: &'a std::path::PathBuf, + root: &'a std::path::PathBuf, + ) -> &'a std::path::Path { + &path.strip_prefix(&root).unwrap_or(&path) + } + const TITLE_REGEX: std::cell::LazyCell = + std::cell::LazyCell::new(|| Regex::new(r"\s*(.*?)\s*").unwrap()); + fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String { + let title_tag_contents = &TITLE_REGEX + .captures(&contents) + .with_context(|| format!("Failed to find title in {:?}", pretty_path)) + .expect("Page has element")[1]; + let title = title_tag_contents + .trim() + .strip_suffix("- Zed") + .unwrap_or(title_tag_contents) + .trim() + .to_string(); + title + } +} diff --git a/crates/zlog/src/sink.rs b/crates/zlog/src/sink.rs index acf0469c775ec89135dfd87813ee20a9351781f5..17aa08026e6dea4bbc98946e044ce3828e5aa28f 100644 --- a/crates/zlog/src/sink.rs +++ b/crates/zlog/src/sink.rs @@ -21,6 +21,8 @@ const ANSI_MAGENTA: &str = "\x1b[35m"; /// Whether stdout output is enabled. static mut ENABLED_SINKS_STDOUT: bool = false; +/// Whether stderr output is enabled. +static mut ENABLED_SINKS_STDERR: bool = false; /// Is Some(file) if file output is enabled. static ENABLED_SINKS_FILE: Mutex<Option<std::fs::File>> = Mutex::new(None); @@ -45,6 +47,12 @@ pub fn init_output_stdout() { } } +pub fn init_output_stderr() { + unsafe { + ENABLED_SINKS_STDERR = true; + } +} + pub fn init_output_file( path: &'static PathBuf, path_rotate: Option<&'static PathBuf>, @@ -115,6 +123,21 @@ pub fn submit(record: Record) { }, record.message ); + } else if unsafe { ENABLED_SINKS_STDERR } { + let mut stdout = std::io::stderr().lock(); + _ = writeln!( + &mut stdout, + "{} {ANSI_BOLD}{}{}{ANSI_RESET} {} {}", + chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), + LEVEL_ANSI_COLORS[record.level as usize], + LEVEL_OUTPUT_STRINGS[record.level as usize], + SourceFmt { + scope: record.scope, + module_path: record.module_path, + ansi: true, + }, + record.message + ); } let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| { ENABLED_SINKS_FILE.clear_poison(); diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs index 570c82314c5d1a56e03610a2740d35833ef07d69..5b40278f3fb0adbafe1815608765aa4ab3d44e57 100644 --- a/crates/zlog/src/zlog.rs +++ b/crates/zlog/src/zlog.rs @@ -5,7 +5,7 @@ mod env_config; pub mod filter; pub mod sink; -pub use sink::{flush, init_output_file, init_output_stdout}; +pub use sink::{flush, init_output_file, init_output_stderr, init_output_stdout}; pub const SCOPE_DEPTH_MAX: usize = 4; diff --git a/docs/README.md b/docs/README.md index 55993c9e36e9a78a9271dbc509ef9129d8c91422..a225903674966b142d5f35845018c98ce9770258 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,3 +69,64 @@ Templates are just functions that modify the source of the docs pages (usually w - Template Trait: crates/docs_preprocessor/src/templates.rs - Example template: crates/docs_preprocessor/src/templates/keybinding.rs - Client-side plugins: docs/theme/plugins.js + +## Postprocessor + +A postprocessor is implemented as a sub-command of `docs_preprocessor` that wraps the builtin `html` renderer and applies post-processing to the `html` files, to add support for page-specific title and meta description values. + +An example of the syntax can be found in `git.md`, as well as below + +```md +--- +title: Some more detailed title for this page +description: A page-specific description +--- + +# Editor +``` + +The above will be transformed into (with non-relevant tags removed) + +```html +<head> + <title>Editor | Some more detailed title for this page + + + +

Editor

+ +``` + +If no front-matter is provided, or If one or both keys aren't provided, the title and description will be set based on the `default-title` and `default-description` keys in `book.toml` respectively. + +### Implementation details + +Unfortunately, `mdbook` does not support post-processing like it does pre-processing, and only supports defining one description to put in the meta tag per book rather than per file. So in order to apply post-processing (necessary to modify the html head tags) the global book description is set to a marker value `#description#` and the html renderer is replaced with a sub-command of `docs_preprocessor` that wraps the builtin `html` renderer and applies post-processing to the `html` files, replacing the marker value and the `(.*)` with the contents of the front-matter if there is one. + +### Known limitations + +The front-matter parsing is extremely simple, which avoids needing to take on an additional dependency, or implement full yaml parsing. + +- Double quotes and multi-line values are not supported, i.e. Keys and values must be entirely on the same line, with no double quotes around the value. + +The following will not work: + +```md +--- +title: Some + Multi-line + Title +--- +``` + +And neither will: + +```md +--- +title: "Some title" +--- +``` + +- The front-matter must be at the top of the file, with only white-space preceding it + +- The contents of the title and description will not be html-escaped. They should be simple ascii text with no unicode or emoji characters diff --git a/docs/book.toml b/docs/book.toml index 518fbec819f9fde7b02dbefce4ecaaa29de65348..60ddc5ac515cb73f7b0b4f2f8c2c193bdddf228b 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -6,13 +6,27 @@ src = "src" title = "Zed" site-url = "/docs/" -[output.html] +[build] +extra-watch-dirs = ["../crates/docs_preprocessor"] + +# zed-html is a "custom" renderer that just wraps the +# builtin mdbook html renderer, and applies post-processing +# as post-processing is not possible with mdbook in the same way +# pre-processing is +# The config is passed directly to the html renderer, so all config +# options that apply to html apply to zed-html +[output.zed-html] +command = "cargo run -p docs_preprocessor -- postprocess" +# Set here instead of above as we only use it replace the `#description#` we set in the template +# when no front-matter is provided value +default-description = "Learn how to use and customize Zed, the fast, collaborative code editor. Official docs on features, configuration, AI tools, and workflows." +default-title = "Zed Code Editor Documentation" no-section-label = true preferred-dark-theme = "dark" additional-css = ["theme/page-toc.css", "theme/plugins.css", "theme/highlight.css"] additional-js = ["theme/page-toc.js", "theme/plugins.js"] -[output.html.print] +[output.zed-html.print] enable = false # Redirects for `/docs` pages. @@ -24,7 +38,7 @@ enable = false # The destination URLs are interpreted relative to `https://zed.dev`. # - Redirects to other docs pages should end in `.html` # - You can link to pages on the Zed site by omitting the `/docs` in front of it. -[output.html.redirect] +[output.zed-html.redirect] # AI "/ai.html" = "/docs/ai/overview.html" "/assistant-panel.html" = "/docs/ai/agent-panel.html" diff --git a/docs/src/git.md b/docs/src/git.md index 5b5c8a3b15a8eb1488b6a81bc2c0b429cabf7eaa..cccbad9b2e37ba55dc45f1f100883437759727f0 100644 --- a/docs/src/git.md +++ b/docs/src/git.md @@ -1,3 +1,8 @@ +--- +description: Zed is a text editor that supports lots of Git features +title: Zed Editor Git integration documentation +--- + # Git Zed currently offers a set of fundamental Git features, with support coming in the future for more advanced ones, like conflict resolution tools, line by line staging, and more. diff --git a/docs/theme/index.hbs b/docs/theme/index.hbs index 8ab4f21cf167668ef3fb2f905dafb6b57496b88e..4339a02d1722d0d64e67b35de66889d9a849e9a4 100644 --- a/docs/theme/index.hbs +++ b/docs/theme/index.hbs @@ -15,7 +15,7 @@ {{> head}} - + From b8f3a9101c77ade0e3e44f06fb339af177031e56 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 19:30:45 -0400 Subject: [PATCH 32/35] Add `cloud_llm_client` crate (#35307) This PR adds a `cloud_llm_client` crate to take the place of the `zed_llm_client`. Release Notes: - N/A --- Cargo.lock | 13 + Cargo.toml | 3 +- crates/cloud_llm_client/Cargo.toml | 23 ++ crates/cloud_llm_client/LICENSE-APACHE | 1 + .../cloud_llm_client/src/cloud_llm_client.rs | 370 ++++++++++++++++++ 5 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 crates/cloud_llm_client/Cargo.toml create mode 120000 crates/cloud_llm_client/LICENSE-APACHE create mode 100644 crates/cloud_llm_client/src/cloud_llm_client.rs diff --git a/Cargo.lock b/Cargo.lock index d91c5d5ecad5a0b5aa3534177b3fba62184e3f60..527b99f3c240092a0ef677280fcc694a8b93866a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3031,6 +3031,19 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "cloud_llm_client" +version = "0.1.0" +dependencies = [ + "anyhow", + "pretty_assertions", + "serde", + "serde_json", + "strum 0.27.1", + "uuid", + "workspace-hack", +] + [[package]] name = "clru" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 16ace7dee0b98be78013efd3cd40df77d6284cf7..e08736e38e76ce3274d5f23b57eb47c90866e601 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "crates/cli", "crates/client", "crates/clock", + "crates/cloud_llm_client", "crates/collab", "crates/collab_ui", "crates/collections", @@ -70,7 +71,6 @@ members = [ "crates/gpui", "crates/gpui_macros", "crates/gpui_tokio", - "crates/html_to_markdown", "crates/http_client", "crates/http_client_tls", @@ -251,6 +251,7 @@ channel = { path = "crates/channel" } cli = { path = "crates/cli" } client = { path = "crates/client" } clock = { path = "crates/clock" } +cloud_llm_client = { path = "crates/cloud_llm_client" } collab = { path = "crates/collab" } collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections" } diff --git a/crates/cloud_llm_client/Cargo.toml b/crates/cloud_llm_client/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..6f090d3c6ea67d8bb189212fb9704b618554f671 --- /dev/null +++ b/crates/cloud_llm_client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cloud_llm_client" +version = "0.1.0" +publish.workspace = true +edition.workspace = true +license = "Apache-2.0" + +[lints] +workspace = true + +[lib] +path = "src/cloud_llm_client.rs" + +[dependencies] +anyhow.workspace = true +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_llm_client/LICENSE-APACHE b/crates/cloud_llm_client/LICENSE-APACHE new file mode 120000 index 0000000000000000000000000000000000000000..1cd601d0a3affae83854be02a0afdec3b7a9ec4d --- /dev/null +++ b/crates/cloud_llm_client/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/cloud_llm_client/src/cloud_llm_client.rs b/crates/cloud_llm_client/src/cloud_llm_client.rs new file mode 100644 index 0000000000000000000000000000000000000000..2488088a496dab9097528cf7bc9833930d3224dc --- /dev/null +++ b/crates/cloud_llm_client/src/cloud_llm_client.rs @@ -0,0 +1,370 @@ +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::Context as _; +use serde::{Deserialize, Serialize}; +use strum::{Display, EnumIter, EnumString}; +use uuid::Uuid; + +/// The name of the header used to indicate which version of Zed the client is running. +pub const ZED_VERSION_HEADER_NAME: &str = "x-zed-version"; + +/// The name of the header used to indicate when a request failed due to an +/// expired LLM token. +/// +/// The client may use this as a signal to refresh the token. +pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token"; + +/// The name of the header used to indicate what plan the user is currently on. +pub const CURRENT_PLAN_HEADER_NAME: &str = "x-zed-plan"; + +/// The name of the header used to indicate the usage limit for model requests. +pub const MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-model-requests-usage-limit"; + +/// The name of the header used to indicate the usage amount for model requests. +pub const MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-model-requests-usage-amount"; + +/// The name of the header used to indicate the usage limit for edit predictions. +pub const EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-limit"; + +/// The name of the header used to indicate the usage amount for edit predictions. +pub const EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-amount"; + +/// The name of the header used to indicate the resource for which the subscription limit has been reached. +pub const SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME: &str = "x-zed-subscription-limit-resource"; + +pub const MODEL_REQUESTS_RESOURCE_HEADER_VALUE: &str = "model_requests"; +pub const EDIT_PREDICTIONS_RESOURCE_HEADER_VALUE: &str = "edit_predictions"; + +/// The name of the header used to indicate that the maximum number of consecutive tool uses has been reached. +pub const TOOL_USE_LIMIT_REACHED_HEADER_NAME: &str = "x-zed-tool-use-limit-reached"; + +/// The name of the header used to indicate the the minimum required Zed version. +/// +/// This can be used to force a Zed upgrade in order to continue communicating +/// with the LLM service. +pub const MINIMUM_REQUIRED_VERSION_HEADER_NAME: &str = "x-zed-minimum-required-version"; + +/// The name of the header used by the client to indicate to the server that it supports receiving status messages. +pub const CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str = + "x-zed-client-supports-status-messages"; + +/// The name of the header used by the server to indicate to the client that it supports sending status messages. +pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str = + "x-zed-server-supports-status-messages"; + +#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UsageLimit { + Limited(i32), + Unlimited, +} + +impl FromStr for UsageLimit { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + match value { + "unlimited" => Ok(Self::Unlimited), + limit => limit + .parse::() + .map(Self::Limited) + .context("failed to parse limit"), + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Plan { + #[default] + #[serde(alias = "Free")] + ZedFree, + #[serde(alias = "ZedPro")] + ZedPro, + #[serde(alias = "ZedProTrial")] + ZedProTrial, +} + +impl Plan { + pub fn as_str(&self) -> &'static str { + match self { + Plan::ZedFree => "zed_free", + Plan::ZedPro => "zed_pro", + Plan::ZedProTrial => "zed_pro_trial", + } + } + + pub fn model_requests_limit(&self) -> UsageLimit { + match self { + Plan::ZedPro => UsageLimit::Limited(500), + Plan::ZedProTrial => UsageLimit::Limited(150), + Plan::ZedFree => UsageLimit::Limited(50), + } + } + + pub fn edit_predictions_limit(&self) -> UsageLimit { + match self { + Plan::ZedPro => UsageLimit::Unlimited, + Plan::ZedProTrial => UsageLimit::Unlimited, + Plan::ZedFree => UsageLimit::Limited(2_000), + } + } +} + +impl FromStr for Plan { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + match value { + "zed_free" => Ok(Plan::ZedFree), + "zed_pro" => Ok(Plan::ZedPro), + "zed_pro_trial" => Ok(Plan::ZedProTrial), + plan => Err(anyhow::anyhow!("invalid plan: {plan:?}")), + } + } +} + +#[derive( + Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum LanguageModelProvider { + Anthropic, + OpenAi, + Google, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PredictEditsBody { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub outline: Option, + pub input_events: String, + pub input_excerpt: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub speculated_output: Option, + /// Whether the user provided consent for sampling this interaction. + #[serde(default, alias = "data_collection_permission")] + pub can_collect_data: bool, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub diagnostic_groups: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PredictEditsResponse { + pub request_id: Uuid, + pub output_excerpt: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AcceptEditPredictionBody { + pub request_id: Uuid, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CompletionMode { + Normal, + Max, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CompletionIntent { + UserPrompt, + ToolResults, + ThreadSummarization, + ThreadContextSummarization, + CreateFile, + EditFile, + InlineAssist, + TerminalInlineAssist, + GenerateGitCommitMessage, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CompletionBody { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub thread_id: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub prompt_id: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub intent: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub mode: Option, + pub provider: LanguageModelProvider, + pub model: String, + pub provider_request: serde_json::Value, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CompletionRequestStatus { + Queued { + position: usize, + }, + Started, + Failed { + code: String, + message: String, + request_id: Uuid, + /// Retry duration in seconds. + retry_after: Option, + }, + UsageUpdated { + amount: usize, + limit: UsageLimit, + }, + ToolUseLimitReached, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CompletionEvent { + Status(CompletionRequestStatus), + Event(T), +} + +impl CompletionEvent { + pub fn into_status(self) -> Option { + match self { + Self::Status(status) => Some(status), + Self::Event(_) => None, + } + } + + pub fn into_event(self) -> Option { + match self { + Self::Event(event) => Some(event), + Self::Status(_) => None, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct WebSearchBody { + pub query: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct WebSearchResponse { + pub results: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct WebSearchResult { + pub title: String, + pub url: String, + pub text: String, +} + +#[derive(Serialize, Deserialize)] +pub struct CountTokensBody { + pub provider: LanguageModelProvider, + pub model: String, + pub provider_request: serde_json::Value, +} + +#[derive(Serialize, Deserialize)] +pub struct CountTokensResponse { + pub tokens: usize, +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)] +pub struct LanguageModelId(pub Arc); + +impl std::fmt::Display for LanguageModelId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct LanguageModel { + pub provider: LanguageModelProvider, + pub id: LanguageModelId, + pub display_name: String, + pub max_token_count: usize, + pub max_token_count_in_max_mode: Option, + pub max_output_tokens: usize, + pub supports_tools: bool, + pub supports_images: bool, + pub supports_thinking: bool, + pub supports_max_mode: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ListModelsResponse { + pub models: Vec, + pub default_model: LanguageModelId, + pub default_fast_model: LanguageModelId, + pub recommended_models: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetSubscriptionResponse { + pub plan: Plan, + pub usage: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CurrentUsage { + pub model_requests: UsageData, + pub edit_predictions: UsageData, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UsageData { + pub used: u32, + pub limit: UsageLimit, +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + use serde_json::json; + + use super::*; + + #[test] + fn test_plan_deserialize_snake_case() { + let plan = serde_json::from_value::(json!("zed_free")).unwrap(); + assert_eq!(plan, Plan::ZedFree); + + let plan = serde_json::from_value::(json!("zed_pro")).unwrap(); + assert_eq!(plan, Plan::ZedPro); + + let plan = serde_json::from_value::(json!("zed_pro_trial")).unwrap(); + assert_eq!(plan, Plan::ZedProTrial); + } + + #[test] + fn test_plan_deserialize_aliases() { + let plan = serde_json::from_value::(json!("Free")).unwrap(); + assert_eq!(plan, Plan::ZedFree); + + let plan = serde_json::from_value::(json!("ZedPro")).unwrap(); + assert_eq!(plan, Plan::ZedPro); + + let plan = serde_json::from_value::(json!("ZedProTrial")).unwrap(); + assert_eq!(plan, Plan::ZedProTrial); + } + + #[test] + fn test_usage_limit_from_str() { + let limit = UsageLimit::from_str("unlimited").unwrap(); + assert!(matches!(limit, UsageLimit::Unlimited)); + + let limit = UsageLimit::from_str(&0.to_string()).unwrap(); + assert!(matches!(limit, UsageLimit::Limited(0))); + + let limit = UsageLimit::from_str(&50.to_string()).unwrap(); + assert!(matches!(limit, UsageLimit::Limited(50))); + + for value in ["not_a_number", "50xyz"] { + let limit = UsageLimit::from_str(value); + assert!(limit.is_err()); + } + } +} From 17a0179f0ae79444407f7d217acb5ad50d783fd9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 29 Jul 2025 16:38:06 -0700 Subject: [PATCH 33/35] Stop caching needlessly (#35308) Release Notes: - N/A --- crates/workspace/src/workspace.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 77d76b44f566832fed2e15c83413c63cad29e058..e58014e7b83319d73d04a6ef1a251e7eb8fa0093 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1065,7 +1065,6 @@ pub struct Workspace { center: PaneGroup, left_dock: Entity, bottom_dock: Entity, - bottom_dock_layout: BottomDockLayout, right_dock: Entity, panes: Vec>, panes_by_item: HashMap>, @@ -1307,7 +1306,6 @@ impl Workspace { ) .detach(); - let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout; let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx); let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx); let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx); @@ -1406,7 +1404,6 @@ impl Workspace { suppressed_notifications: HashSet::default(), left_dock, bottom_dock, - bottom_dock_layout, right_dock, project: project.clone(), follower_states: Default::default(), @@ -1633,10 +1630,6 @@ impl Workspace { &self.bottom_dock } - pub fn bottom_dock_layout(&self) -> BottomDockLayout { - self.bottom_dock_layout - } - pub fn set_bottom_dock_layout( &mut self, layout: BottomDockLayout, @@ -1648,7 +1641,6 @@ impl Workspace { content.bottom_dock_layout = Some(layout); }); - self.bottom_dock_layout = layout; cx.notify(); self.serialize_workspace(window, cx); } @@ -6246,6 +6238,7 @@ impl Render for Workspace { .iter() .map(|(_, notification)| notification.entity_id()) .collect::>(); + let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout; client_side_decorations( self.actions(div(), window, cx) @@ -6369,7 +6362,7 @@ impl Render for Workspace { )) }) .child({ - match self.bottom_dock_layout { + match bottom_dock_layout { BottomDockLayout::Full => div() .flex() .flex_col() From 7be1f2418dc1eefe43e7b6489510e91a7ccd36d0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 29 Jul 2025 20:09:14 -0400 Subject: [PATCH 34/35] Replace `zed_llm_client` with `cloud_llm_client` (#35309) This PR replaces the usage of the `zed_llm_client` with the `cloud_llm_client`. It was ported into this repo in #35307. Release Notes: - N/A --- Cargo.lock | 43 ++++------- Cargo.toml | 1 - crates/agent/Cargo.toml | 4 +- crates/agent/src/thread.rs | 4 +- crates/agent_settings/Cargo.toml | 2 +- crates/agent_settings/src/agent_settings.rs | 6 +- crates/agent_ui/Cargo.toml | 4 +- crates/agent_ui/src/active_thread.rs | 2 +- crates/agent_ui/src/agent_panel.rs | 2 +- crates/agent_ui/src/buffer_codegen.rs | 2 +- crates/agent_ui/src/debug.rs | 2 +- crates/agent_ui/src/message_editor.rs | 10 +-- .../agent_ui/src/terminal_inline_assistant.rs | 2 +- .../agent_ui/src/ui/preview/usage_callouts.rs | 2 +- crates/assistant_context/Cargo.toml | 2 +- .../src/assistant_context.rs | 2 +- crates/assistant_tools/Cargo.toml | 4 +- crates/assistant_tools/src/edit_agent.rs | 2 +- crates/assistant_tools/src/web_search_tool.rs | 2 +- crates/client/Cargo.toml | 10 +-- crates/client/src/user.rs | 8 +- crates/collab/Cargo.toml | 4 +- crates/collab/src/api.rs | 16 ++-- crates/collab/src/api/billing.rs | 2 +- .../src/db/tables/billing_subscription.rs | 2 +- crates/collab/src/llm/db.rs | 2 +- .../collab/src/llm/db/tests/provider_tests.rs | 2 +- crates/collab/src/llm/token.rs | 2 +- crates/collab/src/rpc.rs | 40 +++++----- crates/eval/Cargo.toml | 4 +- crates/eval/src/example.rs | 2 +- crates/git_ui/Cargo.toml | 2 +- crates/git_ui/src/git_panel.rs | 2 +- crates/inline_completion_button/Cargo.toml | 2 +- .../src/inline_completion_button.rs | 2 +- crates/language_model/Cargo.toml | 2 +- crates/language_model/src/language_model.rs | 2 +- crates/language_model/src/request.rs | 7 +- crates/language_models/Cargo.toml | 14 ++-- crates/language_models/src/provider/cloud.rs | 76 ++++++++++--------- .../src/provider/copilot_chat.rs | 2 +- crates/web_search/Cargo.toml | 2 +- crates/web_search/src/web_search.rs | 5 +- crates/web_search_providers/Cargo.toml | 2 +- crates/web_search_providers/src/cloud.rs | 2 +- crates/zeta/Cargo.toml | 4 +- crates/zeta/src/zeta.rs | 8 +- 47 files changed, 157 insertions(+), 169 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 527b99f3c240092a0ef677280fcc694a8b93866a..ff51a57145e1247495bb595aee3dcbd2ad136413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,6 +90,7 @@ dependencies = [ "assistant_tools", "chrono", "client", + "cloud_llm_client", "collections", "component", "context_server", @@ -132,7 +133,6 @@ dependencies = [ "uuid", "workspace", "workspace-hack", - "zed_llm_client", "zstd", ] @@ -189,6 +189,7 @@ name = "agent_settings" version = "0.1.0" dependencies = [ "anyhow", + "cloud_llm_client", "collections", "fs", "gpui", @@ -200,7 +201,6 @@ dependencies = [ "serde_json_lenient", "settings", "workspace-hack", - "zed_llm_client", ] [[package]] @@ -223,6 +223,7 @@ dependencies = [ "buffer_diff", "chrono", "client", + "cloud_llm_client", "collections", "command_palette_hooks", "component", @@ -294,7 +295,6 @@ dependencies = [ "workspace", "workspace-hack", "zed_actions", - "zed_llm_client", ] [[package]] @@ -687,6 +687,7 @@ dependencies = [ "chrono", "client", "clock", + "cloud_llm_client", "collections", "context_server", "fs", @@ -720,7 +721,6 @@ dependencies = [ "uuid", "workspace", "workspace-hack", - "zed_llm_client", ] [[package]] @@ -828,6 +828,7 @@ dependencies = [ "chrono", "client", "clock", + "cloud_llm_client", "collections", "component", "derive_more 0.99.19", @@ -881,7 +882,6 @@ dependencies = [ "which 6.0.3", "workspace", "workspace-hack", - "zed_llm_client", "zlog", ] @@ -2976,6 +2976,7 @@ dependencies = [ "base64 0.22.1", "chrono", "clock", + "cloud_llm_client", "cocoa 0.26.0", "collections", "credentials_provider", @@ -3018,7 +3019,6 @@ dependencies = [ "windows 0.61.1", "workspace-hack", "worktree", - "zed_llm_client", ] [[package]] @@ -3170,6 +3170,7 @@ dependencies = [ "chrono", "client", "clock", + "cloud_llm_client", "collab_ui", "collections", "command_palette_hooks", @@ -3256,7 +3257,6 @@ dependencies = [ "workspace", "workspace-hack", "worktree", - "zed_llm_client", "zlog", ] @@ -5242,6 +5242,7 @@ dependencies = [ "chrono", "clap", "client", + "cloud_llm_client", "collections", "debug_adapter_extension", "dirs 4.0.0", @@ -5281,7 +5282,6 @@ dependencies = [ "uuid", "watch", "workspace-hack", - "zed_llm_client", ] [[package]] @@ -6363,6 +6363,7 @@ dependencies = [ "call", "chrono", "client", + "cloud_llm_client", "collections", "command_palette_hooks", "component", @@ -6405,7 +6406,6 @@ dependencies = [ "workspace", "workspace-hack", "zed_actions", - "zed_llm_client", "zlog", ] @@ -8366,6 +8366,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "cloud_llm_client", "copilot", "editor", "feature_flags", @@ -8388,7 +8389,6 @@ dependencies = [ "workspace", "workspace-hack", "zed_actions", - "zed_llm_client", "zeta", ] @@ -9070,6 +9070,7 @@ dependencies = [ "anyhow", "base64 0.22.1", "client", + "cloud_llm_client", "collections", "futures 0.3.31", "gpui", @@ -9087,7 +9088,6 @@ dependencies = [ "thiserror 2.0.12", "util", "workspace-hack", - "zed_llm_client", ] [[package]] @@ -9103,6 +9103,7 @@ dependencies = [ "bedrock", "chrono", "client", + "cloud_llm_client", "collections", "component", "convert_case 0.8.0", @@ -9144,7 +9145,6 @@ dependencies = [ "vercel", "workspace-hack", "x_ai", - "zed_llm_client", ] [[package]] @@ -18531,11 +18531,11 @@ name = "web_search" version = "0.1.0" dependencies = [ "anyhow", + "cloud_llm_client", "collections", "gpui", "serde", "workspace-hack", - "zed_llm_client", ] [[package]] @@ -18544,6 +18544,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "cloud_llm_client", "futures 0.3.31", "gpui", "http_client", @@ -18552,7 +18553,6 @@ dependencies = [ "serde_json", "web_search", "workspace-hack", - "zed_llm_client", ] [[package]] @@ -20419,19 +20419,6 @@ dependencies = [ "zed_extension_api 0.1.0", ] -[[package]] -name = "zed_llm_client" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc" -dependencies = [ - "anyhow", - "serde", - "serde_json", - "strum 0.27.1", - "uuid", -] - [[package]] name = "zed_proto" version = "0.2.2" @@ -20611,6 +20598,7 @@ dependencies = [ "call", "client", "clock", + "cloud_llm_client", "collections", "command_palette_hooks", "copilot", @@ -20652,7 +20640,6 @@ dependencies = [ "workspace-hack", "worktree", "zed_actions", - "zed_llm_client", "zlog", ] diff --git a/Cargo.toml b/Cargo.toml index e08736e38e76ce3274d5f23b57eb47c90866e601..a6428d897b40e1174b9939676082aea170143f96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -646,7 +646,6 @@ which = "6.0.0" windows-core = "0.61" wit-component = "0.221" workspace-hack = "0.1.0" -zed_llm_client = "= 0.8.6" zstd = "0.11" [workspace.dependencies.async-stripe] diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 135363ab6552a9b6737dfce0e0c95ced3237ae5c..c89a7f3303f812d0387907b209a031035cab9bb1 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -25,6 +25,7 @@ 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 @@ -35,9 +36,9 @@ futures.workspace = true git.workspace = true gpui.workspace = true heed.workspace = true +http_client.workspace = true icons.workspace = true indoc.workspace = true -http_client.workspace = true itertools.workspace = true language.workspace = true language_model.workspace = true @@ -63,7 +64,6 @@ time.workspace = true util.workspace = true uuid.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true zstd.workspace = true [dev-dependencies] diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 1b8aa012a18feda39282fc96987d574e3df38f65..0e5da2d43b29cd6afea4a16bb5944e3dd0f538a5 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -13,6 +13,7 @@ use anyhow::{Result, anyhow}; use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet}; use chrono::{DateTime, Utc}; use client::{ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; use collections::HashMap; use feature_flags::{self, FeatureFlagAppExt}; use futures::{FutureExt, StreamExt as _, future::Shared}; @@ -49,7 +50,6 @@ use std::{ use thiserror::Error; use util::{ResultExt as _, post_inc}; use uuid::Uuid; -use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; const MAX_RETRY_ATTEMPTS: u8 = 4; const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); @@ -1681,7 +1681,7 @@ impl Thread { let completion_mode = request .mode - .unwrap_or(zed_llm_client::CompletionMode::Normal); + .unwrap_or(cloud_llm_client::CompletionMode::Normal); self.last_received_chunk_at = Some(Instant::now()); diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml index 3afe5ae54757953a43a6bdd465c095dc70c27288..d34396a5d35dd8919e519e804a93b50dfe046133 100644 --- a/crates/agent_settings/Cargo.toml +++ b/crates/agent_settings/Cargo.toml @@ -13,6 +13,7 @@ path = "src/agent_settings.rs" [dependencies] anyhow.workspace = true +cloud_llm_client.workspace = true collections.workspace = true gpui.workspace = true language_model.workspace = true @@ -20,7 +21,6 @@ schemars.workspace = true serde.workspace = true settings.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true [dev-dependencies] fs.workspace = true diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 13b966608c096ef7048c442096e55c36b64553b6..4e872c78d7860d0ed53fae6ddb4dc3088f25b635 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -321,11 +321,11 @@ pub enum CompletionMode { Burn, } -impl From for zed_llm_client::CompletionMode { +impl From for cloud_llm_client::CompletionMode { fn from(value: CompletionMode) -> Self { match value { - CompletionMode::Normal => zed_llm_client::CompletionMode::Normal, - CompletionMode::Burn => zed_llm_client::CompletionMode::Max, + CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal, + CompletionMode::Burn => cloud_llm_client::CompletionMode::Max, } } } diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index fbd53e8d09e5d8a74c2fca4d34e36cb95fc58192..95fd2b1757d8f3536986ee415561beccac908a32 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -31,6 +31,7 @@ audio.workspace = true buffer_diff.workspace = true chrono.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true command_palette_hooks.workspace = true component.workspace = true @@ -46,9 +47,9 @@ futures.workspace = true fuzzy.workspace = true gpui.workspace = true html_to_markdown.workspace = true -indoc.workspace = true http_client.workspace = true indexed_docs.workspace = true +indoc.workspace = true inventory.workspace = true itertools.workspace = true jsonschema.workspace = true @@ -97,7 +98,6 @@ watch.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true -zed_llm_client.workspace = true [dev-dependencies] assistant_tools.workspace = true diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e27c3182213dda590a10ade56907afb8c509721f..04a093c7d00f934f47884e595c899a25a686923b 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; use anyhow::Context as _; use assistant_tool::ToolUseStatus; use audio::{Audio, Sound}; +use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; use editor::scroll::Autoscroll; @@ -52,7 +53,6 @@ use util::ResultExt as _; use util::markdown::MarkdownCodeBlock; use workspace::{CollaboratorId, Workspace}; use zed_actions::assistant::OpenRulesLibrary; -use zed_llm_client::CompletionIntent; const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container"; const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1; diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 61a65de50b537a8982b080ae2054ffd0eeaaa706..91217cb0305b28a1c0cdb5cf174bba28c83e4ed0 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -44,6 +44,7 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; use assistant_tool::ToolWorkingSet; use client::{DisableAiSettings, UserStore, zed_urls}; +use cloud_llm_client::{CompletionIntent, UsageLimit}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, FeatureFlagAppExt}; use fs::Fs; @@ -80,7 +81,6 @@ use zed_actions::{ agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; -use zed_llm_client::{CompletionIntent, UsageLimit}; const AGENT_PANEL_KEY: &str = "agent_panel"; diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index 64498e928130d0debfd8a30bdcbcc010c0de48a1..615142b73dfd6eed59f635af780310290e3f6f25 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -6,6 +6,7 @@ use agent::{ use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; +use cloud_llm_client::CompletionIntent; use collections::HashSet; use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint}; use futures::{ @@ -35,7 +36,6 @@ use std::{ }; use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff}; use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; -use zed_llm_client::CompletionIntent; pub struct BufferCodegen { alternatives: Vec>, diff --git a/crates/agent_ui/src/debug.rs b/crates/agent_ui/src/debug.rs index ff6538dc85a45f0072b805b033952da78255f8b7..bd34659210e933ad99357e7e1ceeedb6b53c5ee0 100644 --- a/crates/agent_ui/src/debug.rs +++ b/crates/agent_ui/src/debug.rs @@ -1,10 +1,10 @@ #![allow(unused, dead_code)] use client::{ModelRequestUsage, RequestUsage}; +use cloud_llm_client::{Plan, UsageLimit}; use gpui::Global; use std::ops::{Deref, DerefMut}; use ui::prelude::*; -use zed_llm_client::{Plan, UsageLimit}; /// Debug only: Used for testing various account states /// diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index c160f1de04488b264d9420d2ad78b2d6bbbe3c7c..082d1dfb51005d359fed015bfaad8132e0ddd00d 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -18,6 +18,7 @@ use agent_settings::{AgentSettings, CompletionMode}; use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; use client::UserStore; +use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use editor::actions::{MoveUp, Paste}; use editor::display_map::CreaseId; @@ -53,7 +54,6 @@ use util::ResultExt as _; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::Chat; use zed_actions::agent::ToggleModelSelector; -use zed_llm_client::CompletionIntent; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; @@ -1300,11 +1300,11 @@ impl MessageEditor { let plan = user_store .current_plan() .map(|plan| match plan { - Plan::Free => zed_llm_client::Plan::ZedFree, - Plan::ZedPro => zed_llm_client::Plan::ZedPro, - Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial, + Plan::Free => cloud_llm_client::Plan::ZedFree, + Plan::ZedPro => cloud_llm_client::Plan::ZedPro, + Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, }) - .unwrap_or(zed_llm_client::Plan::ZedFree); + .unwrap_or(cloud_llm_client::Plan::ZedFree); let usage = user_store.model_request_usage()?; diff --git a/crates/agent_ui/src/terminal_inline_assistant.rs b/crates/agent_ui/src/terminal_inline_assistant.rs index 91867957cdcd1b3cb2ff9c40d385737b74d969f1..bcbc308c99da7b80e716fce9e60461352dcb814c 100644 --- a/crates/agent_ui/src/terminal_inline_assistant.rs +++ b/crates/agent_ui/src/terminal_inline_assistant.rs @@ -10,6 +10,7 @@ use agent::{ use agent_settings::AgentSettings; use anyhow::{Context as _, Result}; use client::telemetry::Telemetry; +use cloud_llm_client::CompletionIntent; use collections::{HashMap, VecDeque}; use editor::{MultiBuffer, actions::SelectAll}; use fs::Fs; @@ -27,7 +28,6 @@ use terminal_view::TerminalView; use ui::prelude::*; use util::ResultExt; use workspace::{Toast, Workspace, notifications::NotificationId}; -use zed_llm_client::CompletionIntent; pub fn init( fs: Arc, diff --git a/crates/agent_ui/src/ui/preview/usage_callouts.rs b/crates/agent_ui/src/ui/preview/usage_callouts.rs index 45af41395b52afc8655c7cdd748a3228868b2d0f..64869a6ec71cdbe8e3532983c48784136b3dcb36 100644 --- a/crates/agent_ui/src/ui/preview/usage_callouts.rs +++ b/crates/agent_ui/src/ui/preview/usage_callouts.rs @@ -1,8 +1,8 @@ use client::{ModelRequestUsage, RequestUsage, zed_urls}; +use cloud_llm_client::{Plan, UsageLimit}; use component::{empty_example, example_group_with_title, single_example}; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use ui::{Callout, prelude::*}; -use zed_llm_client::{Plan, UsageLimit}; #[derive(IntoElement, RegisterComponent)] pub struct UsageCallout { diff --git a/crates/assistant_context/Cargo.toml b/crates/assistant_context/Cargo.toml index f35dc43340b98dd8445da9335e27745ec8e35cb8..8f5ff98790f2319c398b7acf04214cd2b3f577f4 100644 --- a/crates/assistant_context/Cargo.toml +++ b/crates/assistant_context/Cargo.toml @@ -19,6 +19,7 @@ assistant_slash_commands.workspace = true chrono.workspace = true client.workspace = true clock.workspace = true +cloud_llm_client.workspace = true collections.workspace = true context_server.workspace = true fs.workspace = true @@ -48,7 +49,6 @@ util.workspace = true uuid.workspace = true workspace-hack.workspace = true workspace.workspace = true -zed_llm_client.workspace = true [dev-dependencies] indoc.workspace = true diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index 136468e084593ef6b6475d29d8526d683b1bdc7b..4518bbff79f7651f76144827381c3de4865ccd06 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -11,6 +11,7 @@ use assistant_slash_command::{ use assistant_slash_commands::FileCommandMetadata; use client::{self, Client, proto, telemetry::Telemetry}; use clock::ReplicaId; +use cloud_llm_client::CompletionIntent; use collections::{HashMap, HashSet}; use fs::{Fs, RenameOptions}; use futures::{FutureExt, StreamExt, future::Shared}; @@ -46,7 +47,6 @@ use text::{BufferSnapshot, ToPoint}; use ui::IconName; use util::{ResultExt, TryFutureExt, post_inc}; use uuid::Uuid; -use zed_llm_client::CompletionIntent; pub use crate::context_store::*; diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 146800e094a25baf3901134bdd5a7fb4f9330214..d4b8fa3afc3dc3311599a2d9e3e97f2984ebde40 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -21,9 +21,11 @@ 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 @@ -63,8 +65,6 @@ web_search.workspace = true which.workspace = true workspace-hack.workspace = true workspace.workspace = true -zed_llm_client.workspace = true -diffy = "0.4.2" [dev-dependencies] lsp = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_tools/src/edit_agent.rs b/crates/assistant_tools/src/edit_agent.rs index 0184dff36c0a4130ce2880f3e7e84acb013aadfd..fed79434bb0a0b7e7ce43093fe9968affa02e462 100644 --- a/crates/assistant_tools/src/edit_agent.rs +++ b/crates/assistant_tools/src/edit_agent.rs @@ -7,6 +7,7 @@ mod streaming_fuzzy_matcher; use crate::{Template, Templates}; use anyhow::Result; use assistant_tool::ActionLog; +use cloud_llm_client::CompletionIntent; use create_file_parser::{CreateFileParser, CreateFileParserEvent}; pub use edit_parser::EditFormat; use edit_parser::{EditParser, EditParserEvent, EditParserMetrics}; @@ -29,7 +30,6 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task:: use streaming_diff::{CharOperation, StreamingDiff}; use streaming_fuzzy_matcher::StreamingFuzzyMatcher; use util::debug_panic; -use zed_llm_client::CompletionIntent; #[derive(Serialize)] struct CreateFilePromptTemplate { diff --git a/crates/assistant_tools/src/web_search_tool.rs b/crates/assistant_tools/src/web_search_tool.rs index 5eeca9c2c44fa6d2d27bc2e95f3e03faca387cc8..d4a12f22c56796c5dfbb2f1935e2ad7646ce7c5a 100644 --- a/crates/assistant_tools/src/web_search_tool.rs +++ b/crates/assistant_tools/src/web_search_tool.rs @@ -6,6 +6,7 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{ ActionLog, 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, @@ -17,7 +18,6 @@ use serde::{Deserialize, Serialize}; use ui::{IconName, Tooltip, prelude::*}; use web_search::WebSearchRegistry; use workspace::Workspace; -use zed_llm_client::{WebSearchResponse, WebSearchResult}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct WebSearchToolInput { diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index b741f515fd1681a721048d831d01db8ec0f889e6..dd97bd9ca45f7bc41d2fe987223321be474a0efc 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua base64.workspace = true chrono = { workspace = true, features = ["serde"] } clock.workspace = true +cloud_llm_client.workspace = true collections.workspace = true credentials_provider.workspace = true derive_more.workspace = true @@ -33,8 +34,8 @@ http_client.workspace = true http_client_tls.workspace = true httparse = "1.10" log.workspace = true -paths.workspace = true parking_lot.workspace = true +paths.workspace = true postage.workspace = true rand.workspace = true regex.workspace = true @@ -46,19 +47,18 @@ serde_json.workspace = true settings.workspace = true sha2.workspace = true smol.workspace = true +telemetry.workspace = true telemetry_events.workspace = true text.workspace = true thiserror.workspace = true time.workspace = true tiny_http.workspace = true tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] } +tokio.workspace = true url.workspace = true util.workspace = true -worktree.workspace = true -telemetry.workspace = true -tokio.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true +worktree.workspace = true [dev-dependencies] clock = { workspace = true, features = ["test-support"] } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 5ed258aa8ed55e7124ef06c5e769c3ccc45c0618..a7dab2a8d35c09cb736d263ecb4afa84442fb6ff 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,6 +1,10 @@ use super::{Client, Status, TypedEnvelope, proto}; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; +use cloud_llm_client::{ + EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME, + MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit, +}; use collections::{HashMap, HashSet, hash_map::Entry}; use derive_more::Deref; use feature_flags::FeatureFlagAppExt; @@ -17,10 +21,6 @@ use std::{ }; use text::ReplicaId; use util::{TryFutureExt as _, maybe}; -use zed_llm_client::{ - EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME, - MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit, -}; pub type UserId = u64; diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index d3b504828350e8cf6be5b44f1b0e1a5361006eb7..9af95317e60db78fc93b9a1fa01eaee687fac4fc 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -23,13 +23,14 @@ async-stripe.workspace = true async-trait.workspace = true async-tungstenite.workspace = true aws-config = { version = "1.1.5" } -aws-sdk-s3 = { version = "1.15.0" } aws-sdk-kinesis = "1.51.0" +aws-sdk-s3 = { version = "1.15.0" } axum = { version = "0.6", features = ["json", "headers", "ws"] } axum-extra = { version = "0.4", features = ["erased-json"] } base64.workspace = true chrono.workspace = true clock.workspace = true +cloud_llm_client.workspace = true collections.workspace = true dashmap.workspace = true derive_more.workspace = true @@ -75,7 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re util.workspace = true uuid.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true [dev-dependencies] agent_settings.workspace = true diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 050a15dd4e549426fbc9dd76a7688df1e5b476c9..6cf3f68f54eda75ac19950c53cf535ff30a107a9 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -310,9 +310,9 @@ async fn refresh_llm_tokens( #[derive(Debug, Serialize, Deserialize)] struct UpdatePlanBody { - pub plan: zed_llm_client::Plan, + pub plan: cloud_llm_client::Plan, pub subscription_period: SubscriptionPeriod, - pub usage: zed_llm_client::CurrentUsage, + pub usage: cloud_llm_client::CurrentUsage, pub trial_started_at: Option>, pub is_usage_based_billing_enabled: bool, pub is_account_too_young: bool, @@ -334,9 +334,9 @@ async fn update_plan( extract::Json(body): extract::Json, ) -> Result> { let plan = match body.plan { - zed_llm_client::Plan::ZedFree => proto::Plan::Free, - zed_llm_client::Plan::ZedPro => proto::Plan::ZedPro, - zed_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial, + cloud_llm_client::Plan::ZedFree => proto::Plan::Free, + cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro, + cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial, }; let update_user_plan = proto::UpdateUserPlan { @@ -368,15 +368,15 @@ async fn update_plan( Ok(Json(UpdatePlanResponse {})) } -fn usage_limit_to_proto(limit: zed_llm_client::UsageLimit) -> proto::UsageLimit { +fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit { proto::UsageLimit { variant: Some(match limit { - zed_llm_client::UsageLimit::Limited(limit) => { + cloud_llm_client::UsageLimit::Limited(limit) => { proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { limit: limit as u32, }) } - zed_llm_client::UsageLimit::Unlimited => { + cloud_llm_client::UsageLimit::Unlimited => { proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) } }), diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 9d0c617ab9268ad5c8e86899a81fb35d50f90457..0e15308ffea7d6e11160ee05843fa9207f234251 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,11 +1,11 @@ use anyhow::{Context as _, bail}; use chrono::{DateTime, Utc}; +use cloud_llm_client::LanguageModelProvider; use collections::{HashMap, HashSet}; use sea_orm::ActiveValue; use std::{sync::Arc, time::Duration}; use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus}; use util::{ResultExt, maybe}; -use zed_llm_client::LanguageModelProvider; use crate::AppState; use crate::db::billing_subscription::{ diff --git a/crates/collab/src/db/tables/billing_subscription.rs b/crates/collab/src/db/tables/billing_subscription.rs index 43198f9859f004f18e944b1ccb591bbbaa6ca69b..522973dbc970b69947b8e790e370bfc9fa93aa99 100644 --- a/crates/collab/src/db/tables/billing_subscription.rs +++ b/crates/collab/src/db/tables/billing_subscription.rs @@ -95,7 +95,7 @@ pub enum SubscriptionKind { ZedFree, } -impl From for zed_llm_client::Plan { +impl From for cloud_llm_client::Plan { fn from(value: SubscriptionKind) -> Self { match value { SubscriptionKind::ZedPro => Self::ZedPro, diff --git a/crates/collab/src/llm/db.rs b/crates/collab/src/llm/db.rs index 6a6efca0de1aea2fb979572400b19ce4094fcfcd..18ad624dab840c47df766a55c2f59cf9a17c55e6 100644 --- a/crates/collab/src/llm/db.rs +++ b/crates/collab/src/llm/db.rs @@ -6,11 +6,11 @@ mod tables; #[cfg(test)] mod tests; +use cloud_llm_client::LanguageModelProvider; use collections::HashMap; pub use ids::*; pub use seed::*; pub use tables::*; -use zed_llm_client::LanguageModelProvider; #[cfg(test)] pub use tests::TestLlmDb; diff --git a/crates/collab/src/llm/db/tests/provider_tests.rs b/crates/collab/src/llm/db/tests/provider_tests.rs index 7d52964b939e7b17ca8ec9f986756c00bd0dad55..f4e1de40ec10705ed9b740619754fcf9ec5f3e1e 100644 --- a/crates/collab/src/llm/db/tests/provider_tests.rs +++ b/crates/collab/src/llm/db/tests/provider_tests.rs @@ -1,5 +1,5 @@ +use cloud_llm_client::LanguageModelProvider; use pretty_assertions::assert_eq; -use zed_llm_client::LanguageModelProvider; use crate::llm::db::LlmDatabase; use crate::test_llm_db; diff --git a/crates/collab/src/llm/token.rs b/crates/collab/src/llm/token.rs index d4566ffcb40715c11a6a714b105238f47f333969..da01c7f3bed5cab1e7dbd6cfdef8cd4d7643044c 100644 --- a/crates/collab/src/llm/token.rs +++ b/crates/collab/src/llm/token.rs @@ -4,12 +4,12 @@ use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEA use crate::{Config, db::billing_preference}; use anyhow::{Context as _, Result}; use chrono::{NaiveDateTime, Utc}; +use cloud_llm_client::Plan; use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use std::time::Duration; use thiserror::Error; use uuid::Uuid; -use zed_llm_client::Plan; #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index b7e5ce0739c35c8e9ad4dde1816ade6ec153200a..5c35394e1d6660e75aea1f4a12cec49741cbd646 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2868,12 +2868,12 @@ async fn make_update_user_plan_message( } fn model_requests_limit( - plan: zed_llm_client::Plan, + plan: cloud_llm_client::Plan, feature_flags: &Vec, -) -> zed_llm_client::UsageLimit { +) -> cloud_llm_client::UsageLimit { match plan.model_requests_limit() { - zed_llm_client::UsageLimit::Limited(limit) => { - let limit = if plan == zed_llm_client::Plan::ZedProTrial + cloud_llm_client::UsageLimit::Limited(limit) => { + let limit = if plan == cloud_llm_client::Plan::ZedProTrial && feature_flags .iter() .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG) @@ -2883,9 +2883,9 @@ fn model_requests_limit( limit }; - zed_llm_client::UsageLimit::Limited(limit) + cloud_llm_client::UsageLimit::Limited(limit) } - zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited, + cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited, } } @@ -2895,21 +2895,21 @@ fn subscription_usage_to_proto( feature_flags: &Vec, ) -> proto::SubscriptionUsage { let plan = match plan { - proto::Plan::Free => zed_llm_client::Plan::ZedFree, - proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro, - proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial, + proto::Plan::Free => cloud_llm_client::Plan::ZedFree, + proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro, + proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, }; proto::SubscriptionUsage { model_requests_usage_amount: usage.model_requests as u32, model_requests_usage_limit: Some(proto::UsageLimit { variant: Some(match model_requests_limit(plan, feature_flags) { - zed_llm_client::UsageLimit::Limited(limit) => { + cloud_llm_client::UsageLimit::Limited(limit) => { proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { limit: limit as u32, }) } - zed_llm_client::UsageLimit::Unlimited => { + cloud_llm_client::UsageLimit::Unlimited => { proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) } }), @@ -2917,12 +2917,12 @@ fn subscription_usage_to_proto( edit_predictions_usage_amount: usage.edit_predictions as u32, edit_predictions_usage_limit: Some(proto::UsageLimit { variant: Some(match plan.edit_predictions_limit() { - zed_llm_client::UsageLimit::Limited(limit) => { + cloud_llm_client::UsageLimit::Limited(limit) => { proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { limit: limit as u32, }) } - zed_llm_client::UsageLimit::Unlimited => { + cloud_llm_client::UsageLimit::Unlimited => { proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) } }), @@ -2935,21 +2935,21 @@ fn make_default_subscription_usage( feature_flags: &Vec, ) -> proto::SubscriptionUsage { let plan = match plan { - proto::Plan::Free => zed_llm_client::Plan::ZedFree, - proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro, - proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial, + proto::Plan::Free => cloud_llm_client::Plan::ZedFree, + proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro, + proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial, }; proto::SubscriptionUsage { model_requests_usage_amount: 0, model_requests_usage_limit: Some(proto::UsageLimit { variant: Some(match model_requests_limit(plan, feature_flags) { - zed_llm_client::UsageLimit::Limited(limit) => { + cloud_llm_client::UsageLimit::Limited(limit) => { proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { limit: limit as u32, }) } - zed_llm_client::UsageLimit::Unlimited => { + cloud_llm_client::UsageLimit::Unlimited => { proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) } }), @@ -2957,12 +2957,12 @@ fn make_default_subscription_usage( edit_predictions_usage_amount: 0, edit_predictions_usage_limit: Some(proto::UsageLimit { variant: Some(match plan.edit_predictions_limit() { - zed_llm_client::UsageLimit::Limited(limit) => { + cloud_llm_client::UsageLimit::Limited(limit) => { proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { limit: limit as u32, }) } - zed_llm_client::UsageLimit::Unlimited => { + cloud_llm_client::UsageLimit::Unlimited => { proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) } }), diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index d5db7f71a4593a66ee8218c053109041035428ab..a0214c76a1c7230e071cbc65c1eadbc44c7d6ca8 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -19,8 +19,8 @@ path = "src/explorer.rs" [dependencies] agent.workspace = true -agent_ui.workspace = true agent_settings.workspace = true +agent_ui.workspace = true anyhow.workspace = true assistant_tool.workspace = true assistant_tools.workspace = true @@ -29,6 +29,7 @@ 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 @@ -68,4 +69,3 @@ util.workspace = true uuid.workspace = true watch.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 7ce3b1fdf101e8f5c792f62e15b272b38477b2cf..23c8814916da2df4016c4196d7767b748da54280 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -15,11 +15,11 @@ 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 gpui::{App, AppContext, AsyncApp, Entity}; use language_model::{LanguageModel, Role, StopReason}; -use zed_llm_client::CompletionIntent; pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2); diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 2fb80b7e7366a103fc3012041f5b35952819547d..4c919249ee01c330eb04c46fb39e9b91667d92a8 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -24,6 +24,7 @@ buffer_diff.workspace = true call.workspace = true chrono.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true command_palette_hooks.workspace = true component.workspace = true @@ -62,7 +63,6 @@ watch.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true -zed_llm_client.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index f7efada469c525a6de17568d9acebbf6332345ee..e196a5b1396ac4f734e6fd05a0ab7c08ec704f45 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -71,12 +71,12 @@ use ui::{ use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; +use cloud_llm_client::CompletionIntent; use workspace::{ Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId}, }; -use zed_llm_client::CompletionIntent; actions!( git_panel, diff --git a/crates/inline_completion_button/Cargo.toml b/crates/inline_completion_button/Cargo.toml index c2a619d50075271be23aec9aa71dc554cf8075c0..b34e59336bb1a93beac06c09ffd206ba3ea38aea 100644 --- a/crates/inline_completion_button/Cargo.toml +++ b/crates/inline_completion_button/Cargo.toml @@ -15,6 +15,7 @@ doctest = false [dependencies] anyhow.workspace = true client.workspace = true +cloud_llm_client.workspace = true copilot.workspace = true editor.workspace = true feature_flags.workspace = true @@ -32,7 +33,6 @@ ui.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true -zed_llm_client.workspace = true zeta.workspace = true [dev-dependencies] diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 2615a8beef7bdcd4e458a215c70da657070d9311..81d9181cfc368628e4cf6e50eb0d14843d8ec882 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -1,5 +1,6 @@ use anyhow::Result; use client::{DisableAiSettings, UserStore, zed_urls}; +use cloud_llm_client::UsageLimit; use copilot::{Copilot, Status}; use editor::{ Editor, SelectionEffects, @@ -34,7 +35,6 @@ use workspace::{ notifications::NotificationId, }; use zed_actions::OpenBrowser; -use zed_llm_client::UsageLimit; use zeta::RateCompletions; actions!( diff --git a/crates/language_model/Cargo.toml b/crates/language_model/Cargo.toml index b718c530f5cd59f25593fb3c5261bdc706839223..841be60b0ea37312e01130fc2b01dd19d1845e10 100644 --- a/crates/language_model/Cargo.toml +++ b/crates/language_model/Cargo.toml @@ -20,6 +20,7 @@ anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true base64.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true futures.workspace = true gpui.workspace = true @@ -37,7 +38,6 @@ telemetry_events.workspace = true thiserror.workspace = true util.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true [dev-dependencies] gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 54640419b674e8680e5b266b5f6eaf2a3d365f76..1637d2de8a3c14b910ea345c03a4eb5db13df28d 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -11,6 +11,7 @@ pub mod fake_provider; use anthropic::{AnthropicError, parse_prompt_too_long}; use anyhow::{Result, anyhow}; use client::Client; +use cloud_llm_client::{CompletionMode, CompletionRequestStatus}; use futures::FutureExt; use futures::{StreamExt, future::BoxFuture, stream::BoxStream}; use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window}; @@ -26,7 +27,6 @@ use std::time::Duration; use std::{fmt, io}; use thiserror::Error; use util::serde::is_default; -use zed_llm_client::{CompletionMode, CompletionRequestStatus}; pub use crate::model::*; pub use crate::rate_limiter::*; diff --git a/crates/language_model/src/request.rs b/crates/language_model/src/request.rs index 6f3d420ad5ac1304daf1f3341b2fb05da8662a18..dc485e9937f7e6579f99212609e6f2383c11ae6c 100644 --- a/crates/language_model/src/request.rs +++ b/crates/language_model/src/request.rs @@ -1,10 +1,9 @@ use std::io::{Cursor, Write}; use std::sync::Arc; -use crate::role::Role; -use crate::{LanguageModelToolUse, LanguageModelToolUseId}; use anyhow::Result; use base64::write::EncoderWriter; +use cloud_llm_client::{CompletionIntent, CompletionMode}; use gpui::{ App, AppContext as _, DevicePixels, Image, ImageFormat, ObjectFit, SharedString, Size, Task, point, px, size, @@ -12,7 +11,9 @@ use gpui::{ use image::codecs::png::PngEncoder; use serde::{Deserialize, Serialize}; use util::ResultExt; -use zed_llm_client::{CompletionIntent, CompletionMode}; + +use crate::role::Role; +use crate::{LanguageModelToolUse, LanguageModelToolUseId}; #[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] pub struct LanguageModelImage { diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 574579aaa762248133c3027fac70efe255ac6997..208b0d99c91d56e4d60c6040118b24dfd4d27e0d 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -16,18 +16,17 @@ ai_onboarding.workspace = true anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true aws-config = { workspace = true, features = ["behavior-version-latest"] } -aws-credential-types = { workspace = true, features = [ - "hardcoded-credentials", -] } +aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] } aws_http_client.workspace = true bedrock.workspace = true chrono.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true component.workspace = true -credentials_provider.workspace = true convert_case.workspace = true copilot.workspace = true +credentials_provider.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true futures.workspace = true @@ -35,6 +34,7 @@ google_ai = { workspace = true, features = ["schemars"] } gpui.workspace = true gpui_tokio.workspace = true http_client.workspace = true +language.workspace = true language_model.workspace = true lmstudio = { workspace = true, features = ["schemars"] } log.workspace = true @@ -43,8 +43,6 @@ mistral = { workspace = true, features = ["schemars"] } ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } -vercel = { workspace = true, features = ["schemars"] } -x_ai = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true proto.workspace = true release_channel.workspace = true @@ -61,9 +59,9 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } ui.workspace = true ui_input.workspace = true util.workspace = true +vercel = { workspace = true, features = ["schemars"] } workspace-hack.workspace = true -zed_llm_client.workspace = true -language.workspace = true +x_ai = { workspace = true, features = ["schemars"] } [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 1e6e7b96d00ce240a2919801ac8adb7ccce57142..3de135c5a2b7622a37f28ddf5ed9d74fec29e321 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -3,6 +3,13 @@ use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, ModelRequestUsage, UserStore, zed_urls}; +use cloud_llm_client::{ + CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, + CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, + EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, + SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, + TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME, +}; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream, }; @@ -33,13 +40,6 @@ use std::time::Duration; use thiserror::Error; use ui::{TintColor, prelude::*}; use util::{ResultExt as _, maybe}; -use zed_llm_client::{ - CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody, - CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse, - EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, - SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME, - TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME, -}; use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, into_anthropic}; use crate::provider::google::{GoogleEventMapper, into_google}; @@ -120,10 +120,10 @@ pub struct State { user_store: Entity, status: client::Status, accept_terms_of_service_task: Option>>, - models: Vec>, - default_model: Option>, - default_fast_model: Option>, - recommended_models: Vec>, + models: Vec>, + default_model: Option>, + default_fast_model: Option>, + recommended_models: Vec>, _fetch_models_task: Task<()>, _settings_subscription: Subscription, _llm_token_subscription: Subscription, @@ -238,8 +238,8 @@ impl State { // Right now we represent thinking variants of models as separate models on the client, // so we need to insert variants for any model that supports thinking. if model.supports_thinking { - models.push(Arc::new(zed_llm_client::LanguageModel { - id: zed_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()), + models.push(Arc::new(cloud_llm_client::LanguageModel { + id: cloud_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()), display_name: format!("{} Thinking", model.display_name), ..model })); @@ -328,7 +328,7 @@ impl CloudLanguageModelProvider { fn create_language_model( &self, - model: Arc, + model: Arc, llm_api_token: LlmApiToken, ) -> Arc { Arc::new(CloudLanguageModel { @@ -518,7 +518,7 @@ fn render_accept_terms( pub struct CloudLanguageModel { id: LanguageModelId, - model: Arc, + model: Arc, llm_api_token: LlmApiToken, client: Arc, request_limiter: RateLimiter, @@ -611,12 +611,12 @@ impl CloudLanguageModel { .headers() .get(CURRENT_PLAN_HEADER_NAME) .and_then(|plan| plan.to_str().ok()) - .and_then(|plan| zed_llm_client::Plan::from_str(plan).ok()) + .and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok()) { let plan = match plan { - zed_llm_client::Plan::ZedFree => Plan::Free, - zed_llm_client::Plan::ZedPro => Plan::ZedPro, - zed_llm_client::Plan::ZedProTrial => Plan::ZedProTrial, + cloud_llm_client::Plan::ZedFree => Plan::Free, + cloud_llm_client::Plan::ZedPro => Plan::ZedPro, + cloud_llm_client::Plan::ZedProTrial => Plan::ZedProTrial, }; return Err(anyhow!(ModelRequestLimitReachedError { plan })); } @@ -729,7 +729,7 @@ impl LanguageModel for CloudLanguageModel { } fn upstream_provider_id(&self) -> LanguageModelProviderId { - use zed_llm_client::LanguageModelProvider::*; + use cloud_llm_client::LanguageModelProvider::*; match self.model.provider { Anthropic => language_model::ANTHROPIC_PROVIDER_ID, OpenAi => language_model::OPEN_AI_PROVIDER_ID, @@ -738,7 +738,7 @@ impl LanguageModel for CloudLanguageModel { } fn upstream_provider_name(&self) -> LanguageModelProviderName { - use zed_llm_client::LanguageModelProvider::*; + use cloud_llm_client::LanguageModelProvider::*; match self.model.provider { Anthropic => language_model::ANTHROPIC_PROVIDER_NAME, OpenAi => language_model::OPEN_AI_PROVIDER_NAME, @@ -772,11 +772,11 @@ impl LanguageModel for CloudLanguageModel { fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { match self.model.provider { - zed_llm_client::LanguageModelProvider::Anthropic - | zed_llm_client::LanguageModelProvider::OpenAi => { + cloud_llm_client::LanguageModelProvider::Anthropic + | cloud_llm_client::LanguageModelProvider::OpenAi => { LanguageModelToolSchemaFormat::JsonSchema } - zed_llm_client::LanguageModelProvider::Google => { + cloud_llm_client::LanguageModelProvider::Google => { LanguageModelToolSchemaFormat::JsonSchemaSubset } } @@ -795,15 +795,15 @@ impl LanguageModel for CloudLanguageModel { fn cache_configuration(&self) -> Option { match &self.model.provider { - zed_llm_client::LanguageModelProvider::Anthropic => { + cloud_llm_client::LanguageModelProvider::Anthropic => { Some(LanguageModelCacheConfiguration { min_total_token: 2_048, should_speculate: true, max_cache_anchors: 4, }) } - zed_llm_client::LanguageModelProvider::OpenAi - | zed_llm_client::LanguageModelProvider::Google => None, + cloud_llm_client::LanguageModelProvider::OpenAi + | cloud_llm_client::LanguageModelProvider::Google => None, } } @@ -813,15 +813,17 @@ impl LanguageModel for CloudLanguageModel { cx: &App, ) -> BoxFuture<'static, Result> { match self.model.provider { - zed_llm_client::LanguageModelProvider::Anthropic => count_anthropic_tokens(request, cx), - zed_llm_client::LanguageModelProvider::OpenAi => { + cloud_llm_client::LanguageModelProvider::Anthropic => { + count_anthropic_tokens(request, cx) + } + cloud_llm_client::LanguageModelProvider::OpenAi => { let model = match open_ai::Model::from_id(&self.model.id.0) { Ok(model) => model, Err(err) => return async move { Err(anyhow!(err)) }.boxed(), }; count_open_ai_tokens(request, model, cx) } - zed_llm_client::LanguageModelProvider::Google => { + cloud_llm_client::LanguageModelProvider::Google => { let client = self.client.clone(); let llm_api_token = self.llm_api_token.clone(); let model_id = self.model.id.to_string(); @@ -832,7 +834,7 @@ impl LanguageModel for CloudLanguageModel { let token = llm_api_token.acquire(&client).await?; let request_body = CountTokensBody { - provider: zed_llm_client::LanguageModelProvider::Google, + provider: cloud_llm_client::LanguageModelProvider::Google, model: model_id, provider_request: serde_json::to_value(&google_ai::CountTokensRequest { generate_content_request, @@ -893,7 +895,7 @@ impl LanguageModel for CloudLanguageModel { let app_version = cx.update(|cx| AppVersion::global(cx)).ok(); let thinking_allowed = request.thinking_allowed; match self.model.provider { - zed_llm_client::LanguageModelProvider::Anthropic => { + cloud_llm_client::LanguageModelProvider::Anthropic => { let request = into_anthropic( request, self.model.id.to_string(), @@ -924,7 +926,7 @@ impl LanguageModel for CloudLanguageModel { prompt_id, intent, mode, - provider: zed_llm_client::LanguageModelProvider::Anthropic, + provider: cloud_llm_client::LanguageModelProvider::Anthropic, model: request.model.clone(), provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, @@ -948,7 +950,7 @@ impl LanguageModel for CloudLanguageModel { }); async move { Ok(future.await?.boxed()) }.boxed() } - zed_llm_client::LanguageModelProvider::OpenAi => { + cloud_llm_client::LanguageModelProvider::OpenAi => { let client = self.client.clone(); let model = match open_ai::Model::from_id(&self.model.id.0) { Ok(model) => model, @@ -976,7 +978,7 @@ impl LanguageModel for CloudLanguageModel { prompt_id, intent, mode, - provider: zed_llm_client::LanguageModelProvider::OpenAi, + provider: cloud_llm_client::LanguageModelProvider::OpenAi, model: request.model.clone(), provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, @@ -996,7 +998,7 @@ impl LanguageModel for CloudLanguageModel { }); async move { Ok(future.await?.boxed()) }.boxed() } - zed_llm_client::LanguageModelProvider::Google => { + cloud_llm_client::LanguageModelProvider::Google => { let client = self.client.clone(); let request = into_google(request, self.model.id.to_string(), GoogleModelMode::Default); @@ -1016,7 +1018,7 @@ impl LanguageModel for CloudLanguageModel { prompt_id, intent, mode, - provider: zed_llm_client::LanguageModelProvider::Google, + provider: cloud_llm_client::LanguageModelProvider::Google, model: request.model.model_id.clone(), provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index d9a84f1eb74465a0d5e72591d450802d5708cb20..3cdc2e54014aa5080e6871a8f7d470abcd85fb0e 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -3,6 +3,7 @@ use std::str::FromStr as _; use std::sync::Arc; use anyhow::{Result, anyhow}; +use cloud_llm_client::CompletionIntent; use collections::HashMap; use copilot::copilot_chat::{ ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat, ImageUrl, @@ -30,7 +31,6 @@ use settings::SettingsStore; use std::time::Duration; use ui::prelude::*; use util::debug_panic; -use zed_llm_client::CompletionIntent; use super::anthropic::count_anthropic_tokens; use super::google::count_google_tokens; diff --git a/crates/web_search/Cargo.toml b/crates/web_search/Cargo.toml index e5b8ca63b25ef5a0eb030a58198492b12ff68470..4ba46faec4362ac98fffaffb6c606608c02373e8 100644 --- a/crates/web_search/Cargo.toml +++ b/crates/web_search/Cargo.toml @@ -13,8 +13,8 @@ path = "src/web_search.rs" [dependencies] anyhow.workspace = true +cloud_llm_client.workspace = true collections.workspace = true gpui.workspace = true serde.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true diff --git a/crates/web_search/src/web_search.rs b/crates/web_search/src/web_search.rs index a131b0de7166709679a8ebc8e7d64af35118eb22..8578cfe4aaab77fdc731a8dd49c62c5afd514600 100644 --- a/crates/web_search/src/web_search.rs +++ b/crates/web_search/src/web_search.rs @@ -1,8 +1,9 @@ +use std::sync::Arc; + use anyhow::Result; +use cloud_llm_client::WebSearchResponse; use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task}; -use std::sync::Arc; -use zed_llm_client::WebSearchResponse; pub fn init(cx: &mut App) { let registry = cx.new(|_cx| WebSearchRegistry::default()); diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml index 2e052796c48601565e5e6870f9848f8dcd9354b1..f7a248d10649dc83d7d76b454e8db2d37b55cbef 100644 --- a/crates/web_search_providers/Cargo.toml +++ b/crates/web_search_providers/Cargo.toml @@ -14,6 +14,7 @@ path = "src/web_search_providers.rs" [dependencies] anyhow.workspace = true client.workspace = true +cloud_llm_client.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true @@ -22,4 +23,3 @@ serde.workspace = true serde_json.workspace = true web_search.workspace = true workspace-hack.workspace = true -zed_llm_client.workspace = true diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index adf79b0ff68c4d569dbf7cd40951c7c6c9761583..52ee0da0d46287c78164d4ff6cc3eb31e46167b1 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -2,12 +2,12 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; use client::Client; +use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse}; use futures::AsyncReadExt as _; use gpui::{App, AppContext, Context, Entity, Subscription, Task}; use http_client::{HttpClient, Method}; use language_model::{LlmApiToken, RefreshLlmTokenListener}; use web_search::{WebSearchProvider, WebSearchProviderId}; -use zed_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse}; pub struct CloudWebSearchProvider { state: Entity, diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index c2b1de08aea4d096dea25b50d54077306489482d..294d95aefde60984a982eab177e4070681947be6 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -21,6 +21,7 @@ ai_onboarding.workspace = true anyhow.workspace = true arrayvec.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true command_palette_hooks.workspace = true copilot.workspace = true @@ -52,11 +53,10 @@ 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 -zed_llm_client.workspace = true -workspace-hack.workspace = true [dev-dependencies] collections = { workspace = true, features = ["test-support"] } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index d6f033899de8443fa736ec92774b9363e6da459b..d5c6be278b761af299db50132633b3832772c5ca 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -17,6 +17,10 @@ pub use rate_completion_modal::*; use anyhow::{Context as _, Result, anyhow}; use arrayvec::ArrayVec; use client::{Client, EditPredictionUsage, UserStore}; +use cloud_llm_client::{ + AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, + PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, +}; use collections::{HashMap, HashSet, VecDeque}; use futures::AsyncReadExt; use gpui::{ @@ -53,10 +57,6 @@ use uuid::Uuid; use workspace::Workspace; use workspace::notifications::{ErrorMessagePrompt, NotificationId}; use worktree::Worktree; -use zed_llm_client::{ - AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, - PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME, -}; const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>"; const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>"; From 49b75e9e93c6d273b86edd0acface7927c68f8d5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 30 Jul 2025 12:18:26 +0300 Subject: [PATCH 35/35] Kb/wasm panics (#35319) Follow-up of https://github.com/zed-industries/zed/pull/34208 Closes https://github.com/zed-industries/zed/issues/35185 Previous code assumed that extensions' language server wrappers may leak only in static data (e.g. fields that were not cleared on deinit), but we seem to have a race that breaks this assumption. 1. We do clean `all_lsp_adapters` field after https://github.com/zed-industries/zed/pull/34334 and it's called for every extension that is unregistered. 2. `LspStore::maintain_workspace_config` -> `LspStore::refresh_workspace_configurations` chain is triggered independently, apparently on `ToolchainStoreEvent::ToolchainActivated` event which means somewhere behind there's potentially a Python code that gets executed to activate the toolchian, making `refresh_workspace_configurations` start timings unpredictable. 3. Seems that toolchain activation overlaps with plugin reload, as `2025-07-28T12:16:19+03:00 INFO [extension_host] extensions updated. loading 0, reloading 1, unloading 0` suggests in the issue logs. The plugin reload seem to happen faster than workspace configuration refresh in https://github.com/zed-industries/zed/blob/c65da547c9aa5d798a1a71468bf253bf55d1cb09/crates/project/src/lsp_store.rs#L7426-L7456 as the language servers are just starting and take extra time to respond to the notification. At least one of the `.clone()`d `adapter`s there is the adapter that got removed during plugin reload and has its channel closed, which causes a panic later. ---------------------------- A good fix would be to re-architect the workspace refresh approach, same as other accesses to the language server collections. One way could be to use `Weak`-based structures instead, as definitely the extension server data belongs to extension, not the `LspStore`. This is quite a large undertaking near the extension core though, so is not done yet. Currently, to stop the excessive panics, no more `.expect` is done on the channel result, as indeed, it now can be closed very dynamically. This will result in more errors (and backtraces, presumably) printed in the logs and no panics. More logging and comments are added, and workspace querying is replaced to the concurrent one: no need to wait until a previous server had processed the notification to send the same to the next one. Release Notes: - Fixed warm-related panic happening during startup --- crates/extension_host/src/wasm_host.rs | 51 +++++++++--------- crates/project/src/lsp_store.rs | 73 +++++++++++++++----------- 2 files changed, 69 insertions(+), 55 deletions(-) diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index d909d06f6b93c5070e6f8fca966778ecefa9f35d..d990b670f49221aca2f0af901293c70d341cf029 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -106,7 +106,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn language_server_initialization_options( @@ -131,7 +131,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn language_server_workspace_configuration( @@ -154,7 +154,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn language_server_additional_initialization_options( @@ -179,7 +179,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn language_server_additional_workspace_configuration( @@ -204,7 +204,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn labels_for_completions( @@ -230,7 +230,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn labels_for_symbols( @@ -256,7 +256,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn complete_slash_command_argument( @@ -275,7 +275,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn run_slash_command( @@ -301,7 +301,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn context_server_command( @@ -320,7 +320,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn context_server_configuration( @@ -347,7 +347,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn suggest_docs_packages(&self, provider: Arc) -> Result> { @@ -362,7 +362,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn index_docs( @@ -388,7 +388,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn get_dap_binary( @@ -410,7 +410,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn dap_request_kind( &self, @@ -427,7 +427,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result { @@ -441,7 +441,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn dap_locator_create_scenario( @@ -465,7 +465,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } async fn run_dap_locator( &self, @@ -481,7 +481,7 @@ impl extension::Extension for WasmExtension { } .boxed() }) - .await + .await? } } @@ -761,7 +761,7 @@ impl WasmExtension { .with_context(|| format!("failed to load wasm extension {}", manifest.id)) } - pub async fn call(&self, f: Fn) -> T + pub async fn call(&self, f: Fn) -> Result where T: 'static + Send, Fn: 'static @@ -777,14 +777,15 @@ impl WasmExtension { } .boxed() })) - .unwrap_or_else(|_| { - panic!( + .map_err(|_| { + anyhow!( "wasm extension channel should not be closed yet, extension {} (id {})", - self.manifest.name, self.manifest.id, + self.manifest.name, + self.manifest.id, ) - }); - return_rx.await.unwrap_or_else(|_| { - panic!( + })?; + return_rx.await.with_context(|| { + format!( "wasm extension channel, extension {} (id {})", self.manifest.name, self.manifest.id, ) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 5b523c9a03da1428e96a7377921ca76466c790d4..5a8cc05d7d93f9877ab7df468d8e8971488d8c1a 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7393,21 +7393,23 @@ impl LspStore { } pub(crate) async fn refresh_workspace_configurations( - this: &WeakEntity, + lsp_store: &WeakEntity, fs: Arc, cx: &mut AsyncApp, ) { maybe!(async move { - let servers = this - .update(cx, |this, cx| { - let Some(local) = this.as_local() else { + let mut refreshed_servers = HashSet::default(); + let servers = lsp_store + .update(cx, |lsp_store, cx| { + let toolchain_store = lsp_store.toolchain_store(cx); + let Some(local) = lsp_store.as_local() else { return Vec::default(); }; local .language_server_ids .iter() .flat_map(|((worktree_id, _), server_ids)| { - let worktree = this + let worktree = lsp_store .worktree_store .read(cx) .worktree_for_id(*worktree_id, cx); @@ -7423,43 +7425,54 @@ impl LspStore { ) }); - server_ids.iter().filter_map(move |server_id| { + let fs = fs.clone(); + let toolchain_store = toolchain_store.clone(); + server_ids.iter().filter_map(|server_id| { + let delegate = delegate.clone()? as Arc; let states = local.language_servers.get(server_id)?; match states { LanguageServerState::Starting { .. } => None, LanguageServerState::Running { adapter, server, .. - } => Some(( - adapter.adapter.clone(), - server.clone(), - delegate.clone()? as Arc, - )), + } => { + let fs = fs.clone(); + let toolchain_store = toolchain_store.clone(); + let adapter = adapter.clone(); + let server = server.clone(); + refreshed_servers.insert(server.name()); + Some(cx.spawn(async move |_, cx| { + let settings = + LocalLspStore::workspace_configuration_for_adapter( + adapter.adapter.clone(), + fs.as_ref(), + &delegate, + toolchain_store, + cx, + ) + .await + .ok()?; + server + .notify::( + &lsp::DidChangeConfigurationParams { settings }, + ) + .ok()?; + Some(()) + })) + } } - }) + }).collect::>() }) .collect::>() }) .ok()?; - let toolchain_store = this.update(cx, |this, cx| this.toolchain_store(cx)).ok()?; - for (adapter, server, delegate) in servers { - let settings = LocalLspStore::workspace_configuration_for_adapter( - adapter, - fs.as_ref(), - &delegate, - toolchain_store.clone(), - cx, - ) - .await - .ok()?; - - server - .notify::( - &lsp::DidChangeConfigurationParams { settings }, - ) - .ok(); - } + log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}"); + // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension + // to stop and unregister its language server wrapper. + // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. + // This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere. + let _: Vec> = join_all(servers).await; Some(()) }) .await;