From 89732f12799923cdc50ed27eb58bcefba75a4f6b Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:15:39 -0400 Subject: [PATCH 01/13] git: Forbid main git worktree deletion from worktree picker (#52841) This operation would always fail in the UI because the git binary already disallowed it. This PR just makes the UI more inline with what's actually valid Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - git: Forbid main git worktree deletion from git worktree picker --------- Co-authored-by: Eric Holk --- crates/collab/tests/integration/git_tests.rs | 4 ++++ crates/fs/src/fake_git_repo.rs | 2 ++ crates/git/src/repository.rs | 18 +++++++++++++++++- crates/git_ui/src/worktree_picker.rs | 19 +++++++++++++------ crates/project/src/git_store.rs | 2 ++ crates/proto/proto/git.proto | 1 + crates/sidebar/src/project_group_builder.rs | 1 + crates/sidebar/src/sidebar_tests.rs | 17 +++++++++++++++++ 8 files changed, 57 insertions(+), 7 deletions(-) diff --git a/crates/collab/tests/integration/git_tests.rs b/crates/collab/tests/integration/git_tests.rs index cc1b748675d421ae92316d490df243f6d79bbc4f..4af1355352554ee6e3350806cefe0b4cd41cf5d6 100644 --- a/crates/collab/tests/integration/git_tests.rs +++ b/crates/collab/tests/integration/git_tests.rs @@ -401,16 +401,19 @@ async fn test_linked_worktrees_sync( path: PathBuf::from(path!("/project")), ref_name: Some("refs/heads/main".into()), sha: "aaa111".into(), + is_main: false, }); state.worktrees.push(GitWorktree { path: PathBuf::from(path!("/project/feature-branch")), ref_name: Some("refs/heads/feature-branch".into()), sha: "bbb222".into(), + is_main: false, }); state.worktrees.push(GitWorktree { path: PathBuf::from(path!("/project/bugfix-branch")), ref_name: Some("refs/heads/bugfix-branch".into()), sha: "ccc333".into(), + is_main: false, }); }) .unwrap(); @@ -480,6 +483,7 @@ async fn test_linked_worktrees_sync( path: PathBuf::from(path!("/project/hotfix-branch")), ref_name: Some("refs/heads/hotfix-branch".into()), sha: "ddd444".into(), + is_main: false, }); }) .unwrap(); diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 38cb1e6b3c467dba4430767c2f4d6705c1d8b2aa..12a095ffe27aa760623fa2b6ce674fdd9008eef1 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -429,6 +429,7 @@ impl GitRepository for FakeGitRepository { path: work_dir, ref_name: Some(branch_ref.into()), sha: head_sha.into(), + is_main: true, }; let mut all = vec![main_worktree]; all.extend(state.worktrees.iter().cloned()); @@ -470,6 +471,7 @@ impl GitRepository for FakeGitRepository { path, ref_name: Some(ref_name.into()), sha: sha.into(), + is_main: false, }); state.branches.insert(branch_name); Ok::<(), anyhow::Error>(()) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 8b0f189d869fe2438aecbac14895d5f30deaf308..b03fe1b0c63904bfc751ab7946f92a7c8595db00 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -238,6 +238,7 @@ pub struct Worktree { pub ref_name: Option, // todo(git_worktree) This type should be a Oid pub sha: SharedString, + pub is_main: bool, } impl Worktree { @@ -259,6 +260,7 @@ impl Worktree { pub fn parse_worktrees_from_str>(raw_worktrees: T) -> Vec { let mut worktrees = Vec::new(); + let mut is_first = true; let normalized = raw_worktrees.as_ref().replace("\r\n", "\n"); let entries = normalized.split("\n\n"); for entry in entries { @@ -286,7 +288,9 @@ pub fn parse_worktrees_from_str>(raw_worktrees: T) -> Vec) -> bool { + !self.is_new + && !self.worktree.is_main + && forbidden_deletion_path != Some(&self.worktree.path) + } +} + pub struct WorktreeListDelegate { matches: Vec, all_worktrees: Option>, @@ -462,7 +470,7 @@ impl WorktreeListDelegate { let Some(entry) = self.matches.get(idx).cloned() else { return; }; - if entry.is_new || self.forbidden_deletion_path.as_ref() == Some(&entry.worktree.path) { + if !entry.can_delete(self.forbidden_deletion_path.as_ref()) { return; } let Some(repo) = self.repo.clone() else { @@ -719,6 +727,7 @@ impl PickerDelegate for WorktreeListDelegate { path: Default::default(), ref_name: Some(format!("refs/heads/{query}").into()), sha: Default::default(), + is_main: false, }, positions: Vec::new(), is_new: true, @@ -805,8 +814,7 @@ impl PickerDelegate for WorktreeListDelegate { let focus_handle = self.focus_handle.clone(); - let can_delete = - !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path); + let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref()); let delete_button = |entry_ix: usize| { IconButton::new(("delete-worktree", entry_ix), IconName::Trash) @@ -894,9 +902,8 @@ impl PickerDelegate for WorktreeListDelegate { let focus_handle = self.focus_handle.clone(); let selected_entry = self.matches.get(self.selected_index); let is_creating = selected_entry.is_some_and(|entry| entry.is_new); - let can_delete = selected_entry.is_some_and(|entry| { - !entry.is_new && self.forbidden_deletion_path.as_ref() != Some(&entry.worktree.path) - }); + let can_delete = selected_entry + .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref())); let footer_container = h_flex() .w_full() diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 1be256a50a1029e54a6002f1301f248b88b80850..36479fb80f561665e01853eba5a214eb84088361 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -7060,6 +7060,7 @@ fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree { .map(|s| s.to_string()) .unwrap_or_default(), sha: worktree.sha.to_string(), + is_main: worktree.is_main, } } @@ -7068,6 +7069,7 @@ fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree { path: PathBuf::from(proto.path.clone()), ref_name: Some(SharedString::from(&proto.ref_name)), sha: proto.sha.clone().into(), + is_main: proto.is_main, } } diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index 4f6b74e9537ac4570c72c3c5319b9819f1e52d0c..cb878cade726002e7e09670cf7c190880d8e66cb 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -575,6 +575,7 @@ message Worktree { string path = 1; string ref_name = 2; string sha = 3; + bool is_main = 4; } message GitCreateWorktree { diff --git a/crates/sidebar/src/project_group_builder.rs b/crates/sidebar/src/project_group_builder.rs index 318dfac0a839e28ceb27c6036b87e6a13d9bc992..0b8e56ac99565218dd827048afdee71e896f2667 100644 --- a/crates/sidebar/src/project_group_builder.rs +++ b/crates/sidebar/src/project_group_builder.rs @@ -255,6 +255,7 @@ mod tests { path: std::path::PathBuf::from("/wt/feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "abc".into(), + is_main: false, }); }) .expect("git state should be set"); diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index d0400d1ae4338ac5de4dfee0daca9196f12030b5..b9bd873d369a44d3e09db9771383c111ead2ccb6 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -2463,6 +2463,7 @@ async fn test_cmd_n_shows_new_thread_entry_in_absorbed_worktree(cx: &mut TestApp path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -2577,6 +2578,7 @@ async fn test_search_matches_worktree_name(cx: &mut TestAppContext) { path: std::path::PathBuf::from("/wt/rosewood"), ref_name: Some("refs/heads/rosewood".into()), sha: "abc".into(), + is_main: false, }); }) .unwrap(); @@ -2638,6 +2640,7 @@ async fn test_git_worktree_added_live_updates_sidebar(cx: &mut TestAppContext) { path: std::path::PathBuf::from("/wt/rosewood"), ref_name: Some("refs/heads/rosewood".into()), sha: "abc".into(), + is_main: false, }); }) .unwrap(); @@ -2739,11 +2742,13 @@ async fn test_two_worktree_workspaces_absorbed_when_main_added(cx: &mut TestAppC path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); state.worktrees.push(git::repository::Worktree { path: std::path::PathBuf::from("/wt-feature-b"), ref_name: Some("refs/heads/feature-b".into()), sha: "bbb".into(), + is_main: false, }); }) .unwrap(); @@ -2821,11 +2826,13 @@ async fn test_threadless_workspace_shows_new_thread_with_worktree_chip(cx: &mut path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); state.worktrees.push(git::repository::Worktree { path: std::path::PathBuf::from("/wt-feature-b"), ref_name: Some("refs/heads/feature-b".into()), sha: "bbb".into(), + is_main: false, }); }) .unwrap(); @@ -2941,6 +2948,7 @@ async fn test_multi_worktree_thread_shows_multiple_chips(cx: &mut TestAppContext path: std::path::PathBuf::from(format!("/worktrees/{repo}/{branch}/{repo}")), ref_name: Some(format!("refs/heads/{branch}").into()), sha: "aaa".into(), + is_main: false, }); } }) @@ -3043,6 +3051,7 @@ async fn test_same_named_worktree_chips_are_deduplicated(cx: &mut TestAppContext path: std::path::PathBuf::from(format!("/worktrees/{repo}/olivetti/{repo}")), ref_name: Some("refs/heads/olivetti".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -3133,6 +3142,7 @@ async fn test_absorbed_worktree_running_thread_shows_live_status(cx: &mut TestAp path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -3248,6 +3258,7 @@ async fn test_absorbed_worktree_completion_triggers_notification(cx: &mut TestAp path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -3354,6 +3365,7 @@ async fn test_clicking_worktree_thread_opens_workspace_when_none_exists(cx: &mut path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -3459,6 +3471,7 @@ async fn test_clicking_worktree_thread_does_not_briefly_render_as_separate_proje path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -3609,6 +3622,7 @@ async fn test_clicking_absorbed_worktree_thread_activates_worktree_workspace( path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -4203,6 +4217,7 @@ async fn test_archive_thread_uses_next_threads_own_workspace(cx: &mut TestAppCon path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -4374,6 +4389,7 @@ async fn test_linked_worktree_threads_not_duplicated_across_groups(cx: &mut Test path: std::path::PathBuf::from("/wt-feature-a"), ref_name: Some("refs/heads/feature-a".into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); @@ -5180,6 +5196,7 @@ mod property_test { path: worktree_pathbuf, ref_name: Some(format!("refs/heads/{}", worktree_name).into()), sha: "aaa".into(), + is_main: false, }); }) .unwrap(); From 3a6faf2b4a65deb6e8e2c38fce6f6e8babe76159 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Tue, 31 Mar 2026 20:56:49 +0200 Subject: [PATCH 02/13] editor: Deduplicate sticky header rows (#52844) Fixes a bug that caused duplicate sticky header rows to appear if multiple outline items start on the same row. Sort of addresses #52722, although arguably the real issue there is that duplicate outline items are being created in the first place. Before: https://github.com/user-attachments/assets/7941cbe8-9b62-470c-b475-f08f2f20fac6 After: https://github.com/user-attachments/assets/c4e291ea-6414-483f-8ff7-3d89d10000b6 Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed a bug that caused duplicate sticky header rows to appear if multiple outline items start on the same row. --- crates/editor/src/editor_tests.rs | 59 +++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 7 ++++ 2 files changed, 66 insertions(+) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 48c92f0f22762f95b1d6ef681951355a340d221e..65a872e6035565bb01fdd78e00d6cf0f35d35ef8 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -31952,6 +31952,65 @@ async fn test_sticky_scroll_with_expanded_deleted_diff_hunks( assert_eq!(sticky_headers(6.0), vec![]); } +#[gpui::test] +async fn test_no_duplicated_sticky_headers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + ˇimpl Foo { fn bar() { + let x = 1; + fn baz() { + let y = 2; + } + } } + "}); + + cx.update_editor(|e, _, cx| { + e.buffer() + .read(cx) + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| { + buffer.set_language(Some(rust_lang()), cx); + }) + }); + + let mut sticky_headers = |offset: ScrollOffset| { + cx.update_editor(|e, window, cx| { + e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx); + }); + cx.run_until_parked(); + cx.update_editor(|e, window, cx| { + EditorElement::sticky_headers(&e, &e.snapshot(window, cx)) + .into_iter() + .map( + |StickyHeader { + start_point, + offset, + .. + }| { (start_point, offset) }, + ) + .collect::>() + }) + }; + + let struct_foo = Point { row: 0, column: 0 }; + let fn_baz = Point { row: 2, column: 4 }; + + assert_eq!(sticky_headers(0.0), vec![]); + assert_eq!(sticky_headers(0.5), vec![(struct_foo, 0.0)]); + assert_eq!(sticky_headers(1.0), vec![(struct_foo, 0.0)]); + assert_eq!(sticky_headers(1.5), vec![(struct_foo, 0.0), (fn_baz, 1.0)]); + assert_eq!(sticky_headers(2.0), vec![(struct_foo, 0.0), (fn_baz, 1.0)]); + assert_eq!(sticky_headers(2.5), vec![(struct_foo, 0.0), (fn_baz, 0.5)]); + assert_eq!(sticky_headers(3.0), vec![(struct_foo, 0.0)]); + assert_eq!(sticky_headers(3.5), vec![(struct_foo, 0.0)]); + assert_eq!(sticky_headers(4.0), vec![(struct_foo, 0.0)]); + assert_eq!(sticky_headers(4.5), vec![(struct_foo, -0.5)]); + assert_eq!(sticky_headers(5.0), vec![]); +} + #[gpui::test] fn test_relative_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 968048f68513a09c460bb06789103923bbbca828..9ce080c87bf82ec1098e2a4b1db6bc6a65d22828 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4674,6 +4674,13 @@ impl EditorElement { .display_snapshot .point_to_display_point(start_point, Bias::Left) .row(); + if rows + .last() + .is_some_and(|last| last.sticky_row == sticky_row) + { + continue; + } + let end_row = snapshot .display_snapshot .point_to_display_point(end_point, Bias::Left) From 88d12750feaa05429ec64ac5fe96f885c1c1f0bf Mon Sep 17 00:00:00 2001 From: David Alecrim <35930364+davidalecrim1@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:09:16 -0300 Subject: [PATCH 03/13] command_palette: Fix keymap editor not matching actions with underscored namespaces (#50415) Closes https://github.com/zed-industries/zed/issues/50223 ## Summary When clicking **Change Keybinding** from the command palette on an action whose namespace contains underscores (e.g. `terminal_panel::Toggle`, `project_panel::ToggleFocus`), the keymap editor showed **"No matches found for the provided query"**. Actions without underscores (e.g. `zed::OpenLog`) worked fine. I opened this issue for this https://github.com/zed-industries/zed/issues/50223, but took the liberty of sending a PR. **Root cause:** `normalize_action_query` preserved underscores in the query, but the search candidates are built with `humanize_action_name` which converts underscores to spaces. The fuzzy matcher looked for `_` in a candidate like `"terminal panel: toggle"` where it doesn't exist, so matching always failed. **Fix:** `normalize_action_query` now converts underscores to spaces before the deduplication checks, consistent with `humanize_action_name`. This also correctly collapses consecutive underscores with adjacent spaces. All three call sites of `normalize_action_query` (command palette search, keymap editor filter, action completion provider) match against humanized candidates, so the fix improves consistency across all of them. ## Before (Left) / After (Right) Screenshot 2026-02-28 at 17 56 05 Screenshot 2026-02-28 at 17 55 38 Release Notes: - Fixed keymap editor showing no results when opening "Change Keybinding" from the command palette for actions with underscores in their namespace (e.g. `terminal_panel::Toggle`, `project_panel::ToggleFocus`) --- crates/command_palette/src/command_palette.rs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 90ed7d0d3518aa4f6d49bb4cc18cbf3c275ce7c5..4a80740c3765f25ee878a60fa061c17e3a795b5f 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -43,24 +43,28 @@ pub struct CommandPalette { picker: Entity>, } -/// Removes subsequent whitespace characters and double colons from the query. +/// Removes subsequent whitespace characters and double colons from the query, and converts +/// underscores to spaces. /// /// This improves the likelihood of a match by either humanized name or keymap-style name. +/// Underscores are converted to spaces because `humanize_action_name` converts them to spaces +/// when building the search candidates (e.g. `terminal_panel::Toggle` -> `terminal panel: toggle`). pub fn normalize_action_query(input: &str) -> String { let mut result = String::with_capacity(input.len()); let mut last_char = None; for char in input.trim().chars() { - match (last_char, char) { + let normalized_char = if char == '_' { ' ' } else { char }; + match (last_char, normalized_char) { (Some(':'), ':') => continue, - (Some(last_char), char) if last_char.is_whitespace() && char.is_whitespace() => { + (Some(last_char), c) if last_char.is_whitespace() && c.is_whitespace() => { continue; } _ => { - last_char = Some(char); + last_char = Some(normalized_char); } } - result.push(char); + result.push(normalized_char); } result @@ -775,6 +779,14 @@ mod tests { normalize_action_query("editor: :GoToDefinition"), "editor: :GoToDefinition" ); + assert_eq!( + normalize_action_query("terminal_panel::Toggle"), + "terminal panel:Toggle" + ); + assert_eq!( + normalize_action_query("project_panel::ToggleFocus"), + "project panel:ToggleFocus" + ); } #[gpui::test] From 03c5d379724e4b2e74418be6c7ec706eb7d44033 Mon Sep 17 00:00:00 2001 From: Daniel Strobusch <1847260+dastrobu@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:12:32 +0200 Subject: [PATCH 04/13] worktree: Close single-file worktrees when file is deleted (#49366) When a single-file worktree's root file no longer exists, the background scanner would previously enter an infinite retry loop attempting to canonicalize the path. This caused continuous error logging and resource waste. This fix detects when a single-file worktree root cannot be canonicalized (after attempting the file handle fallback) and emits a new Deleted event, allowing the worktree to be properly closed. This is most commonly encountered with temporary files, logs, and similar files that are opened in Zed and then deleted externally, but persist in the workspace database across sessions. Closes #34864 ## Test **Logs** from manual testing: ``` 2026-02-17T16:16:11+01:00 INFO [worktree] inserting parent git repo for this worktree: "tmp.md" 2026-02-17T16:16:17+01:00 ERROR [worktree] root path could not be canonicalized: canonicalizing "/Users/***/tmp/tmp.md": No such file or directory (os error 2) 2026-02-17T16:16:17+01:00 INFO [worktree] single-file worktree root "/Users/***/tmp/tmp.md" no longer exists, marking as deleted 2026-02-17T16:16:17+01:00 INFO [worktree] worktree root /Users/***/tmp/tmp.md no longer exists, closing worktree ``` Release Notes: - Fixed an issue where Zed would enter an infinite retry loop when the backing file for a single-file worktree was deleted --- .../edit_prediction/src/license_detection.rs | 4 +- crates/project/src/lsp_store.rs | 3 +- crates/project/src/manifest_tree.rs | 1 + crates/project/src/worktree_store.rs | 4 ++ crates/worktree/src/worktree.rs | 27 ++++++++ crates/worktree/tests/integration/main.rs | 66 +++++++++++++++++++ 6 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/edit_prediction/src/license_detection.rs b/crates/edit_prediction/src/license_detection.rs index 2b44825c4ceef1a317034966aa1a0b6a7a0f54c2..6f701d13a9d4d915bbfbc2442ea5643afac30ef4 100644 --- a/crates/edit_prediction/src/license_detection.rs +++ b/crates/edit_prediction/src/license_detection.rs @@ -308,7 +308,9 @@ impl LicenseDetectionWatcher { } } } - worktree::Event::DeletedEntry(_) | worktree::Event::UpdatedGitRepositories(_) => {} + worktree::Event::DeletedEntry(_) + | worktree::Event::UpdatedGitRepositories(_) + | worktree::Event::Deleted => {} }); let worktree_snapshot = worktree.read(cx).snapshot(); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 10fb447a6f6c7867212a4622d084deb4fcea91a2..286d3a85f86173bff5d17d8d7c86d26464a04714 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -4413,7 +4413,8 @@ impl LspStore { this.update_local_worktree_language_servers(&worktree, changes, cx); } worktree::Event::UpdatedGitRepositories(_) - | worktree::Event::DeletedEntry(_) => {} + | worktree::Event::DeletedEntry(_) + | worktree::Event::Deleted => {} }) .detach() } diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs index 82dd1bc0d3fdd0149ced5ce3f2cf9ae480c9f2b7..1ae5b0e809f3803c3f8858afb065637ba0a0f256 100644 --- a/crates/project/src/manifest_tree.rs +++ b/crates/project/src/manifest_tree.rs @@ -59,6 +59,7 @@ impl WorktreeRoots { let path = TriePath::from(entry.path.as_ref()); this.roots.remove(&path); } + WorktreeEvent::Deleted => {} } }), }) diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 4d464182fa670c6efc7ea2644abd68ef0dcda90a..92f7db453a81c6224455002b7811f2e6945f2a82 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -808,6 +808,10 @@ impl WorktreeStore { worktree::Event::DeletedEntry(id) => { cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id)) } + worktree::Event::Deleted => { + // The worktree root itself has been deleted (for single-file worktrees) + // The worktree will be removed via the observe_release callback + } } }) .detach(); diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 1ad3e7ecaa694326aa479b5b69ccd3206fbf1e8d..6665dfa532be31eedba6e522cafd06945e3b33e4 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -355,6 +355,7 @@ enum ScanState { RootUpdated { new_path: Arc, }, + RootDeleted, } struct UpdateObservationState { @@ -368,6 +369,8 @@ pub enum Event { UpdatedEntries(UpdatedEntriesSet), UpdatedGitRepositories(UpdatedGitRepositoriesSet), DeletedEntry(ProjectEntryId), + /// The worktree root itself has been deleted (for single-file worktrees) + Deleted, } impl EventEmitter for Worktree {} @@ -1106,6 +1109,7 @@ impl LocalWorktree { }; let fs_case_sensitive = fs.is_case_sensitive().await; + let is_single_file = snapshot.snapshot.root_dir().is_none(); let mut scanner = BackgroundScanner { fs, fs_case_sensitive, @@ -1128,6 +1132,7 @@ impl LocalWorktree { share_private_files, settings, watcher, + is_single_file, }; scanner @@ -1156,6 +1161,13 @@ impl LocalWorktree { ScanState::RootUpdated { new_path } => { this.update_abs_path_and_refresh(new_path, cx); } + ScanState::RootDeleted => { + log::info!( + "worktree root {} no longer exists, closing worktree", + this.abs_path().display() + ); + cx.emit(Event::Deleted); + } } }); } @@ -3799,6 +3811,9 @@ struct BackgroundScanner { watcher: Arc, settings: WorktreeSettings, share_private_files: bool, + /// Whether this is a single-file worktree (root is a file, not a directory). + /// Used to determine if we should give up after repeated canonicalization failures. + is_single_file: bool, } #[derive(Copy, Clone, PartialEq)] @@ -4096,6 +4111,18 @@ impl BackgroundScanner { .ok(); } else { log::error!("root path could not be canonicalized: {err:#}"); + + // For single-file worktrees, if we can't canonicalize and the file handle + // fallback also failed, the file is gone - close the worktree + if self.is_single_file { + log::info!( + "single-file worktree root {:?} no longer exists, marking as deleted", + root_path.as_path() + ); + self.status_updates_tx + .unbounded_send(ScanState::RootDeleted) + .ok(); + } } return; } diff --git a/crates/worktree/tests/integration/main.rs b/crates/worktree/tests/integration/main.rs index fb8ec444dd324e935aad873bf201d4f0b8ae2019..cd7dd1c9056a7d501bec2bcd7b07d596f689a908 100644 --- a/crates/worktree/tests/integration/main.rs +++ b/crates/worktree/tests/integration/main.rs @@ -14,10 +14,12 @@ use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandl use serde_json::json; use settings::{SettingsStore, WorktreeId}; use std::{ + cell::Cell, env, fmt::Write, mem, path::{Path, PathBuf}, + rc::Rc, sync::Arc, }; use util::{ @@ -3105,3 +3107,67 @@ async fn test_refresh_entries_for_paths_creates_ancestors(cx: &mut TestAppContex ); }); } + +#[gpui::test] +async fn test_single_file_worktree_deleted(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + + fs.insert_tree( + "/root", + json!({ + "test.txt": "content", + }), + ) + .await; + + let tree = Worktree::local( + Path::new("/root/test.txt"), + true, + fs.clone(), + Default::default(), + true, + WorktreeId::from_proto(0), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.read_with(cx, |tree, _| { + assert!(tree.is_single_file(), "Should be a single-file worktree"); + assert_eq!(tree.abs_path().as_ref(), Path::new("/root/test.txt")); + }); + + // Delete the file + fs.remove_file(Path::new("/root/test.txt"), Default::default()) + .await + .unwrap(); + + // Subscribe to worktree events + let deleted_event_received = Rc::new(Cell::new(false)); + let _subscription = cx.update({ + let deleted_event_received = deleted_event_received.clone(); + |cx| { + cx.subscribe(&tree, move |_, event, _| { + if matches!(event, Event::Deleted) { + deleted_event_received.set(true); + } + }) + } + }); + + // Trigger filesystem events - the scanner should detect the file is gone immediately + // and emit a Deleted event + cx.background_executor.run_until_parked(); + cx.background_executor + .advance_clock(std::time::Duration::from_secs(1)); + cx.background_executor.run_until_parked(); + + assert!( + deleted_event_received.get(), + "Should receive Deleted event when single-file worktree root is deleted" + ); +} From 922b1b512618d96bd5759b1e92f5f4ca5bd5ff49 Mon Sep 17 00:00:00 2001 From: Andre Roelofs Date: Tue, 31 Mar 2026 21:33:37 +0200 Subject: [PATCH 05/13] worktree: Make expanded symlinks be indexed by default (#51382) Closes #41887 This is a stable subset of other efforts to make symlink flows more intuitive like #46344 Release Notes: - Fixed expanded symlinks not being searchable --- crates/worktree/src/worktree.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 6665dfa532be31eedba6e522cafd06945e3b33e4..b08f9aaee016d7047b06bf9ac4a4a1ce2b2d1ad8 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2776,7 +2776,7 @@ impl LocalSnapshot { for entry in self.entries_by_path.cursor::<()>(()) { if entry.is_file() { assert_eq!(files.next().unwrap().inode, entry.inode); - if (!entry.is_ignored && !entry.is_external) || entry.is_always_included { + if !entry.is_ignored || entry.is_always_included { assert_eq!(visible_files.next().unwrap().inode, entry.inode); } } @@ -3455,8 +3455,7 @@ pub struct Entry { /// symlink. /// /// We only scan entries outside of the worktree once the symlinked - /// directory is expanded. External entries are treated like gitignored - /// entries in that they are not included in searches. + /// directory is expanded. pub is_external: bool, /// Whether this entry is considered to be a `.env` file. @@ -3661,8 +3660,7 @@ impl sum_tree::Item for Entry { type Summary = EntrySummary; fn summary(&self, _cx: ()) -> Self::Summary { - let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included - { + let non_ignored_count = if self.is_ignored && !self.is_always_included { 0 } else { 1 From ca7de0fc5815e26c6836b373bb0c2cc7137baa7e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:29:07 -0300 Subject: [PATCH 06/13] sidebar: Add design adjustments to the thread import feature (#52858) - Adds a new icon for thread import - Iterate on the design to access the import modal: it's now through a button in the sidebar's footer, which only appears when the archive view is toggled - Fixed an issue where clicking on checkboxes within the import modal's list items wouldn't do anything Release Notes: - N/A --- assets/icons/thread_import.svg | 5 ++ crates/agent_ui/src/thread_import.rs | 49 +++++--------- crates/agent_ui/src/threads_archive_view.rs | 75 +-------------------- crates/icons/src/icons.rs | 1 + crates/sidebar/src/sidebar.rs | 43 ++++++------ crates/ui/src/components/list/list_item.rs | 12 ++-- 6 files changed, 52 insertions(+), 133 deletions(-) create mode 100644 assets/icons/thread_import.svg diff --git a/assets/icons/thread_import.svg b/assets/icons/thread_import.svg new file mode 100644 index 0000000000000000000000000000000000000000..a56b5a7cccc09c5795bfadff06f06d15833232f3 --- /dev/null +++ b/assets/icons/thread_import.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/crates/agent_ui/src/thread_import.rs b/crates/agent_ui/src/thread_import.rs index 9dd6b5efa0ae1cd3bc19dc6ae6a287218de8c668..f5fc89d3df4991ff5186e2af6d73ad6a840c09a1 100644 --- a/crates/agent_ui/src/thread_import.rs +++ b/crates/agent_ui/src/thread_import.rs @@ -121,18 +121,6 @@ impl ThreadImportModal { .collect() } - fn set_agent_checked(&mut self, agent_id: AgentId, state: ToggleState, cx: &mut Context) { - match state { - ToggleState::Selected => { - self.unchecked_agents.remove(&agent_id); - } - ToggleState::Unselected | ToggleState::Indeterminate => { - self.unchecked_agents.insert(agent_id); - } - } - cx.notify(); - } - fn toggle_agent_checked(&mut self, agent_id: AgentId, cx: &mut Context) { if self.unchecked_agents.contains(&agent_id) { self.unchecked_agents.remove(&agent_id); @@ -283,6 +271,11 @@ impl ModalView for ThreadImportModal {} impl Render for ThreadImportModal { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let has_agents = !self.agent_entries.is_empty(); + let disabled_import_thread = self.is_importing + || !has_agents + || self.unchecked_agents.len() == self.agent_entries.len(); + let agent_rows = self .agent_entries .iter() @@ -295,6 +288,7 @@ impl Render for ThreadImportModal { .rounded() .spacing(ListItemSpacing::Sparse) .focused(is_focused) + .disabled(self.is_importing) .child( h_flex() .w_full() @@ -311,22 +305,14 @@ impl Render for ThreadImportModal { }) .child(Label::new(entry.display_name.clone())), ) - .end_slot( - Checkbox::new( - ("thread-import-agent-checkbox", ix), - if is_checked { - ToggleState::Selected - } else { - ToggleState::Unselected - }, - ) - .on_click({ - let agent_id = entry.agent_id.clone(); - cx.listener(move |this, state: &ToggleState, _window, cx| { - this.set_agent_checked(agent_id.clone(), *state, cx); - }) - }), - ) + .end_slot(Checkbox::new( + ("thread-import-agent-checkbox", ix), + if is_checked { + ToggleState::Selected + } else { + ToggleState::Unselected + }, + )) .on_click({ let agent_id = entry.agent_id.clone(); cx.listener(move |this, _event, _window, cx| { @@ -336,11 +322,6 @@ impl Render for ThreadImportModal { }) .collect::>(); - let has_agents = !self.agent_entries.is_empty(); - let disabled_import_thread = self.is_importing - || !has_agents - || self.unchecked_agents.len() == self.agent_entries.len(); - v_flex() .id("thread-import-modal") .key_context("ThreadImportModal") @@ -373,7 +354,7 @@ impl Render for ThreadImportModal { v_flex() .id("thread-import-agent-list") .max_h(rems_from_px(320.)) - .pb_2() + .pb_1() .overflow_y_scroll() .when(has_agents, |this| this.children(agent_rows)) .when(!has_agents, |this| { diff --git a/crates/agent_ui/src/threads_archive_view.rs b/crates/agent_ui/src/threads_archive_view.rs index 445d86c9ad4e37fa8b2502a754a5264cd1d4dc45..74a93129d387e0aaac6e7092d9e086dd64e369f7 100644 --- a/crates/agent_ui/src/threads_archive_view.rs +++ b/crates/agent_ui/src/threads_archive_view.rs @@ -1,5 +1,5 @@ use crate::agent_connection_store::AgentConnectionStore; -use crate::thread_import::{AcpThreadImportOnboarding, ThreadImportModal}; + use crate::thread_metadata_store::{ThreadMetadata, ThreadMetadataStore}; use crate::{Agent, RemoveSelectedThread}; @@ -15,15 +15,13 @@ use gpui::{ }; use itertools::Itertools as _; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use project::{AgentId, AgentRegistryStore, AgentServerStore}; +use project::{AgentId, AgentServerStore}; use settings::Settings as _; use theme::ActiveTheme; use ui::ThreadItem; use ui::{ Divider, KeyBinding, Tooltip, WithScrollbar, prelude::*, utils::platform_title_bar_height, }; -use util::ResultExt; -use workspace::{MultiWorkspace, Workspace}; use zed_actions::agents_sidebar::FocusSidebarFilter; use zed_actions::editor::{MoveDown, MoveUp}; @@ -114,18 +112,12 @@ pub struct ThreadsArchiveView { _refresh_history_task: Task<()>, agent_connection_store: WeakEntity, agent_server_store: WeakEntity, - agent_registry_store: WeakEntity, - workspace: WeakEntity, - multi_workspace: WeakEntity, } impl ThreadsArchiveView { pub fn new( agent_connection_store: WeakEntity, agent_server_store: WeakEntity, - agent_registry_store: WeakEntity, - workspace: WeakEntity, - multi_workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -184,11 +176,8 @@ impl ThreadsArchiveView { thread_metadata_store_subscription, ], _refresh_history_task: Task::ready(()), - agent_registry_store, agent_connection_store, agent_server_store, - workspace, - multi_workspace, }; this.update_items(cx); @@ -550,43 +539,6 @@ impl ThreadsArchiveView { .detach_and_log_err(cx); } - fn should_render_acp_import_onboarding(&self, cx: &App) -> bool { - let has_external_agents = self - .agent_server_store - .upgrade() - .map(|store| store.read(cx).has_external_agents()) - .unwrap_or(false); - - has_external_agents && !AcpThreadImportOnboarding::dismissed(cx) - } - - fn show_thread_import_modal(&mut self, window: &mut Window, cx: &mut Context) { - let Some(agent_server_store) = self.agent_server_store.upgrade() else { - return; - }; - let Some(agent_registry_store) = self.agent_registry_store.upgrade() else { - return; - }; - - let workspace_handle = self.workspace.clone(); - let multi_workspace = self.multi_workspace.clone(); - - self.workspace - .update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - ThreadImportModal::new( - agent_server_store, - agent_registry_store, - workspace_handle.clone(), - multi_workspace.clone(), - window, - cx, - ) - }); - }) - .log_err(); - } - fn render_header(&self, window: &Window, cx: &mut Context) -> impl IntoElement { let has_query = !self.filter_editor.read(cx).text(cx).is_empty(); let sidebar_on_left = matches!( @@ -729,28 +681,5 @@ impl Render for ThreadsArchiveView { .size_full() .child(self.render_header(window, cx)) .child(content) - .when(!self.should_render_acp_import_onboarding(cx), |this| { - this.child( - div() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Button::new("import-acp", "Import ACP Threads") - .full_width() - .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border)) - .label_size(LabelSize::Small) - .start_icon( - Icon::new(IconName::ArrowDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.show_thread_import_modal(window, cx); - })), - ), - ) - }) } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 400b2a22bc6071b62c6ce22a2b1bf1053c8cf871..ad7faa6664d1ddc618c8984781a244af7dda6c97 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -240,6 +240,7 @@ pub enum IconName { ThinkingModeOff, Thread, ThreadFromSummary, + ThreadImport, ThreadsSidebarLeftClosed, ThreadsSidebarLeftOpen, ThreadsSidebarRightClosed, diff --git a/crates/sidebar/src/sidebar.rs b/crates/sidebar/src/sidebar.rs index 450a2674e0062d917003758c41c445048ee603f7..b1257b4c79c2ef193ec4594139cd1f57b93a5666 100644 --- a/crates/sidebar/src/sidebar.rs +++ b/crates/sidebar/src/sidebar.rs @@ -3309,10 +3309,24 @@ impl Sidebar { } fn render_sidebar_bottom_bar(&mut self, cx: &mut Context) -> impl IntoElement { - let on_right = self.side(cx) == SidebarSide::Right; let is_archive = matches!(self.view, SidebarView::Archive(..)); + let show_import_button = is_archive && !self.should_render_acp_import_onboarding(cx); + let on_right = self.side(cx) == SidebarSide::Right; + let action_buttons = h_flex() .gap_1() + .when(on_right, |this| this.flex_row_reverse()) + .when(show_import_button, |this| { + this.child( + IconButton::new("thread-import", IconName::ThreadImport) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Import ACP Threads")) + .on_click(cx.listener(|this, _, window, cx| { + this.show_archive(window, cx); + this.show_thread_import_modal(window, cx); + })), + ) + }) .child( IconButton::new("archive", IconName::Archive) .icon_size(IconSize::Small) @@ -3325,21 +3339,16 @@ impl Sidebar { })), ) .child(self.render_recent_projects_button(cx)); - let border_color = cx.theme().colors().border; - let toggle_button = self.render_sidebar_toggle_button(cx); - let bar = h_flex() + h_flex() .p_1() .gap_1() + .when(on_right, |this| this.flex_row_reverse()) .justify_between() .border_t_1() - .border_color(border_color); - - if on_right { - bar.child(action_buttons).child(toggle_button) - } else { - bar.child(toggle_button).child(action_buttons) - } + .border_color(cx.theme().colors().border) + .child(self.render_sidebar_toggle_button(cx)) + .child(action_buttons) } fn active_workspace(&self, cx: &App) -> Option> { @@ -3409,7 +3418,7 @@ impl Sidebar { v_flex() .min_w_0() .w_full() - .p_1p5() + .p_2() .border_t_1() .border_color(cx.theme().colors().border) .bg(linear_gradient( @@ -3437,8 +3446,8 @@ impl Sidebar { .style(ButtonStyle::OutlinedCustom(cx.theme().colors().border)) .label_size(LabelSize::Small) .start_icon( - Icon::new(IconName::ArrowDown) - .size(IconSize::XSmall) + Icon::new(IconName::ThreadImport) + .size(IconSize::Small) .color(Color::Muted), ) .on_click(cx.listener(|this, _, window, cx| { @@ -3467,9 +3476,6 @@ impl Sidebar { let Some(agent_panel) = active_workspace.read(cx).panel::(cx) else { return; }; - let Some(agent_registry_store) = AgentRegistryStore::try_global(cx) else { - return; - }; let agent_server_store = active_workspace .read(cx) @@ -3484,9 +3490,6 @@ impl Sidebar { ThreadsArchiveView::new( agent_connection_store.clone(), agent_server_store.clone(), - agent_registry_store.downgrade(), - active_workspace.downgrade(), - self.multi_workspace.clone(), window, cx, ) diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 693cf3d52e34369d04db445d1ddac765691fb429..0d3efc024f1a202947fd0e7b0dab917c40ae8337 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -234,9 +234,9 @@ impl RenderOnce for ListItem { this.ml(self.indent_level as f32 * self.indent_step_size) .px(DynamicSpacing::Base04.rems(cx)) }) - .when(!self.inset && !self.disabled, |this| { + .when(!self.inset, |this| { this.when_some(self.focused, |this, focused| { - if focused { + if focused && !self.disabled { this.border_1() .when(self.docked_right, |this| this.border_r_2()) .border_color(cx.theme().colors().border_focused) @@ -244,7 +244,7 @@ impl RenderOnce for ListItem { this.border_1() } }) - .when(self.selectable, |this| { + .when(self.selectable && !self.disabled, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.outlined, |this| this.rounded_sm()) @@ -268,16 +268,16 @@ impl RenderOnce for ListItem { ListItemSpacing::ExtraDense => this.py_neg_px(), ListItemSpacing::Sparse => this.py_1(), }) - .when(self.inset && !self.disabled, |this| { + .when(self.inset, |this| { this.when_some(self.focused, |this, focused| { - if focused { + if focused && !self.disabled { this.border_1() .border_color(cx.theme().colors().border_focused) } else { this.border_1() } }) - .when(self.selectable, |this| { + .when(self.selectable && !self.disabled, |this| { this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) .when(self.selected, |this| { From a12601f8631070ee2474783b9bfb61bca539a574 Mon Sep 17 00:00:00 2001 From: Eric Holk Date: Tue, 31 Mar 2026 16:23:21 -0700 Subject: [PATCH 07/13] workspace: Break workspace tests into their own file (#52854) Splits the tests out of `multi_workspace.rs` into a dedicated `multi_workspace_tests.rs` file for better organization. Release Notes: - N/A Co-authored-by: Max Brunsfeld --- crates/workspace/src/multi_workspace.rs | 175 ------------------ crates/workspace/src/multi_workspace_tests.rs | 172 +++++++++++++++++ crates/workspace/src/workspace.rs | 2 + 3 files changed, 174 insertions(+), 175 deletions(-) create mode 100644 crates/workspace/src/multi_workspace_tests.rs diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 862f7c7b267721833fa395e501b604d30745a1b7..10a5ce70ead2d5aea7cc21a9af53ee9f216859c3 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -1113,178 +1113,3 @@ impl Render for MultiWorkspace { ) } } - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use settings::SettingsStore; - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme_settings::init(theme::LoadThemes::JustBase, cx); - DisableAiSettings::register(cx); - cx.update_flags(false, vec!["agent-v2".into()]); - }); - } - - #[gpui::test] - async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - - let (multi_workspace, cx) = - cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); - - multi_workspace.read_with(cx, |mw, cx| { - assert!(mw.multi_workspace_enabled(cx)); - }); - - multi_workspace.update_in(cx, |mw, _window, cx| { - mw.open_sidebar(cx); - assert!(mw.sidebar_open()); - }); - - cx.update(|_window, cx| { - DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx); - }); - cx.run_until_parked(); - - multi_workspace.read_with(cx, |mw, cx| { - assert!( - !mw.sidebar_open(), - "Sidebar should be closed when disable_ai is true" - ); - assert!( - !mw.multi_workspace_enabled(cx), - "Multi-workspace should be disabled when disable_ai is true" - ); - }); - - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); - multi_workspace.read_with(cx, |mw, _cx| { - assert!( - !mw.sidebar_open(), - "Sidebar should remain closed when toggled with disable_ai true" - ); - }); - - cx.update(|_window, cx| { - DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx); - }); - cx.run_until_parked(); - - multi_workspace.read_with(cx, |mw, cx| { - assert!( - mw.multi_workspace_enabled(cx), - "Multi-workspace should be enabled after re-enabling AI" - ); - assert!( - !mw.sidebar_open(), - "Sidebar should still be closed after re-enabling AI (not auto-opened)" - ); - }); - - multi_workspace.update_in(cx, |mw, window, cx| { - mw.toggle_sidebar(window, cx); - }); - multi_workspace.read_with(cx, |mw, _cx| { - assert!( - mw.sidebar_open(), - "Sidebar should open when toggled after re-enabling AI" - ); - }); - } - - #[gpui::test] - async fn test_replace(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project_a = Project::test(fs.clone(), [], cx).await; - let project_b = Project::test(fs.clone(), [], cx).await; - let project_c = Project::test(fs.clone(), [], cx).await; - let project_d = Project::test(fs.clone(), [], cx).await; - - let (multi_workspace, cx) = cx - .add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); - - let workspace_a_id = - multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id()); - - // Replace the only workspace (single-workspace case). - let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| { - let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx)); - mw.replace(workspace.clone(), &*window, cx); - workspace - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!(mw.workspaces().len(), 1); - assert_eq!( - mw.workspaces()[0].entity_id(), - workspace_b.entity_id(), - "slot should now be project_b" - ); - assert_ne!( - mw.workspaces()[0].entity_id(), - workspace_a_id, - "project_a should be gone" - ); - }); - - // Add project_c as a second workspace, then replace it with project_d. - let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| { - mw.test_add_workspace(project_c.clone(), window, cx) - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!(mw.workspaces().len(), 2); - assert_eq!(mw.active_workspace_index(), 1); - }); - - let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| { - let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx)); - mw.replace(workspace.clone(), &*window, cx); - workspace - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces"); - assert_eq!(mw.active_workspace_index(), 1); - assert_eq!( - mw.workspaces()[1].entity_id(), - workspace_d.entity_id(), - "active slot should now be project_d" - ); - assert_ne!( - mw.workspaces()[1].entity_id(), - workspace_c.entity_id(), - "project_c should be gone" - ); - }); - - // Replace with workspace_b which is already in the list — should just switch. - multi_workspace.update_in(cx, |mw, window, cx| { - mw.replace(workspace_b.clone(), &*window, cx); - }); - - multi_workspace.read_with(cx, |mw, _cx| { - assert_eq!( - mw.workspaces().len(), - 2, - "no workspace should be added or removed" - ); - assert_eq!( - mw.active_workspace_index(), - 0, - "should have switched to workspace_b" - ); - }); - } -} diff --git a/crates/workspace/src/multi_workspace_tests.rs b/crates/workspace/src/multi_workspace_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..50161121719ec7b2835fd11e389f24860e57d8f5 --- /dev/null +++ b/crates/workspace/src/multi_workspace_tests.rs @@ -0,0 +1,172 @@ +use super::*; +use feature_flags::FeatureFlagAppExt; +use fs::FakeFs; +use gpui::TestAppContext; +use project::DisableAiSettings; +use settings::SettingsStore; + +fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme_settings::init(theme::LoadThemes::JustBase, cx); + DisableAiSettings::register(cx); + cx.update_flags(false, vec!["agent-v2".into()]); + }); +} + +#[gpui::test] +async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + + multi_workspace.read_with(cx, |mw, cx| { + assert!(mw.multi_workspace_enabled(cx)); + }); + + multi_workspace.update_in(cx, |mw, _window, cx| { + mw.open_sidebar(cx); + assert!(mw.sidebar_open()); + }); + + cx.update(|_window, cx| { + DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx); + }); + cx.run_until_parked(); + + multi_workspace.read_with(cx, |mw, cx| { + assert!( + !mw.sidebar_open(), + "Sidebar should be closed when disable_ai is true" + ); + assert!( + !mw.multi_workspace_enabled(cx), + "Multi-workspace should be disabled when disable_ai is true" + ); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + mw.toggle_sidebar(window, cx); + }); + multi_workspace.read_with(cx, |mw, _cx| { + assert!( + !mw.sidebar_open(), + "Sidebar should remain closed when toggled with disable_ai true" + ); + }); + + cx.update(|_window, cx| { + DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx); + }); + cx.run_until_parked(); + + multi_workspace.read_with(cx, |mw, cx| { + assert!( + mw.multi_workspace_enabled(cx), + "Multi-workspace should be enabled after re-enabling AI" + ); + assert!( + !mw.sidebar_open(), + "Sidebar should still be closed after re-enabling AI (not auto-opened)" + ); + }); + + multi_workspace.update_in(cx, |mw, window, cx| { + mw.toggle_sidebar(window, cx); + }); + multi_workspace.read_with(cx, |mw, _cx| { + assert!( + mw.sidebar_open(), + "Sidebar should open when toggled after re-enabling AI" + ); + }); +} + +#[gpui::test] +async fn test_replace(cx: &mut TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.executor()); + let project_a = Project::test(fs.clone(), [], cx).await; + let project_b = Project::test(fs.clone(), [], cx).await; + let project_c = Project::test(fs.clone(), [], cx).await; + let project_d = Project::test(fs.clone(), [], cx).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx)); + + let workspace_a_id = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].entity_id()); + + // Replace the only workspace (single-workspace case). + let workspace_b = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| Workspace::test_new(project_b.clone(), window, cx)); + mw.replace(workspace.clone(), &*window, cx); + workspace + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 1); + assert_eq!( + mw.workspaces()[0].entity_id(), + workspace_b.entity_id(), + "slot should now be project_b" + ); + assert_ne!( + mw.workspaces()[0].entity_id(), + workspace_a_id, + "project_a should be gone" + ); + }); + + // Add project_c as a second workspace, then replace it with project_d. + let workspace_c = multi_workspace.update_in(cx, |mw, window, cx| { + mw.test_add_workspace(project_c.clone(), window, cx) + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 2); + assert_eq!(mw.active_workspace_index(), 1); + }); + + let workspace_d = multi_workspace.update_in(cx, |mw, window, cx| { + let workspace = cx.new(|cx| Workspace::test_new(project_d.clone(), window, cx)); + mw.replace(workspace.clone(), &*window, cx); + workspace + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!(mw.workspaces().len(), 2, "should still have 2 workspaces"); + assert_eq!(mw.active_workspace_index(), 1); + assert_eq!( + mw.workspaces()[1].entity_id(), + workspace_d.entity_id(), + "active slot should now be project_d" + ); + assert_ne!( + mw.workspaces()[1].entity_id(), + workspace_c.entity_id(), + "project_c should be gone" + ); + }); + + // Replace with workspace_b which is already in the list — should just switch. + multi_workspace.update_in(cx, |mw, window, cx| { + mw.replace(workspace_b.clone(), &*window, cx); + }); + + multi_workspace.read_with(cx, |mw, _cx| { + assert_eq!( + mw.workspaces().len(), + 2, + "no workspace should be added or removed" + ); + assert_eq!( + mw.active_workspace_index(), + 0, + "should have switched to workspace_b" + ); + }); +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 33d1befe38e3b48a39377547bd433398b37d6a77..ae05c2c59012b2caf217ac54a80b377aee87f09d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,6 +5,8 @@ pub mod invalid_item_view; pub mod item; mod modal_layer; mod multi_workspace; +#[cfg(test)] +mod multi_workspace_tests; pub mod notifications; pub mod pane; pub mod pane_group; From 20b140664d3b003ef4b23c03384f40c7c57aea06 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 31 Mar 2026 17:10:29 -0700 Subject: [PATCH 08/13] Fix markdown table rendering in the agent panel (#52864) This PR reverts part of https://github.com/zed-industries/zed/pull/50839, as it was causing bad clipping in the agent panel Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A --- crates/markdown/src/markdown.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 7b95688df54610f92b6960d9afc3037bf484b8ed..024e377c2538214c9579c8f025250e2166cf7ace 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1609,23 +1609,18 @@ impl Element for MarkdownElement { builder.table.start(alignments.clone()); let column_count = alignments.len(); - builder.push_div( - div().flex().flex_col().items_start(), - range, - markdown_end, - ); builder.push_div( div() .id(("table", range.start)) - .min_w_0() .grid() .grid_cols(column_count as u16) .when(self.style.table_columns_min_size, |this| { this.grid_cols_min_content(column_count as u16) }) .when(!self.style.table_columns_min_size, |this| { - this.grid_cols_max_content(column_count as u16) + this.grid_cols(column_count as u16) }) + .w_full() .mb_2() .border(px(1.5)) .border_color(cx.theme().colors().border) @@ -1770,7 +1765,6 @@ impl Element for MarkdownElement { } } MarkdownTagEnd::Table => { - builder.pop_div(); builder.pop_div(); builder.table.end(); } From 6837c8aa6bc314cd02e44f71e9377f3eada7120f Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:20:24 -0400 Subject: [PATCH 09/13] git_graph: Fix empty search case (#52845) This fixes a bug where search would match all commits if there was an empty query instead of setting the query to None Self-Review Checklist: - [ ] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A or Added/Fixed/Improved ... --------- Co-authored-by: Remco Smits --- crates/git_graph/src/git_graph.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/git_graph/src/git_graph.rs b/crates/git_graph/src/git_graph.rs index b971566075181350453b28bf9909371e51436021..d169ba686098dddd4881915ece11c8a97148affa 100644 --- a/crates/git_graph/src/git_graph.rs +++ b/crates/git_graph/src/git_graph.rs @@ -1324,6 +1324,12 @@ impl GitGraph { editor.set_text_style_refinement(Default::default()); }); + if query.as_str().is_empty() { + self.search_state.state = QueryState::Empty; + cx.notify(); + return; + } + let (request_tx, request_rx) = smol::channel::unbounded::(); repo.update(cx, |repo, cx| { From 22c123868af5109309c36cff293e525633039d90 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 31 Mar 2026 21:23:42 -0300 Subject: [PATCH 10/13] ui: Improve the `end_hover` method API in the `ListItem` component (#52862) This PR changes the API for the `ListItem`'s `end_hover` slot so that whatever is in there is always part of the flex stack, as opposed to an absolutely-positioned element. Additionally, I'm also improving the API for swapping content between the default state and the hovered state (e.g., list items where by default we render X, but when you hover, we show something else). Lastly, I'm adding buttons to some Git picker items that were only previously available through modal footer buttons. Release Notes: - N/A --- assets/icons/maximize_alt.svg | 6 ++ crates/agent_ui/src/config_options.rs | 2 +- .../src/ui/model_selector_components.rs | 2 +- crates/git_ui/src/branch_picker.rs | 18 ++---- crates/git_ui/src/stash_picker.rs | 54 +++++++++++----- crates/git_ui/src/worktree_picker.rs | 30 +++++++-- crates/icons/src/icons.rs | 1 + crates/recent_projects/src/recent_projects.rs | 27 ++------ crates/recent_projects/src/remote_servers.rs | 40 ++++++------ crates/rules_library/src/rules_library.rs | 2 +- crates/tab_switcher/src/tab_switcher.rs | 2 +- crates/tasks_ui/src/modal.rs | 2 +- crates/ui/src/components/list/list_item.rs | 63 ++++++++++++------- 13 files changed, 144 insertions(+), 105 deletions(-) create mode 100644 assets/icons/maximize_alt.svg diff --git a/assets/icons/maximize_alt.svg b/assets/icons/maximize_alt.svg new file mode 100644 index 0000000000000000000000000000000000000000..b8b8705f902c2469ed959f93f89ca3caf3b8fc51 --- /dev/null +++ b/assets/icons/maximize_alt.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/agent_ui/src/config_options.rs b/crates/agent_ui/src/config_options.rs index b8cf7e5d57921c7710392911829fc2b5045a0f90..44c0baa232222c0ba7c1d54acdecaabacfa85f12 100644 --- a/crates/agent_ui/src/config_options.rs +++ b/crates/agent_ui/src/config_options.rs @@ -650,7 +650,7 @@ impl PickerDelegate for ConfigOptionPickerDelegate { .end_slot(div().pr_2().when(is_selected, |this| { this.child(Icon::new(IconName::Check).color(Color::Accent)) })) - .end_hover_slot(div().pr_1p5().child({ + .end_slot_on_hover(div().pr_1p5().child({ let (icon, color, tooltip) = if is_favorite { (IconName::StarFilled, Color::Accent, "Unfavorite") } else { diff --git a/crates/agent_ui/src/ui/model_selector_components.rs b/crates/agent_ui/src/ui/model_selector_components.rs index 01ba6c4511854e83b97b1fc053e41e5d0e82ff1e..88bf546a0e7beef53c8043fd04f8e6e9e5e92c88 100644 --- a/crates/agent_ui/src/ui/model_selector_components.rs +++ b/crates/agent_ui/src/ui/model_selector_components.rs @@ -160,7 +160,7 @@ impl RenderOnce for ModelSelectorListItem { .end_slot(div().pr_2().when(self.is_selected, |this| { this.child(Icon::new(IconName::Check).color(Color::Accent)) })) - .end_hover_slot(div().pr_1p5().when_some(self.on_toggle_favorite, { + .end_slot_on_hover(div().pr_1p5().when_some(self.on_toggle_favorite, { |this, handle_click| { let (icon, color, tooltip) = if is_favorite { (IconName::StarFilled, Color::Accent, "Unfavorite Model") diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 438df6839949d46d3ba8e0509995beb1300b7c80..83c8119a077ac1c024dbb3b3df948f762b072ec1 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -1087,13 +1087,8 @@ impl PickerDelegate for BranchListDelegate { ), ) .when(!is_new_items && !is_head_branch, |this| { - this.map(|this| { - if self.selected_index() == ix { - this.end_slot(deleted_branch_icon(ix)) - } else { - this.end_hover_slot(deleted_branch_icon(ix)) - } - }) + this.end_slot(deleted_branch_icon(ix)) + .show_end_slot_on_hover() }) .when_some( if is_new_items { @@ -1102,13 +1097,8 @@ impl PickerDelegate for BranchListDelegate { None }, |this, create_from_default_button| { - this.map(|this| { - if self.selected_index() == ix { - this.end_slot(create_from_default_button) - } else { - this.end_hover_slot(create_from_default_button) - } - }) + this.end_slot(create_from_default_button) + .show_end_slot_on_hover() }, ), ) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 9987190f45b73f3f1132ce1295de6f412022abe2..2d3515e833e4d353c323f533f1f0f39bb1d76561 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -501,16 +501,39 @@ impl PickerDelegate for StashListDelegate { .size(LabelSize::Small), ); - let focus_handle = self.focus_handle.clone(); + let view_button = { + let focus_handle = self.focus_handle.clone(); + IconButton::new(("view-stash", ix), IconName::Eye) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in("View Stash", &ShowStashItem, &focus_handle, cx) + }) + .on_click(cx.listener(move |this, _, window, cx| { + this.delegate.show_stash_at(ix, window, cx); + })) + }; + + let pop_button = { + let focus_handle = self.focus_handle.clone(); + IconButton::new(("pop-stash", ix), IconName::MaximizeAlt) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in("Pop Stash", &menu::SecondaryConfirm, &focus_handle, cx) + }) + .on_click(|_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx); + }) + }; - let drop_button = |entry_ix: usize| { - IconButton::new(("drop-stash", entry_ix), IconName::Trash) + let drop_button = { + let focus_handle = self.focus_handle.clone(); + IconButton::new(("drop-stash", ix), IconName::Trash) .icon_size(IconSize::Small) .tooltip(move |_, cx| { Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx) }) .on_click(cx.listener(move |this, _, window, cx| { - this.delegate.drop_stash_at(entry_ix, window, cx); + this.delegate.drop_stash_at(ix, window, cx); })) }; @@ -530,17 +553,14 @@ impl PickerDelegate for StashListDelegate { ) .child(div().w_full().child(stash_label).child(branch_info)), ) - .tooltip(Tooltip::text(format!( - "stash@{{{}}}", - entry_match.entry.index - ))) - .map(|this| { - if selected { - this.end_slot(drop_button(ix)) - } else { - this.end_hover_slot(drop_button(ix)) - } - }), + .end_slot( + h_flex() + .gap_0p5() + .child(view_button) + .child(pop_button) + .child(drop_button), + ) + .show_end_slot_on_hover(), ) } @@ -549,6 +569,10 @@ impl PickerDelegate for StashListDelegate { } fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { + if self.matches.is_empty() { + return None; + } + let focus_handle = self.focus_handle.clone(); Some( diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 3e14b56f9bf4a95452855bc6cbef6f764e2b3530..c3e2259e411c7a3a56a36b92735f8d5e014e53d7 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -884,12 +884,30 @@ impl PickerDelegate for WorktreeListDelegate { } })), ) - .when(can_delete, |this| { - if selected { - this.end_slot(delete_button(ix)) - } else { - this.end_hover_slot(delete_button(ix)) - } + .when(!entry.is_new, |this| { + let focus_handle = self.focus_handle.clone(); + let open_in_new_window_button = + IconButton::new(("open-new-window", ix), IconName::ArrowUpRight) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + "Open in New Window", + &menu::SecondaryConfirm, + &focus_handle, + cx, + ) + }) + .on_click(|_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx); + }); + + this.end_slot( + h_flex() + .gap_0p5() + .child(open_in_new_window_button) + .when(can_delete, |this| this.child(delete_button(ix))), + ) + .show_end_slot_on_hover() }), ) } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index ad7faa6664d1ddc618c8984781a244af7dda6c97..6929ae4e4ca8ca0ee00c9793c948892043dd6dd6 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -174,6 +174,7 @@ pub enum IconName { LockOutlined, MagnifyingGlass, Maximize, + MaximizeAlt, Menu, MenuAltTemp, Mic, diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 4dc06036ef8416fd859cc815ab090ba5896c0040..22987f6c56669e1972a9bfc940449991d9f55642 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1264,13 +1264,8 @@ impl PickerDelegate for RecentProjectsDelegate { this.tooltip(Tooltip::text(path.to_string_lossy().to_string())) }), ) - .map(|el| { - if self.selected_index == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }) + .end_slot(secondary_actions) + .show_end_slot_on_hover() .into_any_element(), ) } @@ -1363,13 +1358,8 @@ impl PickerDelegate for RecentProjectsDelegate { }) .tooltip(Tooltip::text(tooltip_path)), ) - .map(|el| { - if self.selected_index == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }) + .end_slot(secondary_actions) + .show_end_slot_on_hover() .into_any_element(), ) } @@ -1503,13 +1493,8 @@ impl PickerDelegate for RecentProjectsDelegate { }) .tooltip(Tooltip::text(tooltip_path)), ) - .map(|el| { - if self.selected_index == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }) + .end_slot(secondary_actions) + .show_end_slot_on_hover() .into_any_element(), ) } diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index f7054687579155d4895ae191de1b7fa7cd14fbf6..26592a8035d50caa4e267a5478d5aceb9fba6e3e 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1621,23 +1621,24 @@ impl RemoteServerProjects { })) .tooltip(Tooltip::text(project.paths.join("\n"))) .when(is_from_zed, |server_list_item| { - server_list_item.end_hover_slot::(Some( - div() - .mr_2() - .child({ - let project = project.clone(); - // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::Trash) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .size(ButtonSize::Large) - .tooltip(Tooltip::text("Delete Remote Project")) - .on_click(cx.listener(move |this, _, _, cx| { - this.delete_remote_project(server_ix, &project, cx) - })) - }) - .into_any_element(), - )) + server_list_item + .end_slot( + div() + .mr_2() + .child({ + let project = project.clone(); + IconButton::new("remove-remote-project", IconName::Trash) + .icon_size(IconSize::Small) + .shape(IconButtonShape::Square) + .size(ButtonSize::Large) + .tooltip(Tooltip::text("Delete Remote Project")) + .on_click(cx.listener(move |this, _, _, cx| { + this.delete_remote_project(server_ix, &project, cx) + })) + }) + .into_any_element(), + ) + .show_end_slot_on_hover() }), ) } @@ -2413,9 +2414,8 @@ impl RemoteServerProjects { .spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) .child(Label::new("Copy Server Address")) - .end_hover_slot( - Label::new(connection_string.clone()).color(Color::Muted), - ) + .end_slot(Label::new(connection_string.clone()).color(Color::Muted)) + .show_end_slot_on_hover() .on_click({ let connection_string = connection_string.clone(); move |_, _, cx| { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 23e7b83772f755089c49f824719af389ec589bd9..7e5a56f22d48c4d51f60d7d200dc8384582beb23 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -389,7 +389,7 @@ impl PickerDelegate for RulePickerDelegate { })) })) .when(!prompt_id.is_built_in(), |this| { - this.end_hover_slot( + this.end_slot_on_hover( h_flex() .child( IconButton::new("delete-rule", IconName::Trash) diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 0fb13c85d21797e4d57728c88fc8bb014a898f78..d1e19ea4faee8d8259d06e2c24875faac7a0117c 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -875,7 +875,7 @@ impl PickerDelegate for TabSwitcherDelegate { el.end_slot::(close_button) } else { el.end_slot::(indicator) - .end_hover_slot::(close_button) + .end_slot_on_hover::(close_button) } }), ) diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 34f0cd809692d649bcfbabb7952f3075618ead04..285a07c9562849b26b4cbba3de3979614384d875 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -570,7 +570,7 @@ impl PickerDelegate for TasksModalDelegate { Tooltip::simple("Delete Previously Scheduled Task", cx) }), ); - item.end_hover_slot(delete_button) + item.end_slot_on_hover(delete_button) } else { item } diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs index 0d3efc024f1a202947fd0e7b0dab917c40ae8337..9a764efd58cfd3365d92e534a715a0f23ce46e90 100644 --- a/crates/ui/src/components/list/list_item.rs +++ b/crates/ui/src/components/list/list_item.rs @@ -14,6 +14,14 @@ pub enum ListItemSpacing { Sparse, } +#[derive(Default)] +enum EndSlotVisibility { + #[default] + Always, + OnHover, + SwapOnHover(AnyElement), +} + #[derive(IntoElement, RegisterComponent)] pub struct ListItem { id: ElementId, @@ -28,9 +36,7 @@ pub struct ListItem { /// A slot for content that appears after the children, usually on the other side of the header. /// This might be a button, a disclosure arrow, a face pile, etc. end_slot: Option, - /// A slot for content that appears on hover after the children - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, + end_slot_visibility: EndSlotVisibility, toggle: Option, inset: bool, on_click: Option>, @@ -61,7 +67,7 @@ impl ListItem { indent_step_size: px(12.), start_slot: None, end_slot: None, - end_hover_slot: None, + end_slot_visibility: EndSlotVisibility::default(), toggle: None, inset: false, on_click: None, @@ -165,8 +171,14 @@ impl ListItem { self } - pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); + pub fn end_slot_on_hover(mut self, end_slot_on_hover: E) -> Self { + self.end_slot_visibility = + EndSlotVisibility::SwapOnHover(end_slot_on_hover.into_any_element()); + self + } + + pub fn show_end_slot_on_hover(mut self) -> Self { + self.end_slot_visibility = EndSlotVisibility::OnHover; self } @@ -338,28 +350,31 @@ impl RenderOnce for ListItem { .children(self.start_slot) .children(self.children), ) + .when(self.end_slot.is_some(), |this| this.justify_between()) .when_some(self.end_slot, |this, end_slot| { - this.justify_between().child( - h_flex() + this.child(match self.end_slot_visibility { + EndSlotVisibility::Always => { + h_flex().flex_shrink().overflow_hidden().child(end_slot) + } + EndSlotVisibility::OnHover => h_flex() .flex_shrink() .overflow_hidden() - .when(self.end_hover_slot.is_some(), |this| { - this.visible() - .group_hover("list_item", |this| this.invisible()) - }) - .child(end_slot), - ) - }) - .when_some(self.end_hover_slot, |this, end_hover_slot| { - this.child( - h_flex() - .h_full() - .absolute() - .right(DynamicSpacing::Base06.rems(cx)) - .top_0() .visible_on_hover("list_item") - .child(end_hover_slot), - ) + .child(end_slot), + EndSlotVisibility::SwapOnHover(hover_slot) => h_flex() + .relative() + .flex_shrink() + .child(h_flex().visible_on_hover("list_item").child(hover_slot)) + .child( + h_flex() + .absolute() + .inset_0() + .justify_end() + .overflow_hidden() + .group_hover("list_item", |this| this.invisible()) + .child(end_slot), + ), + }) }), ) } From 3b6252ca8048afba195bf2c3071869198bdf7d2f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 31 Mar 2026 18:14:18 -0700 Subject: [PATCH 11/13] Bump tree-sitter for fix to wasm loading of grammars w/ reserved words (#52856) This PR bumps Tree-sitter for this crash fix https://github.com/tree-sitter/tree-sitter/pull/5475 Release Notes: - Fixed a crash that could occasionally occur when parsing files using certain language extensions --- Cargo.lock | 455 +++++++++--------- Cargo.toml | 6 +- crates/extension_host/src/wasm_host.rs | 47 +- crates/extension_host/src/wasm_host/wit.rs | 8 +- .../src/wasm_host/wit/since_v0_0_1.rs | 14 +- .../src/wasm_host/wit/since_v0_0_4.rs | 14 +- .../src/wasm_host/wit/since_v0_0_6.rs | 14 +- .../src/wasm_host/wit/since_v0_1_0.rs | 14 +- .../src/wasm_host/wit/since_v0_2_0.rs | 14 +- .../src/wasm_host/wit/since_v0_3_0.rs | 14 +- .../src/wasm_host/wit/since_v0_4_0.rs | 14 +- .../src/wasm_host/wit/since_v0_5_0.rs | 14 +- .../src/wasm_host/wit/since_v0_6_0.rs | 14 +- .../src/wasm_host/wit/since_v0_8_0.rs | 14 +- 14 files changed, 366 insertions(+), 290 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38dcf369b3739f9087b574489666f4f1dfa012e0..f68704cb6ef68887b102f7f6a1a37c0fe694f662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,22 +111,13 @@ dependencies = [ "workspace", ] -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli 0.31.1", -] - [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli 0.32.3", + "gimli", ] [[package]] @@ -674,7 +665,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" dependencies = [ - "object 0.37.3", + "object", ] [[package]] @@ -1821,11 +1812,11 @@ version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ - "addr2line 0.25.1", + "addr2line", "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object", "rustc-demangle", "windows-link 0.2.1", ] @@ -3818,36 +3809,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5023e06632d8f351c2891793ccccfe4aef957954904392434038745fb6f1f68" +checksum = "ba33ddc4e157cb1abe9da6c821e8824f99e56d057c2c22536850e0141f281d61" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c4012b4c8c1f6eb05c0a0a540e3e1ee992631af51aa2bbb3e712903ce4fd65" +checksum = "69b23dd6ea360e6fb28a3f3b40b7f126509668f58076a4729b2cfd656f26a0ad" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6d883b4942ef3a7104096b8bc6f2d1a41393f159ac8de12aed27b25d67f895" +checksum = "a9d81afcee8fe27ee2536987df3fadcb2e161af4edb7dbe3ef36838d0ce74382" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7b2ee9eec6ca8a716d900d5264d678fb2c290c58c46c8da7f94ee268175d17" +checksum = "fb33595f1279fe7af03b28245060e9085caf98b10ed3137461a85796eb83972a" dependencies = [ "serde", "serde_derive", @@ -3855,9 +3846,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeda0892577afdce1ac2e9a983a55f8c5b87a59334e1f79d8f735a2d7ba4f4b4" +checksum = "0230a6ac0660bfe31eb244cbb43dcd4f2b3c1c4e0addc3e0348c6053ea60272e" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -3868,7 +3859,7 @@ dependencies = [ "cranelift-control", "cranelift-entity", "cranelift-isle", - "gimli 0.31.1", + "gimli", "hashbrown 0.15.5", "log", "postcard", @@ -3880,40 +3871,42 @@ dependencies = [ "sha2", "smallvec", "target-lexicon 0.13.3", + "wasmtime-internal-math", ] [[package]] name = "cranelift-codegen-meta" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e461480d87f920c2787422463313326f67664e68108c14788ba1676f5edfcd15" +checksum = "96d6817fdc15cb8f236fc9d8e610767d3a03327ceca4abff7a14d8e2154c405e" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", "cranelift-srcgen", + "heck 0.5.0", "pulley-interpreter", ] [[package]] name = "cranelift-codegen-shared" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976584d09f200c6c84c4b9ff7af64fc9ad0cb64dffa5780991edd3fe143a30a1" +checksum = "0403796328e9e2e7df2b80191cdbb473fd9ea3889eb45ef5632d0fef168ea032" [[package]] name = "cranelift-control" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46d43d70f4e17c545aa88dbf4c84d4200755d27c6e3272ebe4de65802fa6a955" +checksum = "188f04092279a3814e0b6235c2f9c2e34028e4beb72da7bfed55cbd184702bcc" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75418674520cb400c8772bfd6e11a62736c78fc1b6e418195696841d1bf91f1" +checksum = "43f5e7391167605d505fe66a337e1a69583b3f34b63d359ffa5a430313c555e8" dependencies = [ "cranelift-bitset", "serde", @@ -3922,9 +3915,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8b1a91c86687a344f3c52dd6dfb6e50db0dfa7f2e9c7711b060b3623e1fdeb" +checksum = "ea5440792eb2b5ba0a0976df371b9f94031bd853ae56f389de610bca7128a7cb" dependencies = [ "cranelift-codegen", "log", @@ -3934,15 +3927,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711baa4e3432d4129295b39ec2b4040cc1b558874ba0a37d08e832e857db7285" +checksum = "1e5c05fab6fce38d729088f3fa1060eaa1ad54eefd473588887205ed2ab2f79e" [[package]] name = "cranelift-native" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c83e8666e3bcc5ffeaf6f01f356f0e1f9dcd69ce5511a1efd7ca5722001a3f" +checksum = "9c9a0607a028edf5ba5bba7e7cf5ca1b7f0a030e3ae84dcd401e8b9b05192280" dependencies = [ "cranelift-codegen", "libc", @@ -3951,9 +3944,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.120.2" +version = "0.123.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e3f4d783a55c64266d17dc67d2708852235732a100fc40dd9f1051adc64d7b" +checksum = "cb0f2da72eb2472aaac6cfba4e785af42b1f2d82f5155f30c9c30e8cce351e17" [[package]] name = "crash-context" @@ -7054,21 +7047,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", "indexmap", "stable_deref_trait", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - [[package]] name = "gio-sys" version = "0.21.5" @@ -11361,9 +11348,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "crc32fast", "hashbrown 0.15.5", @@ -11371,15 +11358,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - [[package]] name = "ollama" version = "0.1.0" @@ -13518,13 +13496,25 @@ checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" [[package]] name = "pulley-interpreter" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986beaef947a51d17b42b0ea18ceaa88450d35b6994737065ed505c39172db71" +checksum = "499d922aa0f9faac8d92351416664f1b7acd914008a90fce2f0516d31efddf67" dependencies = [ "cranelift-bitset", "log", - "wasmtime-math", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3848fb193d6dffca43a21f24ca9492f22aab88af1223d06bac7f8a0ef405b81" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -16194,12 +16184,6 @@ dependencies = [ "der 0.7.10", ] -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - [[package]] name = "sqlez" version = "0.1.0" @@ -18242,17 +18226,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "transpose" version = "0.2.3" @@ -18265,9 +18238,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.3" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "974d205cc395652cfa8b37daa053fe56eebd429acf8dc055503fee648dae981e" +checksum = "887bd495d0582c5e3e0d8ece2233666169fa56a9644d172fc22ad179ab2d0538" dependencies = [ "cc", "regex", @@ -19360,12 +19333,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ba1d491ecacb085a2552025c10a675a6fddcbd03b1fc9b36c536010ce265d2" +checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" dependencies = [ "leb128fmt", - "wasmparser 0.229.0", + "wasmparser 0.236.1", ] [[package]] @@ -19488,9 +19461,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3b1f053f5d41aa55640a1fa9b6d1b8a9e4418d118ce308d20e24ff3575a8c" +checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" dependencies = [ "bitflags 2.10.0", "hashbrown 0.15.5", @@ -19513,22 +19486,22 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25dac01892684a99b8fbfaf670eb6b56edea8a096438c75392daeb83156ae2e" +checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.229.0", + "wasmparser 0.236.1", ] [[package]] name = "wasmtime" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57373e1d8699662fb791270ac5dfac9da5c14f618ecf940cdb29dc3ad9472a3c" +checksum = "6a2f8736ddc86e03a9d0e4c477a37939cfc53cd1b052ee38a3133679b87ef830" dependencies = [ - "addr2line 0.24.2", + "addr2line", "anyhow", "async-trait", "bitflags 2.10.0", @@ -19542,10 +19515,9 @@ dependencies = [ "log", "mach2 0.4.3", "memfd", - "object 0.36.7", + "object", "once_cell", "postcard", - "psm", "pulley-interpreter", "rayon", "rustix 1.1.2", @@ -19553,82 +19525,109 @@ dependencies = [ "serde", "serde_derive", "smallvec", - "sptr", "target-lexicon 0.13.3", - "trait-variant", - "wasmparser 0.229.0", - "wasmtime-asm-macros", - "wasmtime-component-macro", - "wasmtime-component-util", - "wasmtime-cranelift", + "wasmparser 0.236.1", "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-icache-coherence", - "wasmtime-math", - "wasmtime-slab", - "wasmtime-versioned-export-macros", - "wasmtime-winch", - "windows-sys 0.59.0", + "wasmtime-internal-asm-macros", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "windows-sys 0.60.2", ] [[package]] -name = "wasmtime-asm-macros" -version = "33.0.2" +name = "wasmtime-c-api-impl" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0fc91372865167a695dc98d0d6771799a388a7541d3f34e939d0539d6583de" +checksum = "f3c62ea3fa30e6b0cf61116b3035121b8f515c60ac118ebfdab2ee56d028ed1e" dependencies = [ - "cfg-if", + "anyhow", + "log", + "tracing", + "wasmtime", + "wasmtime-internal-c-api-macros", ] [[package]] -name = "wasmtime-c-api-impl" -version = "33.0.2" +name = "wasmtime-environ" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46db556f1dccdd88e0672bd407162ab0036b72e5eccb0f4398d8251cba32dba1" +checksum = "733682a327755c77153ac7455b1ba8f2db4d9946c1738f8002fe1fbda1d52e83" dependencies = [ "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", "log", - "tracing", - "wasmtime", - "wasmtime-c-api-macros", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon 0.13.3", + "wasm-encoder 0.236.1", + "wasmparser 0.236.1", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-asm-macros" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68288980a2e02bcb368d436da32565897033ea21918007e3f2bae18843326cf9" +dependencies = [ + "cfg-if", ] [[package]] -name = "wasmtime-c-api-macros" -version = "33.0.2" +name = "wasmtime-internal-c-api-macros" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315cc6bc8cdc66f296accb26d7625ae64c1c7b6da6f189e8a72ce6594bf7bd36" +checksum = "3c8c61294155a6d23c202f08cf7a2f9392a866edd50517508208818be626ce9f" dependencies = [ "proc-macro2", "quote", ] [[package]] -name = "wasmtime-component-macro" -version = "33.0.2" +name = "wasmtime-internal-component-macro" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25c9c7526675ff9a9794b115023c4af5128e3eb21389bfc3dc1fd344d549258f" +checksum = "5dea846da68f8e776c8a43bde3386022d7bb74e713b9654f7c0196e5ff2e4684" dependencies = [ "anyhow", "proc-macro2", "quote", "syn 2.0.117", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser 0.229.0", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.236.1", ] [[package]] -name = "wasmtime-component-util" -version = "33.0.2" +name = "wasmtime-internal-component-util" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc42ec8b078875804908d797cb4950fec781d9add9684c9026487fd8eb3f6291" +checksum = "fe1e5735b3c8251510d2a55311562772d6c6fca9438a3d0329eb6e38af4957d6" [[package]] -name = "wasmtime-cranelift" -version = "33.0.2" +name = "wasmtime-internal-cranelift" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bd72f0a6a0ffcc6a184ec86ac35c174e48ea0e97bbae277c8f15f8bf77a566" +checksum = "e89bb9ef571288e2be6b8a3c4763acc56c348dcd517500b1679d3ffad9e4a757" dependencies = [ "anyhow", "cfg-if", @@ -19637,104 +19636,132 @@ dependencies = [ "cranelift-entity", "cranelift-frontend", "cranelift-native", - "gimli 0.31.1", + "gimli", "itertools 0.14.0", "log", - "object 0.36.7", + "object", "pulley-interpreter", "smallvec", "target-lexicon 0.13.3", "thiserror 2.0.17", - "wasmparser 0.229.0", + "wasmparser 0.236.1", "wasmtime-environ", - "wasmtime-versioned-export-macros", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-environ" -version = "33.0.2" +name = "wasmtime-internal-fiber" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6187bb108a23eb25d2a92aa65d6c89fb5ed53433a319038a2558567f3011ff2" +checksum = "b698d004b15ea1f1ae2d06e5e8b80080cbd684fd245220ce2fac3cdd5ecf87f2" dependencies = [ "anyhow", - "cpp_demangle", - "cranelift-bitset", - "cranelift-entity", - "gimli 0.31.1", - "indexmap", - "log", - "object 0.36.7", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon 0.13.3", - "wasm-encoder 0.229.0", - "wasmparser 0.229.0", - "wasmprinter", - "wasmtime-component-util", + "cc", + "cfg-if", + "libc", + "rustix 1.1.2", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", ] [[package]] -name = "wasmtime-fiber" -version = "33.0.2" +name = "wasmtime-internal-jit-debug" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc8965d2128c012329f390e24b8b2758dd93d01bf67e1a1a0dd3d8fd72f56873" +checksum = "c803a9fec05c3d7fa03474d4595079d546e77a3c71c1d09b21f74152e2165c17" dependencies = [ - "anyhow", "cc", - "cfg-if", - "rustix 1.1.2", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.59.0", + "wasmtime-internal-versioned-export-macros", ] [[package]] -name = "wasmtime-jit-icache-coherence" -version = "33.0.2" +name = "wasmtime-internal-jit-icache-coherence" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7af0e940cb062a45c0b3f01a926f77da5947149e99beb4e3dd9846d5b8f11619" +checksum = "d3866909d37f7929d902e6011847748147e8734e9d7e0353e78fb8b98f586aee" dependencies = [ "anyhow", "cfg-if", "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] -name = "wasmtime-math" -version = "33.0.2" +name = "wasmtime-internal-math" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acfca360e719dda9a27e26944f2754ff2fd5bad88e21919c42c5a5f38ddd93cb" +checksum = "5a23b03fb14c64bd0dfcaa4653101f94ade76c34a3027ed2d6b373267536e45b" dependencies = [ "libm", ] [[package]] -name = "wasmtime-slab" -version = "33.0.2" +name = "wasmtime-internal-slab" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e240559cada55c4b24af979d5f6c95e0029f5772f32027ec3c62b258aaff65" +checksum = "fbff220b88cdb990d34a20b13344e5da2e7b99959a5b1666106bec94b58d6364" [[package]] -name = "wasmtime-versioned-export-macros" -version = "33.0.2" +name = "wasmtime-internal-unwinder" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0963c1438357a3d8c0efe152b4ef5259846c1cf8b864340270744fe5b3bae5e" +checksum = "13e1ad30e88988b20c0d1c56ea4b4fbc01a8c614653cbf12ca50c0dcc695e2f7" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "549aefdaa1398c2fcfbf69a7b882956bb5b6e8e5b600844ecb91a3b5bf658ca7" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "wasmtime-internal-winch" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc96a84c5700171aeecf96fa9a9ab234f333f5afb295dabf3f8a812b70fe832" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon 0.13.3", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "36.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28dc9efea511598c88564ac1974e0825c07d9c0de902dbf68f227431cd4ff8c" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "heck 0.5.0", + "indexmap", + "wit-parser 0.236.1", +] + [[package]] name = "wasmtime-wasi" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae951b72c7c6749a1c15dcdfb6d940a2614c932b4a54f474636e78e2c744b4c" +checksum = "c3c2e99fbaa0c26b4680e0c9af07e3f7b25f5fbc1ad97dd34067980bd027d3e5" dependencies = [ "anyhow", "async-trait", @@ -19758,14 +19785,14 @@ dependencies = [ "wasmtime", "wasmtime-wasi-io", "wiggle", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "wasmtime-wasi-io" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a835790dcecc3d7051ec67da52ba9e04af25e1bc204275b9391e3f0042b10797" +checksum = "de2dc367052562c228ce51ee4426330840433c29c0ea3349eca5ddeb475ecdb9" dependencies = [ "anyhow", "async-trait", @@ -19774,35 +19801,6 @@ dependencies = [ "wasmtime", ] -[[package]] -name = "wasmtime-winch" -version = "33.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc3b117d03d6eeabfa005a880c5c22c06503bb8820f3aa2e30f0e8d87b6752f" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.31.1", - "object 0.36.7", - "target-lexicon 0.13.3", - "wasmparser 0.229.0", - "wasmtime-cranelift", - "wasmtime-environ", - "winch-codegen", -] - -[[package]] -name = "wasmtime-wit-bindgen" -version = "33.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1382f4f09390eab0d75d4994d0c3b0f6279f86a571807ec67a8253c87cf6a145" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "wit-parser 0.229.0", -] - [[package]] name = "wast" version = "35.0.2" @@ -20257,9 +20255,9 @@ dependencies = [ [[package]] name = "wiggle" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649c1aca13ef9e9dccf2d5efbbebf12025bc5521c3fb7754355ef60f5eb810be" +checksum = "c13d1ae265bd6e5e608827d2535665453cae5cb64950de66e2d5767d3e32c43a" dependencies = [ "anyhow", "async-trait", @@ -20272,9 +20270,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164870fc34214ee42bd81b8ce9e7c179800fa1a7d4046d17a84e7f7bf422c8ad" +checksum = "607c4966f6b30da20d24560220137cbd09df722f0558eac81c05624700af5e05" dependencies = [ "anyhow", "heck 0.5.0", @@ -20286,9 +20284,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d873bb5b59ca703b5e41562e96a4796d1af61bf4cf80bf8a7abda755a380ec1c" +checksum = "fc36e39412fa35f7cc86b3705dbe154168721dd3e71f6dc4a726b266d5c60c55" dependencies = [ "proc-macro2", "quote", @@ -20329,21 +20327,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "33.0.2" +version = "36.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7914c296fbcef59d1b89a15e82384d34dc9669bc09763f2ef068a28dd3a64ebf" +checksum = "06c0ec09e8eb5e850e432da6271ed8c4a9d459a9db3850c38e98a3ee9d015e79" dependencies = [ "anyhow", "cranelift-assembler-x64", "cranelift-codegen", - "gimli 0.31.1", + "gimli", "regalloc2", "smallvec", "target-lexicon 0.13.3", "thiserror 2.0.17", - "wasmparser 0.229.0", - "wasmtime-cranelift", + "wasmparser 0.236.1", "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", ] [[package]] @@ -21369,9 +21368,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.229.0" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459c6ba62bf511d6b5f2a845a2a736822e38059c1cfa0b644b467bbbfae4efa6" +checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" dependencies = [ "anyhow", "id-arena", @@ -21382,7 +21381,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.229.0", + "wasmparser 0.236.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6c8f2a78a401cc2adebb712cd8ce739c696af878..d1271e0166677fb4069b5917f320e57c755263b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -732,7 +732,7 @@ toml_edit = { version = "0.22", default-features = false, features = [ "serde", ] } tower-http = "0.4.4" -tree-sitter = { version = "0.26", features = ["wasm"] } +tree-sitter = { version = "0.26.8", features = ["wasm"] } tree-sitter-bash = "0.25.1" tree-sitter-c = "0.24.1" tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" } @@ -767,7 +767,7 @@ uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } walkdir = "2.5" wasm-encoder = "0.221" wasmparser = "0.221" -wasmtime = { version = "33", default-features = false, features = [ +wasmtime = { version = "36", default-features = false, features = [ "async", "demangle", "runtime", @@ -776,7 +776,7 @@ wasmtime = { version = "33", default-features = false, features = [ "incremental-cache", "parallel-compilation", ] } -wasmtime-wasi = "33" +wasmtime-wasi = "36" wax = "0.7" which = "6.0.0" wasm-bindgen = "0.2.113" diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 286639cdd67d716b1137290baf269670ecddebe7..87a2032e831fc942f6848428a901a9fe3f613fc8 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -42,7 +42,7 @@ use wasmtime::{ CacheStore, Engine, Store, component::{Component, ResourceTable}, }; -use wasmtime_wasi::p2::{self as wasi, IoView as _}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; use wit::Extension; pub struct WasmHost { @@ -93,7 +93,7 @@ impl extension::Extension for WasmExtension { ) -> Result { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let command = extension .call_language_server_command( store, @@ -119,7 +119,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_initialization_options( store, @@ -143,7 +143,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_workspace_configuration( store, @@ -166,7 +166,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; extension .call_language_server_initialization_options_schema( store, @@ -187,7 +187,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; extension .call_language_server_workspace_configuration_schema( store, @@ -209,7 +209,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_additional_initialization_options( store, @@ -234,7 +234,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let options = extension .call_language_server_additional_workspace_configuration( store, @@ -331,7 +331,7 @@ impl extension::Extension for WasmExtension { self.call(|extension, store| { async move { let resource = if let Some(delegate) = delegate { - Some(store.data_mut().table().push(delegate)?) + Some(store.data_mut().table.push(delegate)?) } else { None }; @@ -355,7 +355,7 @@ impl extension::Extension for WasmExtension { ) -> Result { self.call(|extension, store| { async move { - let project_resource = store.data_mut().table().push(project)?; + let project_resource = store.data_mut().table.push(project)?; let command = extension .call_context_server_command(store, context_server_id.clone(), project_resource) .await? @@ -374,7 +374,7 @@ impl extension::Extension for WasmExtension { ) -> Result> { self.call(|extension, store| { async move { - let project_resource = store.data_mut().table().push(project)?; + let project_resource = store.data_mut().table.push(project)?; let Some(configuration) = extension .call_context_server_configuration( store, @@ -417,7 +417,7 @@ impl extension::Extension for WasmExtension { ) -> Result<()> { self.call(|extension, store| { async move { - let kv_store_resource = store.data_mut().table().push(kv_store)?; + let kv_store_resource = store.data_mut().table.push(kv_store)?; extension .call_index_docs( store, @@ -444,7 +444,7 @@ impl extension::Extension for WasmExtension { ) -> Result { self.call(|extension, store| { async move { - let resource = store.data_mut().table().push(worktree)?; + let resource = store.data_mut().table.push(worktree)?; let dap_binary = extension .call_get_dap_binary(store, dap_name, config, user_installed_path, resource) .await? @@ -532,7 +532,7 @@ impl extension::Extension for WasmExtension { pub struct WasmState { manifest: Arc, pub table: ResourceTable, - ctx: wasi::WasiCtx, + ctx: WasiCtx, pub host: Arc, pub(crate) capability_granter: CapabilityGranter, } @@ -726,7 +726,7 @@ impl WasmHost { }) } - async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { + async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { let extension_work_dir = self.work_dir.join(manifest.id.as_ref()); self.fs .create_dir(&extension_work_dir) @@ -739,7 +739,7 @@ impl WasmHost { #[cfg(target_os = "windows")] let path = path.replace('\\', "/"); - let mut ctx = wasi::WasiCtxBuilder::new(); + let mut ctx = WasiCtxBuilder::new(); ctx.inherit_stdio() .env("PWD", &path) .env("RUST_BACKTRACE", "full"); @@ -947,15 +947,16 @@ impl WasmState { } } -impl wasi::IoView for WasmState { - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } +impl wasmtime::component::HasData for WasmState { + type Data<'a> = &'a mut WasmState; } -impl wasi::WasiView for WasmState { - fn ctx(&mut self) -> &mut wasi::WasiCtx { - &mut self.ctx +impl WasiView for WasmState { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.ctx, + table: &mut self.table, + } } } diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs index 9c4d3aa298c366ae91d0f8195ed090d74099c6d0..27847422f01680240119877e0864491dd7660d68 100644 --- a/crates/extension_host/src/wasm_host/wit.rs +++ b/crates/extension_host/src/wasm_host/wit.rs @@ -42,18 +42,14 @@ pub use since_v0_0_4::LanguageServerConfig; pub fn new_linker( executor: &BackgroundExecutor, - f: impl Fn(&mut Linker, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, + f: impl FnOnce(&mut Linker) -> Result<()>, ) -> Linker { let mut linker = Linker::new(&wasm_engine(executor)); wasmtime_wasi::p2::add_to_linker_async(&mut linker).unwrap(); - f(&mut linker, wasi_view).unwrap(); + f(&mut linker).unwrap(); linker } -fn wasi_view(state: &mut WasmState) -> &mut WasmState { - state -} - /// Returns whether the given Wasm API version is supported by the Wasm host. pub fn is_supported_wasm_api_version(release_channel: ReleaseChannel, version: Version) -> bool { wasm_api_version_range(release_channel).contains(&version) diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs index fa7539eec9f454c95782cd0249664693074abfba..c231b7e5d69157d523973455b2437a576392a00d 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs @@ -12,8 +12,12 @@ use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: Version = Version::new(0, 0, 1); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.0.1", with: { "worktree": ExtensionWorktree, @@ -26,7 +30,11 @@ pub type ExtensionWorktree = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::DownloadedFileType { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs index 6d7db749f0cd021bfb084eba1bc20ce72780f3d8..41d652cec3087e8e5458a048689be4494de63356 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs @@ -10,8 +10,12 @@ use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: Version = Version::new(0, 0, 4); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.0.4", with: { "worktree": ExtensionWorktree, @@ -24,7 +28,11 @@ pub type ExtensionWorktree = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::DownloadedFileType { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs index e5ff0322088470d47e903c4a83794b654bbba531..e1dfdf8248b41de2de5e9faff3d212d06f1349c4 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs @@ -10,8 +10,12 @@ use wasmtime::component::{Linker, Resource}; pub const MIN_VERSION: Version = Version::new(0, 0, 6); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.0.6", with: { "worktree": ExtensionWorktree, @@ -31,7 +35,11 @@ pub type ExtensionWorktree = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::Command { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs index 0caaa86c2413f1b279319eeea4d8577d1ed4b5a5..4cd034d4d6af02971468ba8e57e1eebf9078353f 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs @@ -26,8 +26,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 1, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.1.0", with: { "worktree": ExtensionWorktree, @@ -52,7 +56,11 @@ pub type ExtensionHttpResponseStream = Arc &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::Command { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs index 074cce73c22d547cd3198a672e6f8cdc5f750d49..691e6d2dd549b64c3783406af210b6b48f4a1dbc 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 2, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.2.0", with: { "worktree": ExtensionWorktree, @@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::Command { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs index 072ad42f2b9c2f5b3a8556b237f3907052665370..53aa65d5187663ea86fa465af76cf3aebc7844e4 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 3, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.3.0", with: { "worktree": ExtensionWorktree, @@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs index 4f1d5c6a48c13ff09a5c81e2b43683fa50a7ccec..44b7d7ba1ad4e3235e8772a051bb906f87c64325 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 4, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.4.0", with: { "worktree": ExtensionWorktree, @@ -40,7 +44,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs index 84f73f567750081d406b20025f0b4598cfd0f9af..4dff0d90a94fe1128c6182592093b38cf43fe573 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs @@ -11,8 +11,12 @@ use super::{latest, since_v0_6_0}; pub const MIN_VERSION: Version = Version::new(0, 5, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.5.0", with: { "worktree": ExtensionWorktree, @@ -41,7 +45,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs index 202bcd6ce959b27b3b7ecf8e15830cb1955ec104..bc5674b051772e464c0cbdb74e75f935959e05d8 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs @@ -12,8 +12,12 @@ pub const MIN_VERSION: Version = Version::new(0, 6, 0); pub const MAX_VERSION: Version = Version::new(0, 7, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.6.0", with: { "worktree": ExtensionWorktree, @@ -43,7 +47,11 @@ pub type ExtensionKeyValueStore = Arc; pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for latest::CodeLabel { diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs index 324a572f40c98037816870c99151a4789793da1b..660ddd9688f7dc69f3ec3c52452122fd807257ad 100644 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs @@ -40,8 +40,12 @@ pub const MIN_VERSION: Version = Version::new(0, 8, 0); pub const MAX_VERSION: Version = Version::new(0, 8, 0); wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, + imports: { + default: async | trappable, + }, + exports: { + default: async, + }, path: "../extension_api/wit/since_v0.8.0", with: { "worktree": ExtensionWorktree, @@ -65,7 +69,11 @@ pub type ExtensionHttpResponseStream = Arc &'static Linker { static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) + LINKER.get_or_init(|| { + super::new_linker(executor, |linker| { + Extension::add_to_linker::<_, WasmState>(linker, |s| s) + }) + }) } impl From for std::ops::Range { From 4087d9f2ca1d6fec171c6751dbf32adad019acc5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 31 Mar 2026 20:10:05 -0600 Subject: [PATCH 12/13] Remove Claude upsell (#52831) Self-Review Checklist: - [ ] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable Release Notes: - Removed the (broken) Claude ACP upsell dialogue --- crates/agent_ui/src/agent_panel.rs | 11 +- crates/agent_ui/src/ui.rs | 2 - .../src/ui/claude_agent_onboarding_modal.rs | 261 ------------------ crates/title_bar/src/onboarding_banner.rs | 26 +- crates/title_bar/src/title_bar.rs | 21 +- crates/zed_actions/src/lib.rs | 2 - 6 files changed, 23 insertions(+), 300 deletions(-) delete mode 100644 crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e6ef267a95110e745534010bae32b1b1fd6c0f0c..a32f92942682fc0c5efbbcd35a9848c90b761184 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -21,8 +21,8 @@ use settings::{LanguageModelProviderSetting, LanguageModelSelection}; use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _}; use zed_actions::agent::{ - AddSelectionToThread, ConflictContent, OpenClaudeAgentOnboardingModal, ReauthenticateAgent, - ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff, + AddSelectionToThread, ConflictContent, ReauthenticateAgent, ResolveConflictedFilesWithAgent, + ResolveConflictsWithAgent, ReviewBranchDiff, }; use crate::{ @@ -40,7 +40,7 @@ use crate::{ }; use crate::{ DEFAULT_THREAD_TITLE, - ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault}, + ui::{AcpOnboardingModal, HoldForDefault}, }; use crate::{ExpandMessageEditor, ThreadHistoryView}; use crate::{ManageProfiles, ThreadHistoryViewEvent}; @@ -245,11 +245,6 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { AcpOnboardingModal::toggle(workspace, window, cx) }) - .register_action( - |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| { - ClaudeCodeOnboardingModal::toggle(workspace, window, cx) - }, - ) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 16732951ce67d76ca8d65259e309c4b81df30c3b..d43b7e4b043bcd1b155699c5eea3ca695585b94b 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,6 +1,5 @@ mod acp_onboarding_modal; mod agent_notification; -mod claude_agent_onboarding_modal; mod end_trial_upsell; mod hold_for_default; mod mention_crease; @@ -9,7 +8,6 @@ mod undo_reject_toast; pub use acp_onboarding_modal::*; pub use agent_notification::*; -pub use claude_agent_onboarding_modal::*; pub use end_trial_upsell::*; pub use hold_for_default::*; pub use mention_crease::*; diff --git a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs b/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs deleted file mode 100644 index 5b7e58eb4fd79a5075446dad997c2642fedf32a6..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/ui/claude_agent_onboarding_modal.rs +++ /dev/null @@ -1,261 +0,0 @@ -use agent_servers::CLAUDE_AGENT_ID; -use gpui::{ - ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, - linear_color_stop, linear_gradient, -}; -use ui::{TintColor, Vector, VectorName, prelude::*}; -use workspace::{ModalView, Workspace}; - -use crate::{Agent, agent_panel::AgentPanel}; - -macro_rules! claude_agent_onboarding_event { - ($name:expr) => { - telemetry::event!($name, source = "ACP Claude Code Onboarding"); - }; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { - telemetry::event!($name, source = "ACP Claude Code Onboarding", $($key $(= $value)?),+); - }; -} - -pub struct ClaudeCodeOnboardingModal { - focus_handle: FocusHandle, - workspace: Entity, -} - -impl ClaudeCodeOnboardingModal { - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let workspace_entity = cx.entity(); - workspace.toggle_modal(window, cx, |_window, cx| Self { - workspace: workspace_entity, - focus_handle: cx.focus_handle(), - }); - } - - fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.new_agent_thread( - Agent::Custom { - id: CLAUDE_AGENT_ID.into(), - }, - window, - cx, - ); - }); - } - }); - - cx.emit(DismissEvent); - - claude_agent_onboarding_event!("Open Panel Clicked"); - } - - fn view_docs(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx); - cx.notify(); - - claude_agent_onboarding_event!("Documentation Link Clicked"); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl EventEmitter for ClaudeCodeOnboardingModal {} - -impl Focusable for ClaudeCodeOnboardingModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for ClaudeCodeOnboardingModal {} - -impl Render for ClaudeCodeOnboardingModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let illustration_element = |icon: IconName, label: Option, opacity: f32| { - h_flex() - .px_1() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.05)) - .border_1() - .border_color(cx.theme().colors().border) - .border_dashed() - .child( - Icon::new(icon) - .size(IconSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), - ) - .map(|this| { - if let Some(label_text) = label { - this.child( - Label::new(label_text) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - div().w_16().h_1().rounded_full().bg(cx - .theme() - .colors() - .element_active - .opacity(0.6)), - ) - } - }) - .opacity(opacity) - }; - - let illustration = h_flex() - .relative() - .h(rems_from_px(126.)) - .bg(cx.theme().colors().editor_background) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .justify_center() - .gap_8() - .rounded_t_md() - .overflow_hidden() - .child( - div().absolute().inset_0().w(px(515.)).h(px(126.)).child( - Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), - ), - ) - .child(div().absolute().inset_0().size_full().bg(linear_gradient( - 0., - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.1), - 0.9, - ), - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.), - 0., - ), - ))) - .child( - div() - .absolute() - .inset_0() - .size_full() - .bg(gpui::black().opacity(0.15)), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(257.), - rems_from_px(47.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - v_flex() - .gap_1p5() - .child(illustration_element(IconName::Stop, None, 0.15)) - .child(illustration_element( - IconName::AiGemini, - Some("New Gemini CLI Thread".into()), - 0.3, - )) - .child( - h_flex() - .pl_1() - .pr_2() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.2)) - .border_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::AiClaude) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("New Claude Agent Thread").size(LabelSize::Small)), - ) - .child(illustration_element( - IconName::Stop, - Some("Your Agent Here".into()), - 0.3, - )) - .child(illustration_element(IconName::Stop, None, 0.15)), - ); - - let heading = v_flex() - .w_full() - .gap_1() - .child( - Label::new("Beta Release") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Headline::new("Claude Agent: Natively in Zed").size(HeadlineSize::Large)); - - let copy = "Powered by the Agent Client Protocol, you can now run Claude Agent as\na first-class citizen in Zed's agent panel."; - - let open_panel_button = Button::new("open-panel", "Start with Claude Agent") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::open_panel)); - - let docs_button = Button::new("add-other-agents", "Add Other Agents") - .end_icon( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Indicator) - .color(Color::Muted), - ) - .full_width() - .on_click(cx.listener(Self::view_docs)); - - let close_button = h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( - |_, _: &ClickEvent, _window, cx| { - claude_agent_onboarding_event!("Canceled", trigger = "X click"); - cx.emit(DismissEvent); - }, - )), - ); - - v_flex() - .id("acp-onboarding") - .key_context("AcpOnboardingModal") - .relative() - .w(rems(34.)) - .h_full() - .elevation_3(cx) - .track_focus(&self.focus_handle(cx)) - .overflow_hidden() - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { - claude_agent_onboarding_event!("Canceled", trigger = "Action"); - cx.emit(DismissEvent); - })) - .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| { - this.focus_handle.focus(window, cx); - })) - .child(illustration) - .child( - v_flex() - .p_4() - .gap_2() - .child(heading) - .child(Label::new(copy).color(Color::Muted)) - .child( - v_flex() - .w_full() - .mt_2() - .gap_1() - .child(open_panel_button) - .child(docs_button), - ), - ) - .child(close_button) - } -} diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index f96ce3a92740da4a0aac3dc154384f20f3b05eb0..96400a91a0a26fdc6a4c1acb6387f27c3077e393 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -1,3 +1,7 @@ +// This module provides infrastructure for showing onboarding banners in the title bar. +// It's currently not in use but is kept for future feature announcements. +#![allow(dead_code)] + use gpui::{Action, Entity, Global, Render, SharedString}; use ui::{ButtonLike, Tooltip, prelude::*}; use util::ResultExt; @@ -94,21 +98,21 @@ fn persist_dismissed(source: &str, cx: &mut App) { } pub fn restore_banner(cx: &mut App) { - cx.defer(|cx| { - cx.global::() - .entity - .clone() - .update(cx, |this, cx| { + if let Some(banner_global) = cx.try_global::() { + let entity = banner_global.entity.clone(); + cx.defer(move |cx| { + entity.update(cx, |this, cx| { this.dismissed = false; cx.notify(); }); - }); + }); - let source = &cx.global::().entity.read(cx).source; - let dismissed_at = dismissed_at_key(source); - let kvp = db::kvp::KeyValueStore::global(cx); - cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await) - .detach_and_log_err(cx); + let source = &cx.global::().entity.read(cx).source; + let dismissed_at = dismissed_at_key(source); + let kvp = db::kvp::KeyValueStore::global(cx); + cx.spawn(async move |_| kvp.delete_kvp(dismissed_at).await) + .detach_and_log_err(cx); + } } impl Render for OnboardingBanner { diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 42c348bacb680e2a09586d0dc0279fc8c95d1604..440249907adb6d29602ad8e950d0fd26a2d1c31d 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -155,7 +155,7 @@ pub struct TitleBar { multi_workspace: Option>, application_menu: Option>, _subscriptions: Vec, - banner: Entity, + banner: Option>, update_version: Entity, screen_share_popover_handle: PopoverMenuHandle, _diagnostics_subscription: Option, @@ -246,7 +246,9 @@ impl Render for TitleBar { children.push(self.render_collaborator_list(window, cx).into_any_element()); if title_bar_settings.show_onboarding_banner { - children.push(self.banner.clone().into_any_element()) + if let Some(banner) = &self.banner { + children.push(banner.clone().into_any_element()) + } } let status = self.client.status(); @@ -385,19 +387,6 @@ impl TitleBar { })); } - let banner = cx.new(|cx| { - OnboardingBanner::new( - "ACP Claude Code Onboarding", - IconName::AiClaude, - "Claude Agent", - Some("Introducing:".into()), - zed_actions::agent::OpenClaudeAgentOnboardingModal.boxed_clone(), - cx, - ) - // When updating this to a non-AI feature release, remove this line. - .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) - }); - let update_version = cx.new(|cx| UpdateVersion::new(cx)); let platform_titlebar = cx.new(|cx| { let mut titlebar = PlatformTitleBar::new(id, cx); @@ -416,7 +405,7 @@ impl TitleBar { user_store, client, _subscriptions: subscriptions, - banner, + banner: None, update_version, screen_share_popover_handle: PopoverMenuHandle::default(), _diagnostics_subscription: None, diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 75b21c528a1e6952700264a154ab4c15045149b0..66ccf9c41c1e1cfcb821e03b4e9b7d4803f53c0b 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -450,8 +450,6 @@ pub mod agent { OpenOnboardingModal, /// Opens the ACP onboarding modal. OpenAcpOnboardingModal, - /// Opens the Claude Agent onboarding modal. - OpenClaudeAgentOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent. From 971775e3b266950fbee99d97b863afe769374321 Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Tue, 31 Mar 2026 19:50:01 -0700 Subject: [PATCH 13/13] gpui: Implement audible system bell (#47531) Relates to #5303 and https://github.com/zed-industries/zed/issues/40826#issuecomment-3684556858 although I haven't found anywhere an actual request for `gpui` itself to support a system alert sound. ### What Basically, this PR adds a function that triggers an OS-dependent alert sound, commonly used by terminal applications for `\a` / `BEL`, and GUI applications to indicate an action failed in some small way (e.g. no search results found, unable to move cursor, button disabled). Also updated the `input` example, which now plays the bell if the user presses backspace with nothing behind the cursor to delete, or delete with nothing in front of the cursor. Test with `cargo run --example input --features gpui_platform/font-kit`. ### Why If this is merged, I plan to take a second step: - Add a new Zed setting (probably something like `terminal.audible_bell`) - If enabled, `printf '\a'`, `tput bel` etc. would call this new API to play an audible sound This isn't the super-shiny dream of #5303 but it would allow users to more easily configure tasks to notify when done. Plus, any TUI/CLI apps that expect this functionality will work. Also, I think many terminal users expect something like this (WezTerm, iTerm, etc. almost all support this). ### Notes ~I was only able to test on macOS and Windows, so if there are any Linux users who could verify this works for X11 / Wayland that would be a huge help! If not I can try~ Confirmed Wayland + X11 both working when I ran the example on a NixOS desktop Release Notes: - N/A --- Cargo.lock | 11 +++++++++++ Cargo.toml | 2 ++ crates/gpui/examples/input.rs | 14 ++++++++++++-- crates/gpui/src/platform.rs | 2 ++ crates/gpui/src/window.rs | 6 ++++++ crates/gpui_linux/src/linux/wayland/client.rs | 4 ++++ crates/gpui_linux/src/linux/wayland/window.rs | 12 ++++++++++++ crates/gpui_linux/src/linux/x11/window.rs | 5 +++++ crates/gpui_macos/Cargo.toml | 1 + crates/gpui_macos/src/window.rs | 5 +++++ crates/gpui_windows/src/window.rs | 9 ++++++++- 11 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f68704cb6ef68887b102f7f6a1a37c0fe694f662..bfd80726843695dbfcb4baf1db4fe3e6ca9a4682 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7601,6 +7601,7 @@ dependencies = [ "media", "metal", "objc", + "objc2-app-kit", "parking_lot", "pathfinder_geometry", "raw-window-handle", @@ -11211,6 +11212,16 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-audio-toolbox" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index d1271e0166677fb4069b5917f320e57c755263b4..3a393237ab9f5a5a8cd4b02517f6d22382ff51ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -604,6 +604,7 @@ nbformat = "1.2.0" nix = "0.29" num-format = "0.4.4" objc = "0.2" +objc2-app-kit = { version = "0.3", default-features = false, features = [ "NSGraphics" ] } objc2-foundation = { version = "=0.3.2", default-features = false, features = [ "NSArray", "NSAttributedString", @@ -821,6 +822,7 @@ features = [ "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Console", + "Win32_System_Diagnostics_Debug", "Win32_System_DataExchange", "Win32_System_IO", "Win32_System_LibraryLoader", diff --git a/crates/gpui/examples/input.rs b/crates/gpui/examples/input.rs index d15d791cd008883506389cc7bb16dbad765969c0..370e27de7d54c317af6683c240f343e750c68698 100644 --- a/crates/gpui/examples/input.rs +++ b/crates/gpui/examples/input.rs @@ -85,14 +85,24 @@ impl TextInput { fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { - self.select_to(self.previous_boundary(self.cursor_offset()), cx) + let prev = self.previous_boundary(self.cursor_offset()); + if self.cursor_offset() == prev { + window.play_system_bell(); + return; + } + self.select_to(prev, cx) } self.replace_text_in_range(None, "", window, cx) } fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { if self.selected_range.is_empty() { - self.select_to(self.next_boundary(self.cursor_offset()), cx) + let next = self.next_boundary(self.cursor_offset()); + if self.cursor_offset() == next { + window.play_system_bell(); + return; + } + self.select_to(next, cx) } self.replace_text_in_range(None, "", window, cx) } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 806a34040a4ec685c3d5c6ec01f47b5026e349a6..efca26a6b4802037a96490bf81f7d1c5c1d8b298 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -689,6 +689,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn update_ime_position(&self, _bounds: Bounds); + fn play_system_bell(&self) {} + #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { None diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 48c381e5275e950bd6754541fedbab03ae3d64c2..7790480e32149fa33dfd082df7a8cdbb09568134 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -5024,6 +5024,12 @@ impl Window { .set_tabbing_identifier(tabbing_identifier) } + /// Request the OS to play an alert sound. On some platforms this is associated + /// with the window, for others it's just a simple global function call. + pub fn play_system_bell(&self) { + self.platform_window.play_system_bell() + } + /// Toggles the inspector mode on this window. #[cfg(any(feature = "inspector", debug_assertions))] pub fn toggle_inspector(&mut self, cx: &mut App) { diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index b65a203dd3448ba191b7e2f5ae0f5b6c396545a8..10f4aab0db19978302143519dd6e2a7e4d25ec4d 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -58,6 +58,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{ zxdg_decoration_manager_v1, zxdg_toplevel_decoration_v1, }; use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; +use wayland_protocols::xdg::system_bell::v1::client::xdg_system_bell_v1; use wayland_protocols::{ wp::cursor_shape::v1::client::{wp_cursor_shape_device_v1, wp_cursor_shape_manager_v1}, xdg::dialog::v1::client::xdg_wm_dialog_v1::{self, XdgWmDialogV1}, @@ -129,6 +130,7 @@ pub struct Globals { pub text_input_manager: Option, pub gesture_manager: Option, pub dialog: Option, + pub system_bell: Option, pub executor: ForegroundExecutor, } @@ -170,6 +172,7 @@ impl Globals { text_input_manager: globals.bind(&qh, 1..=1, ()).ok(), gesture_manager: globals.bind(&qh, 1..=3, ()).ok(), dialog: globals.bind(&qh, dialog_v..=dialog_v, ()).ok(), + system_bell: globals.bind(&qh, 1..=1, ()).ok(), executor, qh, } @@ -1069,6 +1072,7 @@ impl Dispatch for WaylandClientStat } delegate_noop!(WaylandClientStatePtr: ignore xdg_activation_v1::XdgActivationV1); +delegate_noop!(WaylandClientStatePtr: ignore xdg_system_bell_v1::XdgSystemBellV1); delegate_noop!(WaylandClientStatePtr: ignore wl_compositor::WlCompositor); delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_device_v1::WpCursorShapeDeviceV1); delegate_noop!(WaylandClientStatePtr: ignore wp_cursor_shape_manager_v1::WpCursorShapeManagerV1); diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index c4ff55fc80cc4d14069dd510b8e6855c17096773..1e3af66c59858c435ca3da093a1c48056b77667e 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -1479,6 +1479,18 @@ impl PlatformWindow for WaylandWindow { fn gpu_specs(&self) -> Option { self.borrow().renderer.gpu_specs().into() } + + fn play_system_bell(&self) { + let state = self.borrow(); + let surface = if state.surface_state.toplevel().is_some() { + Some(&state.surface) + } else { + None + }; + if let Some(bell) = state.globals.system_bell.as_ref() { + bell.ring(surface); + } + } } fn update_window(mut state: RefMut) { diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 5e1287976cbb3ba9bc2c1571fa9e215f47fdd615..1974cc0bb28f62da4d7dcb3e9fca92b6324470bb 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -1846,4 +1846,9 @@ impl PlatformWindow for X11Window { fn gpu_specs(&self) -> Option { self.0.state.borrow().renderer.gpu_specs().into() } + + fn play_system_bell(&self) { + // Volume 0% means don't increase or decrease from system volume + let _ = self.0.xcb.bell(0); + } } diff --git a/crates/gpui_macos/Cargo.toml b/crates/gpui_macos/Cargo.toml index 06e5d0e7321af523a249f19ec0d5ac50e2da5d3f..3626bbd05e8a7c7fa2ae577f11e5277da995d2f7 100644 --- a/crates/gpui_macos/Cargo.toml +++ b/crates/gpui_macos/Cargo.toml @@ -48,6 +48,7 @@ mach2.workspace = true media.workspace = true metal.workspace = true objc.workspace = true +objc2-app-kit.workspace = true parking_lot.workspace = true pathfinder_geometry = "0.5" raw-window-handle = "0.6" diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 398cf46eab09dc8412ffdda8eb550b8ad4e09b40..ace36d695401ce76949129197dcd05135508f7d3 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -49,6 +49,7 @@ use objc::{ runtime::{BOOL, Class, NO, Object, Protocol, Sel, YES}, sel, sel_impl, }; +use objc2_app_kit::NSBeep; use parking_lot::Mutex; use raw_window_handle as rwh; use smallvec::SmallVec; @@ -1676,6 +1677,10 @@ impl PlatformWindow for MacWindow { } } + fn play_system_bell(&self) { + unsafe { NSBeep() } + } + #[cfg(any(test, feature = "test-support"))] fn render_to_image(&self, scene: &gpui::Scene) -> Result { let mut this = self.0.lock(); diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 3a55100dfb75e961f57b977297bfcd2dc2ae2701..92255f93fd95969931c6b1ae8cb465ff628f82cb 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -20,7 +20,9 @@ use windows::{ Foundation::*, Graphics::Dwm::*, Graphics::Gdi::*, - System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*}, + System::{ + Com::*, Diagnostics::Debug::MessageBeep, LibraryLoader::*, Ole::*, SystemServices::*, + }, UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*}, }, core::*, @@ -950,6 +952,11 @@ impl PlatformWindow for WindowsWindow { self.0.update_ime_position(self.0.hwnd, caret_position); } + + fn play_system_bell(&self) { + // MB_OK: The sound specified as the Windows Default Beep sound. + let _ = unsafe { MessageBeep(MB_OK) }; + } } #[implement(IDropTarget)]