From 055a9f21a085ee866501a1a85b95e1db1b669877 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 28 Jul 2025 14:58:43 -0400 Subject: [PATCH 01/11] Fix keybinds and 'No default binding' shown in docs (#35227) Previously: Screenshot 2025-07-28 at 14 49 50 Release Notes: - N/A --- docs/src/extensions/installing-extensions.md | 2 +- docs/src/getting-started.md | 2 +- docs/src/key-bindings.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/extensions/installing-extensions.md b/docs/src/extensions/installing-extensions.md index aed8bef428..801fe5c55c 100644 --- a/docs/src/extensions/installing-extensions.md +++ b/docs/src/extensions/installing-extensions.md @@ -1,6 +1,6 @@ # Installing Extensions -You can search for extensions by launching the Zed Extension Gallery by pressing `cmd-shift-x` (macOS) or `ctrl-shift-x` (Linux), opening the command palette and selecting `zed: extensions` or by selecting "Zed > Extensions" from the menu bar. +You can search for extensions by launching the Zed Extension Gallery by pressing {#kb zed::Extensions} , opening the command palette and selecting {#action zed::Extensions} or by selecting "Zed > Extensions" from the menu bar. Here you can view the extensions that you currently have installed or search and install new ones. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 5940c74b21..22af3b36d7 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -83,6 +83,6 @@ Visit [the AI overview page](./ai/overview.md) to learn how to quickly get start ## Set up your key bindings -To open your custom keymap to add your key bindings, use the {#kb zed::OpenKeymap} keybinding. +To edit your custom keymap and add or remap bindings, you can either use {#kb zed::OpenKeymapEditor} to spawn the Zed Keymap Editor ({#action zed::OpenKeymapEditor}) or you can directly open your Zed Keymap json (`~/.config/zed/keymap.json`) with {#action zed::OpenKeymap}. To access the default key binding set, open the Command Palette with {#kb command_palette::Toggle} and search for "zed: open default keymap". See [Key Bindings](./key-bindings.md) for more info. diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 90aa400bb4..9984f234ad 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -18,7 +18,7 @@ You can also enable `vim_mode`, which adds vim bindings too. ## User keymaps -Zed reads your keymap from `~/.config/zed/keymap.json`. You can open the file within Zed with {#kb zed::OpenKeymap}, or via `zed: Open Keymap` in the command palette. +Zed reads your keymap from `~/.config/zed/keymap.json`. You can open the file within Zed with {#action zed::OpenKeymap} from the command palette or to spawn the Zed Keymap Editor ({#action zed::OpenKeymapEditor}) use {#kb zed::OpenKeymapEditor}. The file contains a JSON array of objects with `"bindings"`. If no `"context"` is set the bindings are always active. If it is set the binding is only active when the [context matches](#contexts). From c3920b806b896b246bd91dcf3bd3ac4bbe151457 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 28 Jul 2025 21:07:58 +0200 Subject: [PATCH 02/11] editor: Ensure code actions menu doesn't grow beyond its max size (#35211) This resolves the same issue as fixed by https://github.com/zed-industries/zed/pull/34939/commits/d295409f0fc9c1ccd5e06e91c1c5365e96f16486 for the code actions menu. No release notes since this only occurs on Nightly. Release Notes: - N/A --- crates/editor/src/code_context_menus.rs | 2 +- crates/ui/src/components/popover.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 52446ceafc..9f842836ed 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -844,7 +844,7 @@ impl CompletionsMenu { .with_sizing_behavior(ListSizingBehavior::Infer) .w(rems(34.)); - Popover::new().child(div().child(list)).into_any_element() + Popover::new().child(list).into_any_element() } fn render_aside( diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs index 24460f6d9c..7143514c52 100644 --- a/crates/ui/src/components/popover.rs +++ b/crates/ui/src/components/popover.rs @@ -50,7 +50,7 @@ impl RenderOnce for Popover { v_flex() .elevation_2(cx) .py(POPOVER_Y_PADDING / 2.) - .children(self.children), + .child(div().children(self.children)), ) .when_some(self.aside, |this, aside| { this.child( From b64977f6f49a201dabd74b06e08f902981754f83 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Mon, 28 Jul 2025 15:38:20 -0400 Subject: [PATCH 03/11] Use zed settings to detect `.zed` folders (#35224) Behind-the-scenes enhancement of https://github.com/zed-industries/zed/pull/35221 Release Notes: - N/A --- crates/assistant_tools/src/edit_file_tool.rs | 288 ++++++++++++++----- 1 file changed, 215 insertions(+), 73 deletions(-) diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 03679401a1..1c41b26092 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -25,6 +25,7 @@ use language::{ }; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; +use paths; use project::{ Project, ProjectPath, lsp_store::{FormatTrigger, LspFormatTarget}, @@ -141,27 +142,32 @@ impl Tool for EditFileTool { return false; }; - let path = Path::new(&input.path); - - // If any path component is ".zed", then this could affect + // If any path component matches the local settings folder, then this could affect // the editor in ways beyond the project source, so prompt. + let local_settings_folder = paths::local_settings_folder_relative_path(); + let path = Path::new(&input.path); if path .components() - .any(|component| component.as_os_str() == ".zed") + .any(|component| component.as_os_str() == local_settings_folder.as_os_str()) { return true; } - // If the path is outside the project, then prompt. - let is_outside_project = project - .read(cx) - .find_project_path(&input.path, cx) - .is_none(); - if is_outside_project { - return true; + // It's also possible that the global config dir is configured to be inside the project, + // so check for that edge case too. + if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { + if canonical_path.starts_with(paths::config_dir()) { + return true; + } } - false + // Check if path is inside the global config directory + // First check if it's already inside project - if not, try to canonicalize + let project_path = project.read(cx).find_project_path(&input.path, cx); + + // If the path is inside the project, and it's not one of the above edge cases, + // then no confirmation is necessary. Otherwise, confirmation is necessary. + project_path.is_none() } fn may_perform_edits(&self) -> bool { @@ -187,8 +193,16 @@ impl Tool for EditFileTool { let mut description = input.display_description.clone(); // Add context about why confirmation may be needed - if path.components().any(|c| c.as_os_str() == ".zed") { - description.push_str(" (Zed settings)"); + let local_settings_folder = paths::local_settings_folder_relative_path(); + if path + .components() + .any(|c| c.as_os_str() == local_settings_folder.as_os_str()) + { + description.push_str(" (local settings)"); + } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) { + if canonical_path.starts_with(paths::config_dir()) { + description.push_str(" (global settings)"); + } } description @@ -1219,19 +1233,20 @@ async fn build_buffer_diff( #[cfg(test)] mod tests { use super::*; + use ::fs::Fs; use client::TelemetrySettings; - use fs::{FakeFs, Fs}; use gpui::{TestAppContext, UpdateGlobal}; use language_model::fake_provider::FakeLanguageModel; use serde_json::json; use settings::SettingsStore; + use std::fs; use util::path; #[gpui::test] async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let action_log = cx.new(|_| ActionLog::new(project.clone())); @@ -1321,7 +1336,7 @@ mod tests { ) -> anyhow::Result { init_test(cx); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree( "/root", json!({ @@ -1433,11 +1448,25 @@ mod tests { }); } + fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) { + cx.update(|cx| { + // Set custom data directory (config will be under data_dir/config) + paths::set_custom_data_dir(data_dir.to_str().unwrap()); + + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + language::init(cx); + TelemetrySettings::register(cx); + agent_settings::AgentSettings::register(cx); + Project::init_settings(cx); + }); + } + #[gpui::test] async fn test_format_on_save(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({"src": {}})).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; @@ -1636,7 +1665,7 @@ mod tests { async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({"src": {}})).await; // Create a simple file with trailing whitespace @@ -1773,7 +1802,7 @@ mod tests { async fn test_needs_confirmation(cx: &mut TestAppContext) { init_test(cx); let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/root", json!({})).await; // Test 1: Path with .zed component should require confirmation @@ -1847,44 +1876,66 @@ mod tests { } #[gpui::test] - fn test_ui_text_with_confirmation_context(cx: &mut TestAppContext) { - init_test(cx); + async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { + // Set up a custom config directory for testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + let tool = Arc::new(EditFileTool); - // Test ui_text shows context for .zed paths - let input_zed = json!({ - "display_description": "Update settings", - "path": ".zed/settings.json", - "mode": "edit" - }); - cx.update(|_cx| { - let ui_text = tool.ui_text(&input_zed); - assert_eq!( - ui_text, "Update settings (Zed settings)", - "UI text should indicate Zed settings file" - ); - }); + // Test ui_text shows context for various paths + let test_cases = vec![ + ( + json!({ + "display_description": "Update config", + "path": ".zed/settings.json", + "mode": "edit" + }), + "Update config (local settings)", + ".zed path should show local settings context", + ), + ( + json!({ + "display_description": "Fix bug", + "path": "src/.zed/local.json", + "mode": "edit" + }), + "Fix bug (local settings)", + "Nested .zed path should show local settings context", + ), + ( + json!({ + "display_description": "Update readme", + "path": "README.md", + "mode": "edit" + }), + "Update readme", + "Normal path should not show additional context", + ), + ( + json!({ + "display_description": "Edit config", + "path": "config.zed", + "mode": "edit" + }), + "Edit config", + ".zed as extension should not show context", + ), + ]; - // Test ui_text for normal paths - let input_normal = json!({ - "display_description": "Edit source file", - "path": "src/main.rs", - "mode": "edit" - }); - cx.update(|_cx| { - let ui_text = tool.ui_text(&input_normal); - assert_eq!( - ui_text, "Edit source file", - "UI text should not have additional context for normal paths" - ); - }); + for (input, expected_text, description) in test_cases { + cx.update(|_cx| { + let ui_text = tool.ui_text(&input); + assert_eq!(ui_text, expected_text, "Failed for case: {}", description); + }); + } } #[gpui::test] async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { init_test(cx); let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); // Create a project in /project directory fs.insert_tree("/project", json!({})).await; @@ -1918,33 +1969,58 @@ mod tests { } #[gpui::test] - async fn test_needs_confirmation_zed_paths(cx: &mut TestAppContext) { - init_test(cx); + async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) { + // Set up a custom data directory for testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/home/user/myproject", json!({})).await; let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await; - // Test various .zed path patterns + // Get the actual local settings folder name + let local_settings_folder = paths::local_settings_folder_relative_path(); + + // Test various config path patterns let test_cases = vec![ - (".zed/settings.json", true, "Top-level .zed file"), - ("myproject/.zed/settings.json", true, ".zed in project path"), - ("src/.zed/config.toml", true, ".zed in subdirectory"), ( - ".zed.backup/file.txt", + format!("{}/settings.json", local_settings_folder.display()), true, - ".zed.backup is outside project (not a .zed component issue)", + "Top-level local settings file".to_string(), ), ( - "my.zed/file.txt", + format!( + "myproject/{}/settings.json", + local_settings_folder.display() + ), true, - "my.zed is outside project (not a .zed component issue)", + "Local settings in project path".to_string(), ), - ("myproject/src/file.zed", false, ".zed as file extension"), ( - "myproject/normal/path/file.rs", + format!("src/{}/config.toml", local_settings_folder.display()), + true, + "Local settings in subdirectory".to_string(), + ), + ( + ".zed.backup/file.txt".to_string(), + true, + ".zed.backup is outside project".to_string(), + ), + ( + "my.zed/file.txt".to_string(), + true, + "my.zed is outside project".to_string(), + ), + ( + "myproject/src/file.zed".to_string(), false, - "Normal file without .zed", + ".zed as file extension".to_string(), + ), + ( + "myproject/normal/path/file.rs".to_string(), + false, + "Normal file without config paths".to_string(), ), ]; @@ -1966,11 +2042,69 @@ mod tests { } } + #[gpui::test] + async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) { + // Set up a custom data directory for testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + + let tool = Arc::new(EditFileTool); + let fs = project::FakeFs::new(cx.executor()); + + // Create test files in the global config directory + let global_config_dir = paths::config_dir(); + fs::create_dir_all(&global_config_dir).unwrap(); + let global_settings_path = global_config_dir.join("settings.json"); + fs::write(&global_settings_path, "{}").unwrap(); + + fs.insert_tree("/project", json!({})).await; + let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; + + // Test global config paths + let test_cases = vec![ + ( + global_settings_path.to_str().unwrap().to_string(), + true, + "Global settings file should require confirmation", + ), + ( + global_config_dir + .join("keymap.json") + .to_str() + .unwrap() + .to_string(), + true, + "Global keymap file should require confirmation", + ), + ( + "project/normal_file.rs".to_string(), + false, + "Normal project file should not require confirmation", + ), + ]; + + for (path, should_confirm, description) in test_cases { + let input = json!({ + "display_description": "Edit file", + "path": path, + "mode": "edit" + }); + cx.update(|cx| { + assert_eq!( + tool.needs_confirmation(&input, &project, cx), + should_confirm, + "Failed for case: {}", + description + ); + }); + } + } + #[gpui::test] async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { init_test(cx); let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); // Create multiple worktree directories fs.insert_tree( @@ -2052,7 +2186,7 @@ mod tests { async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { init_test(cx); let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree( "/project", json!({ @@ -2112,7 +2246,7 @@ mod tests { } #[gpui::test] - async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) { + async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) { init_test(cx); let tool = Arc::new(EditFileTool); @@ -2124,8 +2258,8 @@ mod tests { "path": ".zed/settings.json", "mode": "edit" }), - "Update config (Zed settings)", - ".zed path should show Zed settings context", + "Update config (local settings)", + ".zed path should show local settings context", ), ( json!({ @@ -2133,8 +2267,8 @@ mod tests { "path": "src/.zed/local.json", "mode": "edit" }), - "Fix bug (Zed settings)", - "Nested .zed path should show Zed settings context", + "Fix bug (local settings)", + "Nested .zed path should show local settings context", ), ( json!({ @@ -2168,7 +2302,7 @@ mod tests { async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { init_test(cx); let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree( "/project", json!({ @@ -2235,9 +2369,12 @@ mod tests { #[gpui::test] async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { - init_test(cx); + // Set up with custom directories for deterministic testing + let temp_dir = tempfile::tempdir().unwrap(); + init_test_with_config(cx, temp_dir.path()); + let tool = Arc::new(EditFileTool); - let fs = FakeFs::new(cx.executor()); + let fs = project::FakeFs::new(cx.executor()); fs.insert_tree("/project", json!({})).await; let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; @@ -2249,9 +2386,14 @@ mod tests { }); // Test that all paths that normally require confirmation are bypassed + let global_settings_path = paths::config_dir().join("settings.json"); + fs::create_dir_all(paths::config_dir()).unwrap(); + fs::write(&global_settings_path, "{}").unwrap(); + let test_cases = vec![ ".zed/settings.json", "project/.zed/config.toml", + global_settings_path.to_str().unwrap(), "/etc/hosts", "/absolute/path/file.txt", "../outside/project.txt", From cf13a766184ebe334a275a4cafa35aac1394f2ac Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Tue, 29 Jul 2025 01:15:29 +0530 Subject: [PATCH 04/11] editor: Prioritize fuzzy score over sort positions in code completion sort (#35229) We already prioritize matches that come after separators like `_`: https://github.com/zed-industries/zed/blob/cef7d53607381975ea00d6302d8a9aab3c40eb1f/crates/fuzzy/src/matcher.rs#L274 and deprioritize non-consecutive matches using distance penalty: https://github.com/zed-industries/zed/blob/cef7d53607381975ea00d6302d8a9aab3c40eb1f/crates/fuzzy/src/matcher.rs#L281 In completion sort, letting fuzzy score be the primary sort factor and sort positions be secondary yields better results upon testing. We still need sort positions because of this kind of test case: https://github.com/zed-industries/zed/blob/cef7d53607381975ea00d6302d8a9aab3c40eb1f/crates/editor/src/code_completion_tests.rs#L195-L217 Before/After: image image Release Notes: - N/A --- crates/editor/src/code_completion_tests.rs | 24 +++++++++++++++++++++- crates/editor/src/code_context_menus.rs | 4 ++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/code_completion_tests.rs b/crates/editor/src/code_completion_tests.rs index 4f9822b597..fd8db29584 100644 --- a/crates/editor/src/code_completion_tests.rs +++ b/crates/editor/src/code_completion_tests.rs @@ -94,7 +94,7 @@ async fn test_fuzzy_score(cx: &mut TestAppContext) { filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await; assert_eq!(matches[0].string, "set_text"); assert_eq!(matches[1].string, "set_text_style_refinement"); - assert_eq!(matches[2].string, "set_context_menu_options"); + assert_eq!(matches[2].string, "set_placeholder_text"); } // fuzzy filter text over label, sort_text and sort_kind @@ -216,6 +216,28 @@ async fn test_sort_positions(cx: &mut TestAppContext) { assert_eq!(matches[0].string, "rounded-full"); } +#[gpui::test] +async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) { + let completions = vec![ + CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score + CompletionBuilder::function( + "language_servers_running_disk_based_diagnostics", + None, + "7fffffff", + ), // 0.168 fuzzy score + CompletionBuilder::function("code_lens", None, "7fffffff"), // 3.2 fuzzy score + CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"), // 3.2 fuzzy score + CompletionBuilder::function("fetch_code_lens", None, "7fffffff"), // 3.2 fuzzy score + ]; + + let matches = + filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await; + + assert_eq!(matches[0].string, "code_lens"); + assert_eq!(matches[1].string, "lsp_code_lens"); + assert_eq!(matches[2].string, "fetch_code_lens"); +} + async fn test_for_each_prefix( target: &str, completions: &Vec, diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 9f842836ed..4ae2a14ca7 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1057,9 +1057,9 @@ impl CompletionsMenu { enum MatchTier<'a> { WordStartMatch { sort_exact: Reverse, - sort_positions: Vec, sort_snippet: Reverse, sort_score: Reverse>, + sort_positions: Vec, sort_text: Option<&'a str>, sort_kind: usize, sort_label: &'a str, @@ -1137,9 +1137,9 @@ impl CompletionsMenu { MatchTier::WordStartMatch { sort_exact, - sort_positions, sort_snippet, sort_score, + sort_positions, sort_text, sort_kind, sort_label, From 994d400ab8c700549c9cec42bd331a7a789301ee Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 28 Jul 2025 15:59:01 -0400 Subject: [PATCH 05/11] Map shift-escape in the Jetbrains keymaps (#35230) Release Notes: - Added support for closing docks (sidebars) with `shift-escape` in the Jetbrains keymaps. --- assets/keymaps/linux/jetbrains.json | 6 +++++- assets/keymaps/macos/jetbrains.json | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 629333663d..f602923fea 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -4,6 +4,7 @@ "ctrl-alt-s": "zed::OpenSettings", "ctrl-{": "pane::ActivatePreviousItem", "ctrl-}": "pane::ActivateNextItem", + "shift-escape": null, // Unmap workspace::zoom "ctrl-f2": "debugger::Stop", "f6": "debugger::Pause", "f7": "debugger::StepInto", @@ -151,6 +152,9 @@ { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } }, { "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", - "bindings": { "escape": "editor::ToggleFocus" } + "bindings": { + "escape": "editor::ToggleFocus", + "shift-escape": "workspace::CloseActiveDock" + } } ] diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index e8b796f534..0f86f98878 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -4,6 +4,7 @@ "cmd-{": "pane::ActivatePreviousItem", "cmd-}": "pane::ActivateNextItem", "cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset + "shift-escape": null, // Unmap workspace::zoom "ctrl-f2": "debugger::Stop", "f6": "debugger::Pause", "f7": "debugger::StepInto", @@ -151,6 +152,9 @@ { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, { "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", - "bindings": { "escape": "editor::ToggleFocus" } + "bindings": { + "escape": "editor::ToggleFocus", + "shift-escape": "workspace::CloseActiveDock" + } } ] From ab90ed41da47262bb839744e284fd3013b8e16bc Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 28 Jul 2025 16:22:54 -0400 Subject: [PATCH 06/11] collab: Remove `POST /billing/subscriptions/sync` endpoint (#35232) This PR removes the `POST /billing/subscriptions/sync` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api.rs | 1 - crates/collab/src/api/billing.rs | 69 +------------------------------- 2 files changed, 1 insertion(+), 69 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 3b0f5396a7..5cb26eb507 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -106,7 +106,6 @@ pub fn routes(rpc_server: Arc) -> Router<(), Body> { .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens)) .route("/users/:id/update_plan", post(update_plan)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) - .merge(billing::router()) .merge(contributors::router()) .layer( ServiceBuilder::new() diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 9a27e22f87..1cb20173c1 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,15 +1,13 @@ use anyhow::{Context as _, bail}; -use axum::{Extension, Json, Router, extract, routing::post}; use chrono::{DateTime, Utc}; use collections::{HashMap, HashSet}; -use reqwest::StatusCode; use sea_orm::ActiveValue; -use serde::{Deserialize, Serialize}; use std::{sync::Arc, time::Duration}; use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus}; use util::{ResultExt, maybe}; use zed_llm_client::LanguageModelProvider; +use crate::AppState; use crate::db::billing_subscription::{ StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, }; @@ -19,7 +17,6 @@ use crate::stripe_client::{ StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, StripeSubscriptionId, }; -use crate::{AppState, Error, Result}; use crate::{db::UserId, llm::db::LlmDatabase}; use crate::{ db::{ @@ -30,70 +27,6 @@ use crate::{ stripe_billing::StripeBilling, }; -pub fn router() -> Router { - Router::new().route( - "/billing/subscriptions/sync", - post(sync_billing_subscription), - ) -} - -#[derive(Debug, Deserialize)] -struct SyncBillingSubscriptionBody { - github_user_id: i32, -} - -#[derive(Debug, Serialize)] -struct SyncBillingSubscriptionResponse { - stripe_customer_id: String, -} - -async fn sync_billing_subscription( - Extension(app): Extension>, - extract::Json(body): extract::Json, -) -> Result> { - let Some(stripe_client) = app.stripe_client.clone() else { - log::error!("failed to retrieve Stripe client"); - Err(Error::http( - StatusCode::NOT_IMPLEMENTED, - "not supported".into(), - ))? - }; - - let user = app - .db - .get_user_by_github_user_id(body.github_user_id) - .await? - .context("user not found")?; - - let billing_customer = app - .db - .get_billing_customer_by_user_id(user.id) - .await? - .context("billing customer not found")?; - let stripe_customer_id = StripeCustomerId(billing_customer.stripe_customer_id.clone().into()); - - let subscriptions = stripe_client - .list_subscriptions_for_customer(&stripe_customer_id) - .await?; - - for subscription in subscriptions { - let subscription_id = subscription.id.clone(); - - sync_subscription(&app, &stripe_client, subscription) - .await - .with_context(|| { - format!( - "failed to sync subscription {subscription_id} for user {}", - user.id, - ) - })?; - } - - Ok(Json(SyncBillingSubscriptionResponse { - stripe_customer_id: billing_customer.stripe_customer_id.clone(), - })) -} - /// The amount of time we wait in between each poll of Stripe events. /// /// This value should strike a balance between: From 8207621a4a594dd3d8634b594039df468577636e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Mon, 28 Jul 2025 17:00:01 -0400 Subject: [PATCH 07/11] Improve JetBrains keymap for dock toggling (#35234) Follow-up to: - https://github.com/zed-industries/zed/pull/34641 - https://github.com/zed-industries/zed/pull/35230 This improves Zed's behavior with the Jetbrains keymap for toggling specific docks/sidebars with cmd-0 thru cmd-9 (macos) and alt-0 thru alt-9 (linux). Added in https://github.com/zed-industries/zed/pull/34641. Additionally, this also maps `ctrl-b` / `ctrl-alt-`b to their JetBrains equivalents (`editor::GoToDefinition` and `editor::GoToDefinitionSplit`) instead of the default vscode-compatable behavior (toggle left / right dock). This is because we those specific toggles and a default Hide keyboard shortcut (`shift-escape`) added in https://github.com/zed-industries/zed/pull/35230. Thanks to @thomaseizinger for raising this in: - https://github.com/zed-industries/zed/discussions/34643#discussioncomment-13856746 Release Notes: - Improve support keyboard-based dock show/hide in Jetbrains keymap. --- assets/keymaps/linux/jetbrains.json | 21 ++++++++++++++++++--- assets/keymaps/macos/jetbrains.json | 16 ++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index f602923fea..f81f363ae0 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -45,8 +45,8 @@ "ctrl-alt-right": "pane::GoForward", "alt-f7": "editor::FindAllReferences", "ctrl-alt-f7": "editor::FindAllReferences", - // "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock - // "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock + "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock + "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock "ctrl-shift-b": "editor::GoToTypeDefinition", "ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit", "f2": "editor::GoToDiagnostic", @@ -101,12 +101,27 @@ "shift shift": "command_palette::Toggle", "ctrl-alt-shift-n": "project_symbols::Toggle", "alt-0": "git_panel::ToggleFocus", - "alt-1": "workspace::ToggleLeftDock", + "alt-1": "project_panel::ToggleFocus", "alt-5": "debug_panel::ToggleFocus", "alt-6": "diagnostics::Deploy", "alt-7": "outline_panel::ToggleFocus" } }, + { + "context": "Pane", // this is to override the default Pane mappings to switch tabs + "bindings": { + "alt-1": "project_panel::ToggleFocus", + "alt-2": null, // Bookmarks (left dock) + "alt-3": null, // Find Panel (bottom dock) + "alt-4": null, // Run Panel (bottom dock) + "alt-5": "debug_panel::ToggleFocus", + "alt-6": "diagnostics::Deploy", + "alt-7": "outline_panel::ToggleFocus", + "alt-8": null, // Services (bottom dock) + "alt-9": null, // Git History (bottom dock) + "alt-0": "git_panel::ToggleFocus" + } + }, { "context": "Workspace || Editor", "bindings": { diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 0f86f98878..5795d2ac7e 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -109,6 +109,21 @@ "cmd-7": "outline_panel::ToggleFocus" } }, + { + "context": "Pane", // this is to override the default Pane mappings to switch tabs + "bindings": { + "cmd-1": "project_panel::ToggleFocus", + "cmd-2": null, // Bookmarks (left dock) + "cmd-3": null, // Find Panel (bottom dock) + "cmd-4": null, // Run Panel (bottom dock) + "cmd-5": "debug_panel::ToggleFocus", + "cmd-6": "diagnostics::Deploy", + "cmd-7": "outline_panel::ToggleFocus", + "cmd-8": null, // Services (bottom dock) + "cmd-9": null, // Git History (bottom dock) + "cmd-0": "git_panel::ToggleFocus" + } + }, { "context": "Workspace || Editor", "bindings": { @@ -147,6 +162,7 @@ } }, { "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } }, + { "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } }, { "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } }, { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } }, { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, From 7ccf8c2f8cbcd0945df1c6931de93956a9b1b6e3 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 28 Jul 2025 23:10:28 +0200 Subject: [PATCH 08/11] onboarding: Continue work on new flow (#35233) This PR continues the work on the new and revamped onboarding flow. Release Notes: - N/A --- Cargo.lock | 1 + crates/onboarding/Cargo.toml | 1 + crates/onboarding/src/onboarding.rs | 36 +++- crates/onboarding/src/welcome.rs | 276 +++++++++++++++++++++++++ crates/ui/src/components/keybinding.rs | 2 +- 5 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 crates/onboarding/src/welcome.rs diff --git a/Cargo.lock b/Cargo.lock index 8d9a622655..25196fc349 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11028,6 +11028,7 @@ dependencies = [ "ui", "workspace", "workspace-hack", + "zed_actions", ] [[package]] diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml index 693e39d4ca..6ec8f8b162 100644 --- a/crates/onboarding/Cargo.toml +++ b/crates/onboarding/Cargo.toml @@ -26,3 +26,4 @@ theme.workspace = true ui.workspace = true workspace.workspace = true workspace-hack.workspace = true +zed_actions.workspace = true diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index dfdea1ca5b..b675ed2dd7 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -1,3 +1,4 @@ +use crate::welcome::{ShowWelcome, WelcomePage}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; @@ -20,6 +21,8 @@ use workspace::{ open_new, with_active_or_new_workspace, }; +mod welcome; + pub struct OnBoardingFeatureFlag {} impl FeatureFlag for OnBoardingFeatureFlag { @@ -63,12 +66,43 @@ pub fn init(cx: &mut App) { .detach(); }); }); + + cx.on_action(|_: &ShowWelcome, cx| { + with_active_or_new_workspace(cx, |workspace, window, cx| { + workspace + .with_local_workspace(window, cx, |workspace, window, cx| { + let existing = workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::()); + + if let Some(existing) = existing { + workspace.activate_item(&existing, true, true, window, cx); + } else { + let settings_page = WelcomePage::new(cx); + workspace.add_item_to_active_pane( + Box::new(settings_page), + None, + true, + window, + cx, + ) + } + }) + .detach(); + }); + }); + cx.observe_new::(|_, window, cx| { let Some(window) = window else { return; }; - let onboarding_actions = [std::any::TypeId::of::()]; + let onboarding_actions = [ + std::any::TypeId::of::(), + std::any::TypeId::of::(), + ]; CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&onboarding_actions); diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs new file mode 100644 index 0000000000..2ed44cf2ce --- /dev/null +++ b/crates/onboarding/src/welcome.rs @@ -0,0 +1,276 @@ +use gpui::{ + Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, + NoAction, ParentElement, Render, Styled, Window, actions, +}; +use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; +use workspace::{ + NewFile, Open, Workspace, WorkspaceId, + item::{Item, ItemEvent}, +}; +use zed_actions::{Extensions, OpenSettings, command_palette}; + +actions!( + zed, + [ + /// Show the Zed welcome screen + ShowWelcome + ] +); + +const CONTENT: (Section<4>, Section<3>) = ( + Section { + title: "Get Started", + entries: [ + SectionEntry { + icon: IconName::Plus, + title: "New File", + action: &NewFile, + }, + SectionEntry { + icon: IconName::FolderOpen, + title: "Open Project", + action: &Open, + }, + SectionEntry { + // TODO: use proper icon + icon: IconName::Download, + title: "Clone a Repo", + // TODO: use proper action + action: &NoAction, + }, + SectionEntry { + icon: IconName::ListCollapse, + title: "Open Command Palette", + action: &command_palette::Toggle, + }, + ], + }, + Section { + title: "Configure", + entries: [ + SectionEntry { + icon: IconName::Settings, + title: "Open Settings", + action: &OpenSettings, + }, + SectionEntry { + icon: IconName::ZedAssistant, + title: "View AI Settings", + // TODO: use proper action + action: &NoAction, + }, + SectionEntry { + icon: IconName::Blocks, + title: "Explore Extensions", + action: &Extensions { + category_filter: None, + id: None, + }, + }, + ], + }, +); + +struct Section { + title: &'static str, + entries: [SectionEntry; COLS], +} + +impl Section { + fn render( + self, + index_offset: usize, + focus: &FocusHandle, + window: &mut Window, + cx: &mut App, + ) -> impl IntoElement { + v_flex() + .min_w_full() + .gap_2() + .child( + h_flex() + .px_1() + .gap_4() + .child( + Label::new(self.title.to_ascii_uppercase()) + .buffer_font(cx) + .color(Color::Muted) + .size(LabelSize::XSmall), + ) + .child(Divider::horizontal().color(DividerColor::Border)), + ) + .children( + self.entries + .iter() + .enumerate() + .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)), + ) + } +} + +struct SectionEntry { + icon: IconName, + title: &'static str, + action: &'static dyn Action, +} + +impl SectionEntry { + fn render( + &self, + button_index: usize, + focus: &FocusHandle, + window: &Window, + cx: &App, + ) -> impl IntoElement { + ButtonLike::new(("onboarding-button-id", button_index)) + .full_width() + .child( + h_flex() + .w_full() + .gap_1() + .justify_between() + .child( + h_flex() + .gap_2() + .child( + Icon::new(self.icon) + .color(Color::Muted) + .size(IconSize::XSmall), + ) + .child(Label::new(self.title)), + ) + .children(KeyBinding::for_action_in(self.action, focus, window, cx)), + ) + .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) + } +} + +pub struct WelcomePage { + focus_handle: FocusHandle, +} + +impl Render for WelcomePage { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let (first_section, second_entries) = CONTENT; + let first_section_entries = first_section.entries.len(); + + h_flex() + .size_full() + .justify_center() + .overflow_hidden() + .bg(cx.theme().colors().editor_background) + .key_context("Welcome") + .track_focus(&self.focus_handle(cx)) + .child( + h_flex() + .px_12() + .py_40() + .size_full() + .relative() + .max_w(px(1100.)) + .child( + div() + .size_full() + .max_w_128() + .mx_auto() + .child( + h_flex() + .w_full() + .justify_center() + .gap_4() + .child(Vector::square(VectorName::ZedLogo, rems(2.))) + .child( + div().child(Headline::new("Welcome to Zed")).child( + Label::new("The editor for what's next") + .size(LabelSize::Small) + .color(Color::Muted) + .italic(), + ), + ), + ) + .child( + v_flex() + .mt_12() + .gap_8() + .child(first_section.render( + Default::default(), + &self.focus_handle, + window, + cx, + )) + .child(second_entries.render( + first_section_entries, + &self.focus_handle, + window, + cx, + )) + .child( + h_flex() + .w_full() + .pt_4() + .justify_center() + // We call this a hack + .rounded_b_xs() + .border_t_1() + .border_color(DividerColor::Border.hsla(cx)) + .border_dashed() + .child( + div().child( + Button::new("welcome-exit", "Return to Setup") + .full_width() + .label_size(LabelSize::XSmall), + ), + ), + ), + ), + ), + ) + } +} + +impl WelcomePage { + pub fn new(cx: &mut Context) -> Entity { + let this = cx.new(|cx| WelcomePage { + focus_handle: cx.focus_handle(), + }); + + this + } +} + +impl EventEmitter for WelcomePage {} + +impl Focusable for WelcomePage { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for WelcomePage { + type Event = ItemEvent; + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Welcome".into() + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("New Welcome Page Opened") + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: Option, + _: &mut Window, + _: &mut Context, + ) -> Option> { + None + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } +} diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 1d91492f26..5779093ccc 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -44,7 +44,7 @@ impl KeyBinding { pub fn for_action_in( action: &dyn Action, focus: &FocusHandle, - window: &mut Window, + window: &Window, cx: &App, ) -> Option { let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?; From f3dc842ce69d81c6786628897e9aeca82d30beef Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:25:48 -0300 Subject: [PATCH 09/11] keymap editor: Make table denser (#35236) Hopefully, this will make it a bit easier to parse as a whole. Release Notes: - Made the keymap editor denser, improving how easy you can parse it at a glance. --- crates/settings_ui/src/keybindings.rs | 7 ++++++- crates/settings_ui/src/ui_components/table.rs | 19 ++++++++++--------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 431daff203..667483a434 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1690,7 +1690,7 @@ impl Render for KeymapEditor { move |window, cx| this.read(cx).render_no_matches_hint(window, cx) }) .column_widths([ - DefiniteLength::Absolute(AbsoluteLength::Pixels(px(40.))), + DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))), DefiniteLength::Fraction(0.25), DefiniteLength::Fraction(0.20), DefiniteLength::Fraction(0.14), @@ -1765,6 +1765,7 @@ impl Render for KeymapEditor { }, ) .into_any_element(); + let keystrokes = binding.ui_key_binding().cloned().map_or( binding .keystroke_text() @@ -1773,6 +1774,7 @@ impl Render for KeymapEditor { .into_any_element(), IntoElement::into_any_element, ); + let action_arguments = match binding.action().arguments.clone() { Some(arguments) => arguments.into_any_element(), @@ -1785,6 +1787,7 @@ impl Render for KeymapEditor { } } }; + let context = binding.context().cloned().map_or( gpui::Empty.into_any_element(), |context| { @@ -1809,11 +1812,13 @@ impl Render for KeymapEditor { .into_any_element() }, ); + let source = binding .keybind_source() .map(|source| source.name()) .unwrap_or_default() .into_any_element(); + Some([ icon.into_any_element(), action, diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 65778c20eb..3c9992bd68 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -17,7 +17,7 @@ use ui::{ StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, }; -const RESIZE_COLUMN_WIDTH: f32 = 5.0; +const RESIZE_COLUMN_WIDTH: f32 = 8.0; #[derive(Debug)] struct DraggedColumn(usize); @@ -214,6 +214,7 @@ impl TableInteractionState { let mut column_ix = 0; let resizable_columns_slice = *resizable_columns; let mut resizable_columns = resizable_columns.into_iter(); + let dividers = intersperse_with(spacers, || { window.with_id(column_ix, |window| { let mut resize_divider = div() @@ -221,9 +222,9 @@ impl TableInteractionState { .id(column_ix) .relative() .top_0() - .w_0p5() + .w_px() .h_full() - .bg(cx.theme().colors().border.opacity(0.5)); + .bg(cx.theme().colors().border.opacity(0.8)); let mut resize_handle = div() .id("column-resize-handle") @@ -237,9 +238,11 @@ impl TableInteractionState { .is_some_and(ResizeBehavior::is_resizable) { let hovered = window.use_state(cx, |_window, _cx| false); + resize_divider = resize_divider.when(*hovered.read(cx), |div| { div.bg(cx.theme().colors().border_focused) }); + resize_handle = resize_handle .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) .cursor_col_resize() @@ -269,12 +272,11 @@ impl TableInteractionState { }) }); - div() + h_flex() .id("resize-handles") - .h_flex() .absolute() - .w_full() .inset_0() + .w_full() .children(dividers) .into_any_element() } @@ -896,7 +898,6 @@ fn base_cell_style(width: Option) -> Div { .px_1p5() .when_some(width, |this, width| this.w(width)) .when(width.is_none(), |this| this.flex_1()) - .justify_start() .whitespace_nowrap() .text_ellipsis() .overflow_hidden() @@ -941,7 +942,7 @@ pub fn render_row( .map(IntoElement::into_any_element) .into_iter() .zip(column_widths) - .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)), + .map(|(cell, width)| base_cell_style_text(width, cx).px_1().py_0p5().child(cell)), ); let row = if let Some(map_row) = table_context.map_row { @@ -950,7 +951,7 @@ pub fn render_row( row.into_any_element() }; - div().h_full().w_full().child(row).into_any_element() + div().size_full().child(row).into_any_element() } pub fn render_header( From fa6b1a011406d6552715e5e9c1eb42e00b12e76f Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 28 Jul 2025 16:27:10 -0500 Subject: [PATCH 10/11] keymap_ui: Fix bug introduced in #35208 (#35237) Closes #ISSUE Fixes a bug that was cherry picked onto stable and preview branches introduced in #35208 whereby modifier keys would show up and not be removable when editing a keybind Release Notes: - (preview only) Keymap Editor: Fixed an issue introduced in v0.197.2 whereby modifier keys would show up and not be removable while recording keystrokes in the keybind edit modal --- crates/gpui/src/platform/keystroke.rs | 6 +----- crates/settings_ui/src/keybindings.rs | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 0f8dbdf9d2..24601eefd6 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -534,11 +534,7 @@ impl Modifiers { /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`]. pub fn is_subset_of(&self, other: &Modifiers) -> bool { - (other.control || !self.control) - && (other.alt || !self.alt) - && (other.shift || !self.shift) - && (other.platform || !self.platform) - && (other.function || !self.function) + (*other & *self) == *self } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 667483a434..5ff91246f4 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -3114,7 +3114,9 @@ impl KeystrokeInput { ) { let keystrokes_len = self.keystrokes.len(); - if event.modifiers.is_subset_of(&self.previous_modifiers) { + if self.previous_modifiers.modified() + && event.modifiers.is_subset_of(&self.previous_modifiers) + { self.previous_modifiers &= event.modifiers; cx.stop_propagation(); return; From 158f65fd1ef8524469cb2da6b8843a28fd44ae25 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Mon, 28 Jul 2025 23:33:20 +0200 Subject: [PATCH 11/11] gpui: Ensure tab index handles are properly reused across frames (#35235) This fixes an issue where focus handles with a tab index would get lost between rendered frames because the focus handles were not reused for the following paint cycle. Release Notes: - N/A --- crates/gpui/Cargo.toml | 4 ++++ crates/gpui/src/tab_stop.rs | 2 +- crates/gpui/src/window.rs | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 29e81269e3..680111a6ce 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -287,6 +287,10 @@ path = "examples/shadow.rs" name = "svg" path = "examples/svg/svg.rs" +[[example]] +name = "tab_stop" +path = "examples/tab_stop.rs" + [[example]] name = "text" path = "examples/text.rs" diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index 2ec3f560e8..1aa4cd6d9f 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -5,7 +5,7 @@ use crate::{FocusHandle, FocusId}; /// Used to manage the `Tab` event to switch between focus handles. #[derive(Default)] pub(crate) struct TabHandles { - handles: Vec, + pub(crate) handles: Vec, } impl TabHandles { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 963d2bb45c..01fbfff1c5 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -702,6 +702,7 @@ pub(crate) struct PaintIndex { input_handlers_index: usize, cursor_styles_index: usize, accessed_element_states_index: usize, + tab_handle_index: usize, line_layout_index: LineLayoutIndex, } @@ -2208,6 +2209,7 @@ impl Window { input_handlers_index: self.next_frame.input_handlers.len(), cursor_styles_index: self.next_frame.cursor_styles.len(), accessed_element_states_index: self.next_frame.accessed_element_states.len(), + tab_handle_index: self.next_frame.tab_handles.handles.len(), line_layout_index: self.text_system.layout_index(), } } @@ -2237,6 +2239,12 @@ impl Window { .iter() .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)), ); + self.next_frame.tab_handles.handles.extend( + self.rendered_frame.tab_handles.handles + [range.start.tab_handle_index..range.end.tab_handle_index] + .iter() + .cloned(), + ); self.text_system .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index);