From 184e1d93e3f28dd1cdc3e08adbae2417c33666f1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 23 Apr 2026 15:26:44 -0300 Subject: [PATCH] branch_picker: Add button to filter remote branches (#54632) This PR brings back the button to filter remote branches when accessing the title bar's branch picker with the mouse. It was unintentionally removed when we introduced the new worktree picker. Release Notes: - N/A --- .github/workflows/bump_zed_version.yml | 4 +- .github/workflows/release.yml | 2 +- .github/workflows/run_tests.yml | 2 +- Cargo.lock | 6 - assets/icons/bookmark.svg | 3 +- assets/keymaps/vim.json | 2 +- assets/settings/default.json | 13 - crates/acp_thread/src/acp_thread.rs | 2 +- crates/acp_thread/src/mention.rs | 54 +- crates/agent/src/agent.rs | 4 +- crates/agent/src/tools/grep_tool.rs | 11 +- crates/agent_ui/src/agent_diff.rs | 67 +- crates/agent_ui/src/agent_panel.rs | 129 +- crates/agent_ui/src/conversation_view.rs | 5 +- crates/agent_ui/src/mention_set.rs | 4 - crates/agent_ui/src/message_editor.rs | 406 +----- crates/agent_ui/src/thread_metadata_store.rs | 9 - crates/anthropic/src/anthropic.rs | 16 +- crates/anthropic/src/completion.rs | 10 +- crates/askpass/src/askpass.rs | 2 +- crates/auto_update_ui/Cargo.toml | 6 +- crates/auto_update_ui/src/auto_update_ui.rs | 6 +- crates/bedrock/src/bedrock.rs | 9 +- crates/client/src/user.rs | 15 - .../tests/integration/integration_tests.rs | 1 - crates/docs_preprocessor/src/main.rs | 94 +- crates/edit_prediction/src/edit_prediction.rs | 138 +- .../src/edit_prediction_tests.rs | 273 +--- .../src/zed_edit_prediction_delegate.rs | 58 +- .../src/edit_prediction_context.rs | 64 +- crates/editor/src/code_lens.rs | 90 +- crates/editor/src/editor.rs | 55 +- crates/editor/src/editor_tests.rs | 180 +-- crates/editor/src/element.rs | 40 +- crates/editor/src/items.rs | 71 +- crates/editor/src/split.rs | 35 +- crates/file_finder/Cargo.toml | 1 - crates/file_finder/src/file_finder.rs | 44 +- crates/fs/Cargo.toml | 1 - crates/fs/src/fs.rs | 158 +-- crates/fs/src/fs_watcher.rs | 476 +++---- crates/git_ui/Cargo.toml | 1 - crates/git_ui/src/branch_picker.rs | 55 +- crates/git_ui/src/commit_modal.rs | 58 +- crates/git_ui/src/file_diff_view.rs | 86 +- crates/git_ui/src/git_panel.rs | 134 +- crates/git_ui/src/git_panel_settings.rs | 2 - crates/git_ui/src/project_diff.rs | 6 +- crates/git_ui/src/text_diff_view.rs | 9 +- crates/git_ui/src/worktree_service.rs | 231 +-- crates/gpui/src/app.rs | 8 +- crates/gpui/src/elements/div.rs | 2 +- crates/gpui/src/executor.rs | 20 +- crates/gpui/src/text_system/line.rs | 12 +- crates/gpui_linux/src/linux/wayland/client.rs | 8 +- crates/gpui_linux/src/linux/x11/client.rs | 8 +- crates/gpui_util/src/lib.rs | 115 +- crates/grammars/src/cpp/indents.scm | 8 - crates/language/src/buffer.rs | 13 +- crates/language/src/file_content.rs | 158 --- crates/language/src/language.rs | 2 - crates/language/src/language_settings.rs | 14 +- .../src/provider/copilot_chat.rs | 4 +- .../src/language_models_cloud.rs | 4 +- crates/languages/Cargo.toml | 1 - crates/languages/src/cpp.rs | 326 ----- .../src/markdown_preview_view.rs | 1 + crates/picker/src/picker.rs | 21 +- crates/project/src/lsp_store.rs | 37 +- crates/project/src/project_search.rs | 10 - crates/project/src/search.rs | 1 - crates/project/src/task_store.rs | 2 +- .../tests/integration/project_tests.rs | 35 +- crates/recent_projects/src/recent_projects.rs | 40 +- crates/search/src/buffer_search.rs | 23 +- crates/search/src/project_search.rs | 26 +- crates/settings_content/src/language.rs | 35 - .../settings_content/src/settings_content.rs | 6 - crates/settings_content/src/theme.rs | 3 - .../src/components/ollama_model_picker.rs | 3 +- crates/settings_ui/src/page_data.rs | 893 ++++++------ .../src/pages/audio_input_output_setup.rs | 4 +- .../pages/edit_prediction_provider_setup.rs | 24 +- crates/settings_ui/src/settings_ui.rs | 35 +- crates/sidebar/src/sidebar_tests.rs | 39 - crates/terminal/Cargo.toml | 1 - crates/terminal/src/terminal_hyperlinks.rs | 20 - crates/terminal_view/src/terminal_view.rs | 23 +- crates/theme/src/default_colors.rs | 2 - crates/theme/src/fallback_themes.rs | 1 - crates/theme/src/styles/colors.rs | 2 - .../theme_selector/src/icon_theme_selector.rs | 25 +- crates/theme_selector/src/theme_selector.rs | 29 +- crates/theme_settings/src/schema.rs | 41 - crates/theme_settings/src/settings.rs | 32 +- crates/theme_settings/src/theme_settings.rs | 12 +- crates/title_bar/src/title_bar.rs | 130 +- crates/util/src/rel_path.rs | 8 - crates/vim/src/helix.rs | 1249 +---------------- crates/vim/src/state.rs | 25 +- crates/vim/src/vim.rs | 123 +- crates/workspace/src/status_bar.rs | 3 +- crates/worktree/src/worktree.rs | 161 ++- docs/src/SUMMARY.md | 1 - docs/src/ai/parallel-agents.md | 5 +- docs/src/finding-navigating.md | 7 - docs/src/project-panel.md | 184 --- tooling/compliance/src/checks.rs | 312 +--- tooling/compliance/src/git.rs | 101 +- tooling/compliance/src/github.rs | 35 +- tooling/compliance/src/report.rs | 29 +- tooling/xtask/src/tasks/compliance.rs | 22 +- .../src/tasks/workflows/bump_zed_version.rs | 10 +- .../xtask/src/tasks/workflows/run_tests.rs | 2 +- 114 files changed, 1588 insertions(+), 6096 deletions(-) delete mode 100644 crates/language/src/file_content.rs delete mode 100644 docs/src/project-panel.md diff --git a/.github/workflows/bump_zed_version.yml b/.github/workflows/bump_zed_version.yml index f8fae3de7a..02159b8ad2 100644 --- a/.github/workflows/bump_zed_version.yml +++ b/.github/workflows/bump_zed_version.yml @@ -160,7 +160,7 @@ jobs: name: steps::bot_commit uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c with: - message: ${{ needs.resolve_versions.outputs.preview_branch }} preview for @${{ github.actor }} + message: ${{ needs.resolve_versions.outputs.preview_branch }} preview ref: refs/heads/${{ needs.resolve_versions.outputs.preview_branch }} files: crates/zed/RELEASE_CHANNEL token: ${{ steps.generate-token.outputs.token }} @@ -206,7 +206,7 @@ jobs: name: steps::bot_commit uses: IAreKyleW00t/verified-bot-commit@126a6a11889ab05bcff72ec2403c326cd249b84c with: - message: ${{ needs.resolve_versions.outputs.stable_branch }} stable for @${{ github.actor }} + message: ${{ needs.resolve_versions.outputs.stable_branch }} stable ref: refs/heads/${{ needs.resolve_versions.outputs.stable_branch }} files: crates/zed/RELEASE_CHANNEL token: ${{ steps.generate-token.outputs.token }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f39b4ed52..17e121b995 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -241,7 +241,7 @@ jobs: timeout-minutes: 60 check_scripts: if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') - runs-on: namespace-profile-8x16-ubuntu-2204 + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - name: steps::checkout_repo uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 2051fa567b..d7b9883f01 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -705,7 +705,7 @@ jobs: needs: - orchestrate if: needs.orchestrate.outputs.run_action_checks == 'true' - runs-on: namespace-profile-8x16-ubuntu-2204 + runs-on: namespace-profile-2x4-ubuntu-2404 steps: - name: steps::checkout_repo uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd diff --git a/Cargo.lock b/Cargo.lock index c11331308f..658736d021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1293,12 +1293,10 @@ dependencies = [ "gpui", "markdown_preview", "notifications", - "project", "release_channel", "semver", "serde", "serde_json", - "settings", "smol", "telemetry", "ui", @@ -6308,7 +6306,6 @@ dependencies = [ "theme", "theme_settings", "ui", - "ui_input", "util", "workspace", "zed_actions", @@ -6609,7 +6606,6 @@ dependencies = [ "serde", "serde_json", "smol", - "telemetry", "tempfile", "text", "thiserror 2.0.17", @@ -7409,7 +7405,6 @@ dependencies = [ "smallvec", "smol", "strum 0.27.2", - "task", "telemetry", "theme", "theme_settings", @@ -17771,7 +17766,6 @@ dependencies = [ "libc", "log", "parking_lot", - "percent-encoding", "rand 0.9.3", "regex", "release_channel", diff --git a/assets/icons/bookmark.svg b/assets/icons/bookmark.svg index 50914b2933..999c9b72a1 100644 --- a/assets/icons/bookmark.svg +++ b/assets/icons/bookmark.svg @@ -1,3 +1,4 @@ - + + diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d57794e61a..d5bc26b941 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -507,7 +507,6 @@ "shift-l": "pane::ActivateNextItem", // not a helix default "g p": "pane::ActivatePreviousItem", "shift-h": "pane::ActivatePreviousItem", // not a helix default - "g w": "vim::HelixJumpToWord", "g .": "vim::HelixGotoLastModification", "g o": "editor::ToggleSelectedDiffHunks", // Zed specific "g shift-o": "git::ToggleStaged", // Zed specific @@ -546,6 +545,7 @@ "]": ["vim::PushHelixNext", { "around": true }], "[": ["vim::PushHelixPrevious", { "around": true }], "g q": "vim::PushRewrap", + "g w": "vim::PushRewrap", // not a helix default & clashes with helix `goto_word` }, }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index dcbf999190..3bf837abe9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -975,11 +975,6 @@ // // Default: true "diff_stats": true, - // Maximum length of the commit message title before a warning is shown. - // Set to 0 to disable. - // - // Default: 72 - "commit_title_max_length": 72, }, "message_editor": { // Whether to automatically replace emoji shortcodes with emoji characters. @@ -1688,14 +1683,6 @@ "prompt_format": "infer", "max_output_tokens": 64, }, - // Controls whether Zed may collect training data when using Zed's Edit Predictions. - // Data is only captured when the project is detected as open source. - // Possible values: - // - "default": use the preference previously set via the status-bar toggle, - // or false if no preference has been stored. - // - "yes": allow data collection for files in open-source projects. - // - "no": never allow data collection. - "allow_data_collection": "default", }, // Settings specific to journaling "journal": { diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index bf1d933acd..cf4693beba 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2125,7 +2125,7 @@ impl AcpThread { let curr_status = mem::replace(&mut call.status, new_status); if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status { - respond_tx.send(outcome).ok(); + respond_tx.send(outcome).log_err(); } else if cfg!(debug_assertions) { panic!("tried to authorize an already authorized tool call"); } diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 403b71736c..ac7b2d23cb 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -96,18 +96,13 @@ impl MentionUri { let path = url.path(); match url.scheme() { "file" => { - let trimmed = if path_style.is_windows() { + let normalized = if path_style.is_windows() { path.trim_start_matches("/") } else { path }; - let decoded = decode(trimmed).unwrap_or(Cow::Borrowed(trimmed)); - let normalized: Cow = if path_style.is_windows() { - Cow::Owned(decoded.replace('/', "\\")) - } else { - decoded - }; - let path = normalized.as_ref(); + let decoded = decode(normalized).unwrap_or(Cow::Borrowed(normalized)); + let path = decoded.as_ref(); if let Some(fragment) = url.fragment() { let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1); @@ -498,49 +493,6 @@ mod tests { assert_eq!(parsed.to_uri().to_string(), file_uri); } - #[test] - fn test_parse_file_uris_use_native_separators_on_windows() { - let parsed = MentionUri::parse("file:///C:/path/to/file.rs", PathStyle::Windows).unwrap(); - match parsed { - MentionUri::File { abs_path } => { - assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs")); - } - other => panic!("Expected File variant, got {other:?}"), - } - - let parsed = MentionUri::parse("file:///C:/path/to/dir/", PathStyle::Windows).unwrap(); - match parsed { - MentionUri::Directory { abs_path } => { - assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\dir\\")); - } - other => panic!("Expected Directory variant, got {other:?}"), - } - - let parsed = MentionUri::parse( - "file:///C:/path/to/file.rs?symbol=MySymbol#L10:20", - PathStyle::Windows, - ) - .unwrap(); - match parsed { - MentionUri::Symbol { abs_path, .. } => { - assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs")); - } - other => panic!("Expected Symbol variant, got {other:?}"), - } - - let parsed = - MentionUri::parse("file:///C:/path/to/file.rs#L5:15", PathStyle::Windows).unwrap(); - match parsed { - MentionUri::Selection { - abs_path: Some(abs_path), - .. - } => { - assert_eq!(abs_path, PathBuf::from("C:\\path\\to\\file.rs")); - } - other => panic!("Expected Selection variant, got {other:?}"), - } - } - #[test] fn test_to_directory_uri_without_slash() { let uri = MentionUri::Directory { diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index c5bc4c5828..9eb0f84b6f 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -1309,9 +1309,7 @@ impl NativeAgentConnection { { response .send(outcome) - .map_err(|_| { - anyhow!("authorization receiver was dropped") - }) + .map(|_| anyhow!("authorization receiver was dropped")) .log_err(); } }) diff --git a/crates/agent/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs index 9ccebd80e8..a56c793bb8 100644 --- a/crates/agent/src/tools/grep_tool.rs +++ b/crates/agent/src/tools/grep_tool.rs @@ -189,15 +189,8 @@ impl AgentTool for GrepTool { return Err("Search cancelled by user".to_string()); } }; - - let (buffer, ranges) = match search_result { - Some(SearchResult::Buffer { buffer, ranges }) => (buffer, ranges), - Some(SearchResult::LimitReached) => { - has_more_matches = true; - break; - } - Some(SearchResult::WaitingForScan) => continue, - None => break, + let Some(SearchResult::Buffer { buffer, ranges }) = search_result else { + break; }; if ranges.is_empty() { continue; diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 04951fcbe7..5029739097 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -7,7 +7,7 @@ use buffer_diff::DiffHunkStatus; use collections::{HashMap, HashSet}; use editor::{ Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, - SelectionEffects, SplittableEditor, ToPoint, + SelectionEffects, ToPoint, actions::{GoToHunk, GoToPreviousHunk}, multibuffer_context_lines, scroll::Autoscroll, @@ -40,7 +40,7 @@ use zed_actions::assistant::ToggleFocus; pub struct AgentDiffPane { multibuffer: Entity, - editor: Entity, + editor: Entity, thread: Entity, focus_handle: FocusHandle, workspace: WeakEntity, @@ -91,21 +91,15 @@ impl AgentDiffPane { let project = thread.read(cx).project().clone(); let editor = cx.new(|cx| { - let workspace_entity = workspace.upgrade().expect("workspace must exist"); - let diff_display_editor = SplittableEditor::new( - EditorSettings::get_global(cx).diff_view_style, - multibuffer.clone(), - project.clone(), - workspace_entity, - window, - cx, - ); - diff_display_editor + let mut editor = + Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); + editor.disable_inline_diagnostics(); + editor.set_expand_all_diff_hunks(cx); + editor .set_render_diff_hunk_controls(diff_hunk_controls(&thread, workspace.clone()), cx); - diff_display_editor.update_editors(cx, |editor, _cx| { - editor.register_addon(AgentDiffAddon); - }); - diff_display_editor + editor.register_addon(AgentDiffAddon); + editor.disable_mouse_wheel_zoom(); + editor }); let action_log = thread.read(cx).action_log().clone(); @@ -187,8 +181,7 @@ impl AgentDiffPane { (was_empty, is_excerpt_newly_added) }); - let rhs_editor = self.editor.read(cx).rhs_editor().clone(); - rhs_editor.update(cx, |editor, cx| { + self.editor.update(cx, |editor, cx| { if was_empty { let first_hunk = editor .diff_hunks_in_ranges( @@ -245,8 +238,7 @@ impl AgentDiffPane { pub fn move_to_path(&self, path_key: PathKey, window: &mut Window, cx: &mut App) { if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) { - let rhs_editor = self.editor.read(cx).rhs_editor().clone(); - rhs_editor.update(cx, |editor, cx| { + self.editor.update(cx, |editor, cx| { let first_hunk = editor .diff_hunks_in_ranges( &[position..editor::Anchor::Max], @@ -265,16 +257,14 @@ impl AgentDiffPane { } fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context) { - let rhs_editor = self.editor.read(cx).rhs_editor().clone(); - rhs_editor.update(cx, |editor, cx| { + self.editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx); }); } fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context) { - let rhs_editor = self.editor.read(cx).rhs_editor().clone(); - rhs_editor.update(cx, |editor, cx| { + self.editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); reject_edits_in_selection( editor, @@ -288,8 +278,7 @@ impl AgentDiffPane { } fn reject_all(&mut self, _: &RejectAll, window: &mut Window, cx: &mut Context) { - let rhs_editor = self.editor.read(cx).rhs_editor().clone(); - rhs_editor.update(cx, |editor, cx| { + self.editor.update(cx, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); reject_edits_in_ranges( editor, @@ -562,10 +551,7 @@ impl Item for AgentDiffPane { cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { - self.editor - .read(cx) - .rhs_editor() - .for_each_project_item(cx, f) + self.editor.for_each_project_item(cx, f) } fn set_nav_history( @@ -574,10 +560,8 @@ impl Item for AgentDiffPane { _: &mut Window, cx: &mut Context, ) { - self.editor.update(cx, |editor, cx| { - editor.rhs_editor().update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); }); } @@ -644,12 +628,14 @@ impl Item for AgentDiffPane { &'a self, type_id: TypeId, self_handle: &'a Entity, - cx: &'a App, + _: &'a App, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.clone().into()) + } else if type_id == TypeId::of::() { + Some(self.editor.clone().into()) } else { - self.editor.act_as_type(type_id, cx) + None } } @@ -1813,7 +1799,7 @@ mod tests { use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; use project::{FakeFs, Project}; use serde_json::json; - use settings::{DiffViewStyle, SettingsStore}; + use settings::SettingsStore; use std::{path::Path, rc::Rc}; use util::path; use workspace::{MultiWorkspace, PathList}; @@ -1823,11 +1809,6 @@ mod tests { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.editor.diff_view_style = Some(DiffViewStyle::Unified); - }); - }); prompt_store::init(cx); theme_settings::init(theme::LoadThemes::JustBase, cx); language_model::init(cx); @@ -1866,7 +1847,7 @@ mod tests { let agent_diff = cx.new_window_entity(|window, cx| { AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx) }); - let editor = agent_diff.read_with(cx, |diff, cx| diff.editor.read(cx).rhs_editor().clone()); + let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone()); let buffer = project .update(cx, |project, cx| project.open_buffer(buffer_path, cx)) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index e5c6f16102..b363d6a42a 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -33,7 +33,7 @@ use crate::DEFAULT_THREAD_TITLE; use crate::ExpandMessageEditor; use crate::ManageProfiles; use crate::agent_connection_store::AgentConnectionStore; -use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent}; +use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore}; use crate::{ AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, @@ -708,7 +708,6 @@ pub struct AgentPanel { show_trust_workspace_message: bool, _base_view_observation: Option, _draft_editor_observation: Option, - _thread_metadata_store_subscription: Subscription, } impl AgentPanel { @@ -1023,17 +1022,6 @@ impl AgentPanel { } _ => {} }); - - let _thread_metadata_store_subscription = cx.subscribe( - &ThreadMetadataStore::global(cx), - |this, _store, event, cx| { - let ThreadMetadataStoreEvent::ThreadArchived(thread_id) = event; - if this.retained_threads.remove(thread_id).is_some() { - cx.notify(); - } - }, - ); - let mut panel = Self { workspace_id, base_view, @@ -1067,7 +1055,6 @@ impl AgentPanel { new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)), _base_view_observation: None, _draft_editor_observation: None, - _thread_metadata_store_subscription, }; // Initial sync of agent servers from extensions @@ -1404,11 +1391,20 @@ impl AgentPanel { ) { let session_id = action.from_session_id.clone(); - let Some(content) = Self::initial_content_for_thread_summary(session_id.clone(), cx) else { + let Some(thread) = ThreadStore::global(cx) + .read(cx) + .entries() + .find(|t| t.id == session_id) + else { log::error!("No session found for summarization with id {}", session_id); return; }; + let Some(parent_session_id) = thread.parent_session_id else { + log::error!("Session {} has no parent session", session_id); + return; + }; + cx.spawn_in(window, async move |this, cx| { this.update_in(cx, |this, window, cx| { this.external_thread( @@ -1416,7 +1412,10 @@ impl AgentPanel { None, None, None, - Some(content), + Some(AgentInitialContent::ThreadSummary { + session_id: parent_session_id, + title: Some(thread.title), + }), true, "agent_panel", window, @@ -1428,21 +1427,6 @@ impl AgentPanel { .detach_and_log_err(cx); } - fn initial_content_for_thread_summary( - session_id: acp::SessionId, - cx: &App, - ) -> Option { - let thread = ThreadStore::global(cx) - .read(cx) - .entries() - .find(|t| t.id == session_id)?; - - Some(AgentInitialContent::ThreadSummary { - session_id: thread.id, - title: Some(thread.title), - }) - } - fn external_thread( &mut self, agent_choice: Option, @@ -5064,89 +5048,6 @@ mod tests { }); } - #[gpui::test] - async fn test_initial_content_for_thread_summary_uses_own_session_id(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { - agent::ThreadStore::init_global(cx); - language_model::LanguageModelRegistry::test(cx); - }); - - let source_session_id = acp::SessionId::new("source-thread-session"); - let source_title: SharedString = "Source Thread Title".into(); - let db_thread = agent::DbThread { - title: source_title.clone(), - messages: Vec::new(), - updated_at: Utc::now(), - detailed_summary: None, - initial_project_snapshot: None, - cumulative_token_usage: Default::default(), - request_token_usage: HashMap::default(), - model: None, - profile: None, - imported: false, - subagent_context: None, - speed: None, - thinking_enabled: false, - thinking_effort: None, - draft_prompt: None, - ui_scroll_position: None, - }; - - let thread_store = cx.update(|cx| ThreadStore::global(cx)); - thread_store - .update(cx, |store, cx| { - store.save_thread( - source_session_id.clone(), - db_thread, - PathList::default(), - cx, - ) - }) - .await - .expect("saving source thread should succeed"); - cx.run_until_parked(); - - thread_store.read_with(cx, |store, _cx| { - let entry = store - .thread_from_session_id(&source_session_id) - .expect("saved thread should be listed in the store"); - assert!( - entry.parent_session_id.is_none(), - "saved thread is a root thread with no parent session" - ); - }); - - let content = cx - .update(|cx| { - AgentPanel::initial_content_for_thread_summary(source_session_id.clone(), cx) - }) - .expect("initial content should be produced for a root thread"); - - match content { - AgentInitialContent::ThreadSummary { session_id, title } => { - assert_eq!( - session_id, source_session_id, - "thread-summary mention should use the source thread's own session id" - ); - assert_eq!(title, Some(source_title.clone())); - } - _ => panic!("expected AgentInitialContent::ThreadSummary"), - } - - // Unknown session ids should still produce no content. - let missing = cx.update(|cx| { - AgentPanel::initial_content_for_thread_summary( - acp::SessionId::new("does-not-exist"), - cx, - ) - }); - assert!( - missing.is_none(), - "unknown session ids should not produce initial content" - ); - } - #[gpui::test] async fn test_cleanup_retained_threads_keeps_five_most_recent_idle_loadable_threads( cx: &mut TestAppContext, diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs index 992d5ac937..6577496965 100644 --- a/crates/agent_ui/src/conversation_view.rs +++ b/crates/agent_ui/src/conversation_view.rs @@ -56,7 +56,7 @@ use std::time::Instant; use std::{collections::BTreeMap, rc::Rc, time::Duration}; use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; -use theme_settings::{AgentBufferFontSize, AgentUiFontSize}; +use theme_settings::AgentFontSize; use ui::{ Callout, CircularProgress, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon, DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, @@ -632,8 +632,7 @@ impl ConversationView { let agent_server_store = project.read(cx).agent_server_store().clone(); let subscriptions = vec![ cx.observe_global_in::(window, Self::agent_ui_font_size_changed), - cx.observe_global_in::(window, Self::agent_ui_font_size_changed), - cx.observe_global_in::(window, Self::agent_ui_font_size_changed), + cx.observe_global_in::(window, Self::agent_ui_font_size_changed), cx.subscribe_in( &agent_server_store, window, diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index 6b552610cf..0c7b3eb6ba 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -174,10 +174,6 @@ impl MentionSet { self.mentions.values().map(|(uri, _)| uri.clone()).collect() } - pub fn mention_uri_for_crease(&self, crease_id: &CreaseId) -> Option { - self.mentions.get(crease_id).map(|(uri, _)| uri.clone()) - } - pub fn set_mentions(&mut self, mentions: HashMap) { self.mentions = mentions; } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 213ce4e88c..0f213cb9f1 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -15,14 +15,12 @@ use anyhow::{Result, anyhow}; use editor::{ Addon, AnchorRangeExt, ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, MultiBufferSnapshot, ToOffset, - actions::{Copy, Paste}, - code_context_menus::CodeContextMenu, - scroll::Autoscroll, + actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll, }; use futures::{FutureExt as _, future::join_all}; use gpui::{ - AppContext, ClipboardEntry, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, - Focusable, ImageFormat, KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity, + AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, + KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity, }; use language::{Buffer, language_settings::InlayHintKind}; use parking_lot::RwLock; @@ -1188,16 +1186,6 @@ impl MessageEditor { cx.propagate(); } - fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { - let Some(text) = self.serialized_copy_text(cx) else { - cx.propagate(); - return; - }; - - cx.stop_propagation(); - cx.write_to_clipboard(ClipboardItem::new_string(text)); - } - fn paste_raw(&mut self, _: &PasteRaw, window: &mut Window, cx: &mut Context) { let editor = self.editor.clone(); window.defer(cx, move |window, cx| { @@ -1777,76 +1765,6 @@ impl MessageEditor { editor.set_text(text, window, cx); }); } - - fn serialized_copy_text(&self, cx: &mut App) -> Option { - let display_snapshot = self - .editor - .update(cx, |editor, cx| editor.display_snapshot(cx)); - let editor = self.editor.read(cx); - if !editor.has_non_empty_selection(&display_snapshot) { - return None; - } - - let snapshot = editor.buffer().read(cx).snapshot(cx); - let mention_set = self.mention_set.read(cx); - let mention_ranges = display_snapshot - .crease_snapshot - .crease_items_with_offsets(&snapshot) - .into_iter() - .filter_map(|(crease_id, range)| { - mention_set.mention_uri_for_crease(&crease_id).map(|uri| { - ( - range.start.to_offset(&snapshot), - range.end.to_offset(&snapshot), - uri, - ) - }) - }) - .collect::>(); - - let mut text = String::new(); - let mut has_mentions = false; - let mut is_first = true; - - for selection in editor - .selections - .all::(&display_snapshot) - { - if is_first { - is_first = false; - } else { - text.push('\n'); - } - - let mut overlapping_mentions = mention_ranges - .iter() - .filter(|(start, end, _)| *start < selection.end && selection.start < *end) - .peekable(); - - if overlapping_mentions.peek().is_none() { - text.extend(snapshot.text_for_range(selection.start..selection.end)); - continue; - } - - has_mentions = true; - - let mut cursor = selection.start; - for (start, end, uri) in overlapping_mentions { - if cursor < *start { - text.extend(snapshot.text_for_range(cursor..*start)); - } - - write!(text, "{}", uri.as_link()).unwrap(); - cursor = *end; - } - - if cursor < selection.end { - text.extend(snapshot.text_for_range(cursor..selection.end)); - } - } - - has_mentions.then_some(text) - } } impl Focusable for MessageEditor { @@ -1863,7 +1781,6 @@ impl Render for MessageEditor { .on_action(cx.listener(Self::send_immediately)) .on_action(cx.listener(Self::chat_with_follow)) .on_action(cx.listener(Self::cancel)) - .capture_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::paste_raw)) .capture_action(cx.listener(Self::paste)) .flex_1() @@ -1998,10 +1915,10 @@ mod tests { }; use fs::FakeFs; - use futures::{FutureExt as _, StreamExt as _}; + use futures::StreamExt as _; use gpui::{ AppContext, ClipboardEntry, ClipboardItem, Entity, EventEmitter, ExternalPaths, - FocusHandle, Focusable, Task, TestAppContext, VisualTestContext, + FocusHandle, Focusable, TestAppContext, VisualTestContext, }; use language_model::LanguageModelRegistry; use lsp::{CompletionContext, CompletionTriggerKind}; @@ -2017,7 +1934,6 @@ mod tests { use crate::completion_provider::PromptContextType; use crate::{ conversation_view::tests::init_test, - mention_set::insert_crease_for_mention, message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links}, }; @@ -3932,318 +3848,6 @@ mod tests { ); } - #[gpui::test] - async fn test_copy_with_selection_mentions_serializes_links(cx: &mut TestAppContext) { - init_test(cx); - - let (source_message_editor, _source_editor, mut cx) = setup_paste_test_message_editor( - json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}), - cx, - ) - .await; - - let workspace = source_message_editor.read_with(&cx, |message_editor, _| { - message_editor.workspace.upgrade().expect("workspace") - }); - let project = workspace.read_with(&cx, |workspace, _| workspace.project().clone()); - - let source_text = "selection needs work\nselection looks fine"; - let first_range = 0..9; - let second_start = "selection needs work\n".len(); - let second_range = second_start..(second_start + "selection".len()); - let first_uri = MentionUri::Selection { - abs_path: Some(path!("/project/file.rs").into()), - line_range: 0..=1, - }; - let second_uri = MentionUri::Selection { - abs_path: Some(path!("/project/file.rs").into()), - line_range: 2..=3, - }; - - source_message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.set_text(source_text, window, cx); - - let snapshot = message_editor - .editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx); - for (range, uri, content) in [ - ( - first_range.clone(), - first_uri.clone(), - "line 1\nline 2\n".to_string(), - ), - ( - second_range.clone(), - second_uri.clone(), - "line 3\nline 4\n".to_string(), - ), - ] { - let Some((crease_id, tx)) = insert_crease_for_mention( - snapshot - .anchor_to_buffer_anchor( - snapshot.anchor_before(MultiBufferOffset(range.start)), - ) - .expect("selection mention anchor should map to a buffer") - .0, - range.len(), - uri.name().into(), - uri.icon_path(cx), - uri.tooltip_text(), - Some(uri.clone()), - Some(message_editor.workspace.clone()), - None, - message_editor.editor.clone(), - window, - cx, - ) else { - panic!("expected mention crease insertion"); - }; - drop(tx); - - message_editor.mention_set.update(cx, |mention_set, _cx| { - mention_set.insert_mention( - crease_id, - uri, - Task::ready(Ok(Mention::Text { - content, - tracked_buffers: Vec::new(), - })) - .shared(), - ); - }); - } - - let buffer_len = snapshot.len(); - message_editor.editor.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(0)..buffer_len]); - }); - }); - }); - - let copied_text = source_message_editor.update(&mut cx, |message_editor, cx| { - message_editor - .serialized_copy_text(cx) - .expect("selection mentions should serialize") - }); - let expected_text = format!( - "{} needs work\n{} looks fine", - first_uri.as_link(), - second_uri.as_link() - ); - assert_eq!(copied_text, expected_text); - - let target_message_editor = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let thread_store = cx.new(|cx| ThreadStore::new(cx)); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - Some(thread_store), - None, - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window, cx); - message_editor - }); - - cx.write_to_clipboard(ClipboardItem::new_string(copied_text)); - target_message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.paste(&Paste, window, cx); - }); - cx.run_until_parked(); - - let target_text = target_message_editor.read_with(&cx, |message_editor, cx| { - message_editor.editor.read(cx).text(cx) - }); - assert_eq!(target_text, expected_text); - - let contents = mention_contents(&target_message_editor, &mut cx).await; - assert_eq!(contents.len(), 2); - assert!(contents.iter().any(|(uri, _)| uri == &first_uri)); - assert!(contents.iter().any(|(uri, _)| uri == &second_uri)); - } - - struct SelectionMentionFixture { - message_editor: Entity, - first_uri: MentionUri, - first_range: Range, - second_range: Range, - } - - async fn setup_selection_mention_fixture( - cx: &mut TestAppContext, - ) -> (SelectionMentionFixture, VisualTestContext) { - let (message_editor, _source_editor, mut cx) = setup_paste_test_message_editor( - json!({"file.rs": "line 1\nline 2\nline 3\nline 4\n"}), - cx, - ) - .await; - - let source_text = "selection needs work\nselection looks fine"; - let first_range = 0..9; - let second_start = "selection needs work\n".len(); - let second_range = second_start..(second_start + "selection".len()); - let first_uri = MentionUri::Selection { - abs_path: Some(path!("/project/file.rs").into()), - line_range: 0..=1, - }; - let second_uri = MentionUri::Selection { - abs_path: Some(path!("/project/file.rs").into()), - line_range: 2..=3, - }; - - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.set_text(source_text, window, cx); - - let snapshot = message_editor - .editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx); - for (range, uri, content) in [ - ( - first_range.clone(), - first_uri.clone(), - "line 1\nline 2\n".to_string(), - ), - ( - second_range.clone(), - second_uri.clone(), - "line 3\nline 4\n".to_string(), - ), - ] { - let Some((crease_id, tx)) = insert_crease_for_mention( - snapshot - .anchor_to_buffer_anchor( - snapshot.anchor_before(MultiBufferOffset(range.start)), - ) - .expect("selection mention anchor should map to a buffer") - .0, - range.len(), - uri.name().into(), - uri.icon_path(cx), - uri.tooltip_text(), - Some(uri.clone()), - Some(message_editor.workspace.clone()), - None, - message_editor.editor.clone(), - window, - cx, - ) else { - panic!("expected mention crease insertion"); - }; - drop(tx); - - message_editor.mention_set.update(cx, |mention_set, _cx| { - mention_set.insert_mention( - crease_id, - uri, - Task::ready(Ok(Mention::Text { - content, - tracked_buffers: Vec::new(), - })) - .shared(), - ); - }); - } - }); - - ( - SelectionMentionFixture { - message_editor, - first_uri, - first_range, - second_range, - }, - cx, - ) - } - - #[gpui::test] - async fn test_serialized_copy_text_selection_covers_only_mention(cx: &mut TestAppContext) { - init_test(cx); - - let (fixture, mut cx) = setup_selection_mention_fixture(cx).await; - - fixture - .message_editor - .update_in(&mut cx, |message_editor, window, cx| { - let range = fixture.first_range.clone(); - message_editor.editor.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([ - MultiBufferOffset(range.start)..MultiBufferOffset(range.end) - ]); - }); - }); - }); - - let copied = fixture - .message_editor - .update(&mut cx, |message_editor, cx| { - message_editor.serialized_copy_text(cx) - }); - - assert_eq!(copied, Some(fixture.first_uri.as_link().to_string())); - } - - #[gpui::test] - async fn test_serialized_copy_text_returns_none_when_mentions_outside_selection( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let (fixture, mut cx) = setup_selection_mention_fixture(cx).await; - - let between_start = fixture.first_range.end; - let between_end = fixture.second_range.start - 1; - - fixture - .message_editor - .update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([ - MultiBufferOffset(between_start)..MultiBufferOffset(between_end) - ]); - }); - }); - }); - - let copied = fixture - .message_editor - .update(&mut cx, |message_editor, cx| { - message_editor.serialized_copy_text(cx) - }); - - assert_eq!(copied, None); - } - #[gpui::test] async fn test_paste_mention_link_with_completion_trigger_does_not_panic( cx: &mut TestAppContext, diff --git a/crates/agent_ui/src/thread_metadata_store.rs b/crates/agent_ui/src/thread_metadata_store.rs index 0b7e023e2c..21ac2af099 100644 --- a/crates/agent_ui/src/thread_metadata_store.rs +++ b/crates/agent_ui/src/thread_metadata_store.rs @@ -785,8 +785,6 @@ impl ThreadMetadataStore { if let Some(job) = archive_job { self.in_flight_archives.insert(thread_id, job); } - - cx.emit(ThreadMetadataStoreEvent::ThreadArchived(thread_id)); } pub fn unarchive(&mut self, thread_id: ThreadId, cx: &mut Context) { @@ -1230,13 +1228,6 @@ impl ThreadMetadataStore { impl Global for ThreadMetadataStore {} -#[derive(Clone, Debug)] -pub enum ThreadMetadataStoreEvent { - ThreadArchived(ThreadId), -} - -impl gpui::EventEmitter for ThreadMetadataStore {} - struct ThreadMetadataDb(ThreadSafeConnection); impl Domain for ThreadMetadataDb { diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 269edbea7e..ac94daba71 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -764,20 +764,8 @@ pub enum ToolChoice { #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum Thinking { - Enabled { - budget_tokens: Option, - }, - Adaptive { - #[serde(default, skip_serializing_if = "Option::is_none")] - display: Option, - }, -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum AdaptiveThinkingDisplay { - Omitted, - Summarized, + Enabled { budget_tokens: Option }, + Adaptive, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, EnumString)] diff --git a/crates/anthropic/src/completion.rs b/crates/anthropic/src/completion.rs index 7bb4821cc7..16bb5012e5 100644 --- a/crates/anthropic/src/completion.rs +++ b/crates/anthropic/src/completion.rs @@ -11,9 +11,9 @@ use std::pin::Pin; use std::str::FromStr; use crate::{ - AdaptiveThinkingDisplay, AnthropicError, AnthropicModelMode, CacheControl, CacheControlType, - ContentDelta, Event, ImageSource, Message, RequestContent, ResponseContent, StringOrContents, - Thinking, Tool, ToolChoice, ToolResultContent, ToolResultPart, Usage, + AnthropicError, AnthropicModelMode, CacheControl, CacheControlType, ContentDelta, Event, + ImageSource, Message, RequestContent, ResponseContent, StringOrContents, Thinking, Tool, + ToolChoice, ToolResultContent, ToolResultPart, Usage, }; fn to_anthropic_content(content: MessageContent) -> Option { @@ -180,9 +180,7 @@ pub fn into_anthropic( AnthropicModelMode::Thinking { budget_tokens } => { Some(Thinking::Enabled { budget_tokens }) } - AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive { - display: Some(AdaptiveThinkingDisplay::Summarized), - }), + AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive), AnthropicModelMode::Default => None, } } else { diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index c550d95979..4e84d38946 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -124,7 +124,7 @@ impl AskPassSession { ControlFlow::Continue(Ok(password)) } else { if let Some(kill_tx) = kill_tx.lock().await.take() { - kill_tx.send(()).ok(); + kill_tx.send(()).log_err(); } ControlFlow::Break(()) } diff --git a/crates/auto_update_ui/Cargo.toml b/crates/auto_update_ui/Cargo.toml index 57daa55394..16783aa9e9 100644 --- a/crates/auto_update_ui/Cargo.toml +++ b/crates/auto_update_ui/Cargo.toml @@ -17,17 +17,15 @@ anyhow.workspace = true auto_update.workspace = true client.workspace = true db.workspace = true -editor.workspace = true fs.workspace = true +editor.workspace = true +notifications.workspace = true gpui.workspace = true markdown_preview.workspace = true -notifications.workspace = true -project.workspace = true release_channel.workspace = true semver.workspace = true serde.workspace = true serde_json.workspace = true -settings.workspace = true smol.workspace = true telemetry.workspace = true ui.workspace = true diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 77ba83597e..ebd0d2c06d 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -13,7 +13,6 @@ use notifications::status_toast::StatusToast; use release_channel::{AppVersion, ReleaseChannel}; use semver::Version; use serde::Deserialize; -use settings::Settings as _; use smol::io::AsyncReadExt; use ui::{AnnouncementToast, ListBulletItem, ParallelAgentsIllustration, prelude::*}; use util::{ResultExt as _, maybe}; @@ -205,10 +204,7 @@ fn announcement_for_version(version: &Version, cx: &App) -> Option= version_with_parallel_agents - && !ParallelAgentAnnouncement::dismissed(cx) - && !project::DisableAiSettings::get_global(cx).disable_ai - { + if *version >= version_with_parallel_agents && !ParallelAgentAnnouncement::dismissed(cx) { let fs = ::global(cx); Some(AnnouncementContent { heading: "Introducing Parallel Agents".into(), diff --git a/crates/bedrock/src/bedrock.rs b/crates/bedrock/src/bedrock.rs index a3f60fd7b5..12db814d3a 100644 --- a/crates/bedrock/src/bedrock.rs +++ b/crates/bedrock/src/bedrock.rs @@ -58,13 +58,8 @@ pub async fn stream_completion( additional_fields.insert("thinking".to_string(), Document::from(thinking_config)); } Some(Thinking::Adaptive { effort: _ }) => { - let thinking_config = HashMap::from([ - ("type".to_string(), Document::String("adaptive".to_string())), - ( - "display".to_string(), - Document::String("summarized".to_string()), - ), - ]); + let thinking_config = + HashMap::from([("type".to_string(), Document::String("adaptive".to_string()))]); additional_fields.insert("thinking".to_string(), Document::from(thinking_config)); } _ => {} diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index ff4450f269..323d6346ed 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -740,21 +740,6 @@ impl UserStore { .get(¤t_organization.id) } - #[cfg(any(test, feature = "test-support"))] - pub fn set_current_organization_configuration_for_test( - &mut self, - organization: Arc, - configuration: OrganizationConfiguration, - cx: &mut Context, - ) { - self.current_organization = Some(organization.clone()); - self.organizations = vec![organization.clone()]; - self.configuration_by_organization - .insert(organization.id.clone(), configuration); - cx.emit(Event::OrganizationChanged); - cx.notify(); - } - pub fn plan(&self) -> Option { #[cfg(debug_assertions)] if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() { diff --git a/crates/collab/tests/integration/integration_tests.rs b/crates/collab/tests/integration/integration_tests.rs index b7479f9562..3e423d2615 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -5295,7 +5295,6 @@ async fn test_project_search( "Unexpectedly reached search limit in tests. If you do want to assert limit-reached, change this panic call." ) } - SearchResult::WaitingForScan => {} }; } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index 6655d86d25..3eaae6a1d4 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -336,28 +336,17 @@ fn is_missing_action(name: &str) -> bool { actions_available() && find_action_by_name(name).is_none() } -// Find the last binding (in keymap order) for the given action. -// Exact action matches are preferred over parameterized variants. +// Find the binding in reverse order, as the last binding takes precedence. fn find_binding_in_keymap(keymap: &KeymapFile, action: &str) -> Option { - let find = |predicate: &dyn Fn(&str) -> bool| { - keymap.sections().rev().find_map(|section| { - section.bindings().rev().find_map(|(keystroke, a)| { - if predicate(&a.to_string()) { - Some(keystroke.to_string()) - } else { - None - } - }) + keymap.sections().rev().find_map(|section| { + section.bindings().rev().find_map(|(keystroke, a)| { + if name_for_action(a.to_string()) == action { + Some(keystroke.to_string()) + } else { + None + } }) - }; - - // Look for exact match - if let Some(binding) = find(&|a| a == action) { - return Some(binding); - } - - // Look for parameterized match - find(&|a| name_for_action(a.to_string()) == action) + }) } fn find_binding(os: Os, action: &str) -> Option { @@ -883,68 +872,3 @@ fn keymap_schema_for_actions( &deprecation_messages, ) } - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_find_binding_prefers_exact_match_over_parameterized() { - let keymap: KeymapFile = serde_json::from_value(json!([ - { - "bindings": { - "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher", - "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }] - } - } - ])) - .unwrap(); - - let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher"); - assert_eq!(binding.as_deref(), Some("ctrl-tab")); - } - - #[test] - fn test_find_binding_falls_back_to_parameterized_match() { - let keymap: KeymapFile = serde_json::from_value(json!([ - { - "bindings": { - "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }] - } - } - ])) - .unwrap(); - - let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher"); - assert_eq!(binding.as_deref(), Some("ctrl-shift-tab")); - } - - #[test] - fn test_find_binding_prefers_exact_match_regardless_of_order() { - let keymap: KeymapFile = serde_json::from_value(json!([ - { - "bindings": { - "ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }], - "ctrl-tab": "agents_sidebar::ToggleThreadSwitcher" - } - } - ])) - .unwrap(); - - let binding = find_binding_in_keymap(&keymap, "agents_sidebar::ToggleThreadSwitcher"); - assert_eq!(binding.as_deref(), Some("ctrl-tab")); - } - - #[test] - fn test_find_binding_later_section_overrides_earlier() { - let keymap: KeymapFile = serde_json::from_value(json!([ - { "bindings": { "ctrl-a": "some::Action" } }, - { "bindings": { "ctrl-b": "some::Action" } } - ])) - .unwrap(); - - let binding = find_binding_in_keymap(&keymap, "some::Action"); - assert_eq!(binding.as_deref(), Some("ctrl-b")); - } -} diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index c156e69466..16ce0d6599 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -40,8 +40,7 @@ use release_channel::AppVersion; use semver::Version; use serde::de::DeserializeOwned; use settings::{ - EditPredictionDataCollectionChoice, EditPredictionPromptFormat, EditPredictionProvider, - Settings as _, update_settings_file, + EditPredictionPromptFormat, EditPredictionProvider, Settings as _, update_settings_file, }; use std::collections::{VecDeque, hash_map}; use std::env; @@ -152,7 +151,7 @@ pub struct EditPredictionStore { preferred_experiment: Option, available_experiments: Vec, pub mercury: Mercury, - legacy_data_collection_enabled: bool, + data_collection_choice: DataCollectionChoice, reject_predictions_tx: mpsc::UnboundedSender, settled_predictions_tx: mpsc::UnboundedSender, shown_predictions: VecDeque, @@ -758,8 +757,9 @@ impl EditPredictionStore { } pub fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { + let data_collection_choice = Self::load_data_collection_choice(cx); + let llm_token = global_llm_token(cx); - let legacy_data_collection_enabled = Self::load_legacy_data_collection_enabled(cx); let (reject_tx, reject_rx) = mpsc::unbounded(); cx.background_spawn({ @@ -814,8 +814,8 @@ impl EditPredictionStore { preferred_experiment: None, available_experiments: Vec::new(), mercury: Mercury::new(cx), - legacy_data_collection_enabled, + data_collection_choice, reject_predictions_tx: reject_tx, settled_predictions_tx, rated_predictions: Default::default(), @@ -2770,45 +2770,38 @@ impl EditPredictionStore { } pub(crate) fn is_data_collection_enabled(&self, cx: &App) -> bool { - if !self.is_data_collection_allowed_by_organization(cx) { - return false; - } - - if cx.is_staff() { - return true; - } - - match all_language_settings(None, cx) - .edit_predictions - .allow_data_collection - { - EditPredictionDataCollectionChoice::Yes => true, - EditPredictionDataCollectionChoice::No => false, - // Fall back to the legacy KV entry captured when the store was - // created, preserving existing users' choices without per-request - // database reads. - EditPredictionDataCollectionChoice::Default => self.legacy_data_collection_enabled, - } + self.data_collection_choice.is_enabled(cx) } - fn load_legacy_data_collection_enabled(cx: &App) -> bool { - KeyValueStore::global(cx) + fn load_data_collection_choice(cx: &App) -> DataCollectionChoice { + let choice = KeyValueStore::global(cx) .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) .log_err() - .flatten() - .as_deref() - == Some("true") + .flatten(); + + match choice.as_deref() { + Some("true") => DataCollectionChoice::Enabled, + Some("false") => DataCollectionChoice::Disabled, + Some(_) => { + log::error!("unknown value in '{ZED_PREDICT_DATA_COLLECTION_CHOICE}'"); + DataCollectionChoice::NotAnswered + } + None => DataCollectionChoice::NotAnswered, + } } - pub(crate) fn is_data_collection_allowed_by_organization(&self, cx: &App) -> bool { - self.user_store - .read(cx) - .current_organization_configuration() - .is_none_or(|organization_configuration| { - organization_configuration - .edit_prediction - .is_feedback_enabled - }) + fn toggle_data_collection_choice(&mut self, cx: &mut Context) { + self.data_collection_choice = self.data_collection_choice.toggle(); + let new_choice = self.data_collection_choice; + let is_enabled = new_choice.is_enabled(cx); + let kvp = KeyValueStore::global(cx); + db::write_and_log(cx, move || async move { + kvp.write_kvp( + ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), + is_enabled.to_string(), + ) + .await + }); } pub fn shown_predictions(&self) -> impl DoubleEndedIterator { @@ -3001,37 +2994,70 @@ pub struct ZedUpdateRequiredError { minimum_version: Version, } -struct ZedPredictUpsell; +#[derive(Debug, Clone, Copy)] +pub enum DataCollectionChoice { + NotAnswered, + Enabled, + Disabled, +} -fn is_upsell_dismissed(cx: &App) -> bool { - // To make this backwards compatible with older versions of Zed, we - // check if the user has seen the previous Edit Prediction Onboarding - // before, by checking the data collection choice which was written to - // the database once the user clicked on "Accept and Enable" - let kvp = KeyValueStore::global(cx); - if kvp - .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) - .log_err() - .is_some_and(|s| s.is_some()) - { - return true; +impl DataCollectionChoice { + pub fn is_enabled(self, cx: &App) -> bool { + if cx.is_staff() { + return true; + } + match self { + Self::Enabled => true, + Self::NotAnswered | Self::Disabled => false, + } } - kvp.read_kvp(ZedPredictUpsell::KEY) - .log_err() - .is_some_and(|s| s.is_some()) + #[must_use] + pub fn toggle(&self) -> DataCollectionChoice { + match self { + Self::Enabled => Self::Disabled, + Self::Disabled => Self::Enabled, + Self::NotAnswered => Self::Enabled, + } + } } +impl From for DataCollectionChoice { + fn from(value: bool) -> Self { + match value { + true => DataCollectionChoice::Enabled, + false => DataCollectionChoice::Disabled, + } + } +} + +struct ZedPredictUpsell; + impl Dismissable for ZedPredictUpsell { const KEY: &'static str = "dismissed-edit-predict-upsell"; fn dismissed(cx: &App) -> bool { - is_upsell_dismissed(cx) + // To make this backwards compatible with older versions of Zed, we + // check if the user has seen the previous Edit Prediction Onboarding + // before, by checking the data collection choice which was written to + // the database once the user clicked on "Accept and Enable" + let kvp = KeyValueStore::global(cx); + if kvp + .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) + .log_err() + .is_some_and(|s| s.is_some()) + { + return true; + } + + kvp.read_kvp(Self::KEY) + .log_err() + .is_some_and(|s| s.is_some()) } } pub fn should_show_upsell_modal(cx: &App) -> bool { - !is_upsell_dismissed(cx) + !ZedPredictUpsell::dismissed(cx) } pub fn init(cx: &mut App) { diff --git a/crates/edit_prediction/src/edit_prediction_tests.rs b/crates/edit_prediction/src/edit_prediction_tests.rs index 8869e1c3f3..0a8cd1b066 100644 --- a/crates/edit_prediction/src/edit_prediction_tests.rs +++ b/crates/edit_prediction/src/edit_prediction_tests.rs @@ -3,16 +3,11 @@ use crate::udiff::apply_diff_to_string; use client::{RefreshLlmTokenListener, UserStore, test::FakeServer}; use clock::FakeSystemClock; use clock::ReplicaId; -use cloud_api_types::{ - CreateLlmTokenResponse, LlmToken, Organization, OrganizationConfiguration, - OrganizationEditPredictionConfiguration, OrganizationId, -}; +use cloud_api_types::{CreateLlmTokenResponse, LlmToken}; use cloud_llm_client::{ EditPredictionRejectReason, EditPredictionRejection, RejectEditPredictionsBody, predict_edits_v3::{PredictEditsV3Request, PredictEditsV3Response}, }; -use db::AppDatabase; -use settings::EditPredictionDataCollectionChoice; use futures::{ AsyncReadExt, FutureExt, StreamExt, @@ -2395,31 +2390,12 @@ struct RequestChannels { fn init_test_with_fake_client( cx: &mut TestAppContext, -) -> (Entity, RequestChannels) { - init_test_with_fake_client_and_legacy_data_collection(cx, None) -} - -fn init_test_with_fake_client_and_legacy_data_collection( - cx: &mut TestAppContext, - legacy_data_collection_choice: Option<&str>, ) -> (Entity, RequestChannels) { cx.update(move |cx| { - cx.set_global(AppDatabase::test_new()); let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); zlog::init_test(); - if let Some(legacy_data_collection_choice) = legacy_data_collection_choice { - KeyValueStore::global(cx) - .write_kvp( - ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), - legacy_data_collection_choice.to_string(), - ) - .now_or_never() - .expect("legacy data collection write should complete immediately") - .expect("legacy data collection write should succeed"); - } - let (predict_req_tx, predict_req_rx) = mpsc::unbounded(); let (reject_req_tx, reject_req_rx) = mpsc::unbounded(); @@ -2796,7 +2772,6 @@ async fn test_v3_prediction_strips_cursor_marker_from_edit_text(cx: &mut TestApp fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { - cx.set_global(AppDatabase::test_new()); let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); }); @@ -3417,252 +3392,6 @@ async fn test_edit_prediction_settled(cx: &mut TestAppContext) { } } -#[gpui::test] -async fn test_data_collection_disabled_by_default(cx: &mut TestAppContext) { - let (ep_store, _channels) = init_test_with_fake_client(cx); - - cx.update(|cx| { - assert!(!ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -#[gpui::test] -async fn test_data_collection_enabled_via_legacy_kv_store(cx: &mut TestAppContext) { - let (ep_store, _channels) = - init_test_with_fake_client_and_legacy_data_collection(cx, Some("true")); - - cx.update(|cx| { - assert!(ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -#[gpui::test] -async fn test_data_collection_default_uses_cached_legacy_value(cx: &mut TestAppContext) { - let (ep_store, _channels) = - init_test_with_fake_client_and_legacy_data_collection(cx, Some("true")); - - cx.update(|cx| { - assert!(ep_store.read(cx).is_data_collection_enabled(cx)); - }); - - cx.update(|cx| KeyValueStore::global(cx)) - .delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into()) - .await - .unwrap(); - - cx.update(|cx| { - assert!(ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -#[gpui::test] -async fn test_data_collection_setting_overrides_kv_store(cx: &mut TestAppContext) { - let (ep_store, _channels) = - init_test_with_fake_client_and_legacy_data_collection(cx, Some("true")); - - // An explicit false in settings.json wins over the KV store. - cx.update_global::(|settings, cx| { - settings.update_user_settings(cx, |content| { - content - .project - .all_languages - .edit_predictions - .get_or_insert_default() - .allow_data_collection = Some(EditPredictionDataCollectionChoice::No); - }); - }); - - cx.update(|cx| { - assert!(!ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -#[gpui::test] -async fn test_data_collection_enabled_via_setting(cx: &mut TestAppContext) { - let (ep_store, _channels) = init_test_with_fake_client(cx); - - cx.update_global::(|settings, cx| { - settings.update_user_settings(cx, |content| { - content - .project - .all_languages - .edit_predictions - .get_or_insert_default() - .allow_data_collection = Some(EditPredictionDataCollectionChoice::Yes); - }); - }); - - cx.update(|cx| { - assert!(ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -#[gpui::test] -async fn test_data_collection_always_enabled_for_staff(cx: &mut TestAppContext) { - let (ep_store, _channels) = init_test_with_fake_client(cx); - - cx.update(|cx| { - cx.set_staff(true); - assert!(ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -#[gpui::test] -async fn test_data_collection_disabled_by_organization_configuration(cx: &mut TestAppContext) { - let (ep_store, _channels) = init_test_with_fake_client(cx); - - cx.update_global::(|settings, cx| { - settings.update_user_settings(cx, |content| { - content - .project - .all_languages - .edit_predictions - .get_or_insert_default() - .allow_data_collection = Some(EditPredictionDataCollectionChoice::Yes); - }); - }); - - let user_store = cx.update(|cx| ep_store.read(cx).user_store.clone()); - cx.update(|cx| { - user_store.update(cx, |user_store, cx| { - user_store.set_current_organization_configuration_for_test( - Arc::new(Organization { - id: OrganizationId("org-1".into()), - name: "Org 1".into(), - is_personal: false, - }), - OrganizationConfiguration { - is_zed_model_provider_enabled: true, - is_agent_thread_feedback_enabled: true, - is_collaboration_enabled: true, - edit_prediction: OrganizationEditPredictionConfiguration { - is_enabled: true, - is_feedback_enabled: false, - }, - }, - cx, - ); - }); - - assert!(!ep_store.read(cx).is_data_collection_enabled(cx)); - }); -} - -// When a user had data collection enabled via the legacy KV store (with no explicit -// setting in settings.json), toggle_data_collection must read the *resolved* state -// (true) and write Some(false). -#[gpui::test] -async fn test_toggle_data_collection_from_kv_enabled_state(cx: &mut TestAppContext) { - let (ep_store, _channels) = - init_test_with_fake_client_and_legacy_data_collection(cx, Some("true")); - - cx.update(|cx| { - assert!( - ep_store.read(cx).is_data_collection_enabled(cx), - "data collection should be enabled via KV store before toggle" - ); - }); - - // Simulate what toggle_data_collection does: capture the resolved current - // state, then write its inverse. - let is_currently_enabled = cx.update(|cx| ep_store.read(cx).is_data_collection_enabled(cx)); - cx.update_global::(|settings, cx| { - settings.update_user_settings(cx, |content| { - content - .project - .all_languages - .edit_predictions - .get_or_insert_default() - .allow_data_collection = Some(if is_currently_enabled { - EditPredictionDataCollectionChoice::No - } else { - EditPredictionDataCollectionChoice::Yes - }); - }); - }); - - cx.update(|cx| { - assert!( - !ep_store.read(cx).is_data_collection_enabled(cx), - "data collection should be disabled after toggling off from KV-enabled state" - ); - }); -} - -#[gpui::test] -async fn test_upsell_shown_by_default(cx: &mut TestAppContext) { - init_test(cx); - let kvp = cx.update(|cx| KeyValueStore::global(cx)); - kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into()) - .await - .ok(); - kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.ok(); - - cx.update(|cx| assert!(should_show_upsell_modal(cx))); -} - -#[gpui::test] -async fn test_upsell_dismissed_when_data_collection_choice_in_kv_store(cx: &mut TestAppContext) { - init_test(cx); - - // Any value for the data collection key means the old upsell was already - // shown, regardless of whether data collection was accepted or declined. - for value in &["true", "false"] { - cx.update(|cx| KeyValueStore::global(cx)) - .write_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), value.to_string()) - .await - .unwrap(); - - cx.update(|cx| { - assert!( - !should_show_upsell_modal(cx), - "upsell should be suppressed when data collection choice is '{value}'" - ); - }); - } - - cx.update(|cx| KeyValueStore::global(cx)) - .delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into()) - .await - .unwrap(); -} - -#[gpui::test] -async fn test_upsell_dismissed_when_dismissed_key_set(cx: &mut TestAppContext) { - init_test(cx); - let kvp = cx.update(|cx| KeyValueStore::global(cx)); - kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into()) - .await - .ok(); - kvp.write_kvp(ZedPredictUpsell::KEY.into(), "1".into()) - .await - .unwrap(); - - cx.update(|cx| assert!(!should_show_upsell_modal(cx))); - - kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.unwrap(); -} - -#[gpui::test] -async fn test_upsell_dismissed_via_dismissable_api(cx: &mut TestAppContext) { - init_test(cx); - let kvp = cx.update(|cx| KeyValueStore::global(cx)); - kvp.delete_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE.into()) - .await - .ok(); - kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.ok(); - - cx.update(|cx| { - assert!(should_show_upsell_modal(cx)); - ZedPredictUpsell::set_dismissed(true, cx); - }); - cx.run_until_parked(); - - cx.update(|cx| assert!(!should_show_upsell_modal(cx))); - - kvp.delete_kvp(ZedPredictUpsell::KEY.into()).await.unwrap(); -} - #[ctor::ctor] fn init_logger() { zlog::init_test(); diff --git a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs index 072051a8de..f0fa37c4d6 100644 --- a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs +++ b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs @@ -7,11 +7,9 @@ use edit_prediction_types::{ EditPredictionIconSet, SuggestionDisplayType, }; use feature_flags::FeatureFlagAppExt; -use fs::Fs; use gpui::{App, Entity, prelude::*}; use language::{Buffer, ToPoint as _}; use project::Project; -use settings::{EditPredictionDataCollectionChoice, update_settings_file}; use crate::{BufferEditPrediction, EditPredictionStore}; @@ -77,7 +75,24 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { .read(cx) .is_file_open_source(&self.project, file, cx); - if self.store.read(cx).is_data_collection_enabled(cx) { + if let Some(organization_configuration) = self + .store + .read(cx) + .user_store + .read(cx) + .current_organization_configuration() + { + if !organization_configuration + .edit_prediction + .is_feedback_enabled + { + return DataCollectionState::Disabled { + is_project_open_source, + }; + } + } + + if self.store.read(cx).data_collection_choice.is_enabled(cx) { DataCollectionState::Enabled { is_project_open_source, } @@ -87,9 +102,9 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { } } } else { - DataCollectionState::Disabled { + return DataCollectionState::Disabled { is_project_open_source: false, - } + }; } } @@ -98,26 +113,27 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { return false; } - self.store + if let Some(organization_configuration) = self + .store .read(cx) - .is_data_collection_allowed_by_organization(cx) + .user_store + .read(cx) + .current_organization_configuration() + { + if !organization_configuration + .edit_prediction + .is_feedback_enabled + { + return false; + } + } + + true } fn toggle_data_collection(&mut self, cx: &mut App) { - let fs = ::global(cx); - let is_currently_enabled = self.store.read(cx).is_data_collection_enabled(cx); - update_settings_file(fs, cx, move |settings, _| { - let edit_predictions = settings - .project - .all_languages - .edit_predictions - .get_or_insert_default(); - - edit_predictions.allow_data_collection = Some(if is_currently_enabled { - EditPredictionDataCollectionChoice::No - } else { - EditPredictionDataCollectionChoice::Yes - }); + self.store.update(cx, |store, cx| { + store.toggle_data_collection_choice(cx); }); } diff --git a/crates/edit_prediction_context/src/edit_prediction_context.rs b/crates/edit_prediction_context/src/edit_prediction_context.rs index bbd12dec4e..261d3383d0 100644 --- a/crates/edit_prediction_context/src/edit_prediction_context.rs +++ b/crates/edit_prediction_context/src/edit_prediction_context.rs @@ -289,11 +289,6 @@ impl RelatedExcerptStore { .ok()?; let type_definitions = project .update(cx, |project, cx| { - // tombi LSP for toml will open a scratch buffer with the JSON schema of - // the toml file when a goto type definition is requested - if is_tombi_lsp_in_toml(project, &buffer, cx) { - return Task::ready(Ok(None)); - } project.type_definitions(&buffer, identifier.range.start, cx) }) .ok()?; @@ -568,7 +563,7 @@ impl RelatedBuffer { }) .collect::>(); self.cached_file = Some(CachedRelatedFile { - excerpts, + excerpts: excerpts, buffer_version: buffer.version().clone(), }); self.cached_file.as_ref().unwrap() @@ -674,7 +669,6 @@ fn identifiers_for_position( if let Some(config) = config && config.identifier_capture_indices.contains(&capture.index) && range.contains_inclusive(&node_range) - && !is_tsx_tag(&buffer, &capture.node) && Some(&node_range) != last_range.as_ref() { let name = buffer.text_for_range(node_range.clone()).collect(); @@ -692,59 +686,3 @@ fn identifiers_for_position( identifiers } - -fn is_tsx_tag(buffer: &BufferSnapshot, node: &tree_sitter::Node) -> bool { - let Some(language_config) = buffer - .language() - .and_then(|l| l.config().jsx_tag_auto_close.as_ref()) - else { - return false; - }; - let Some(parent_kind) = node.parent().map(|n| n.kind()) else { - return false; - }; - - if parent_kind != &language_config.open_tag_node_name - && parent_kind != &language_config.close_tag_node_name - && parent_kind != &language_config.tag_name_node_name - && language_config - .erroneous_close_tag_name_node_name - .as_ref() - .is_some_and(|kind| parent_kind != kind) - && language_config - .erroneous_close_tag_node_name - .as_ref() - .is_some_and(|kind| parent_kind == kind) - && parent_kind != &language_config.jsx_element_node_name - { - return false; - } - // do fetch ``, model probably understands `
`, but needs info for user defined components - if !buffer - .text_for_range(node.byte_range()) - .all(|str| str.chars().all(|c| c.is_lowercase())) - { - return false; - } - true -} - -fn is_tombi_lsp_in_toml( - project: &Project, - buffer: &Entity, - cx: &mut Context, -) -> bool { - buffer.update(cx, |buffer, cx| { - if !buffer.language().is_some_and(|lang| lang.name() == "TOML") { - return false; - } - project.lsp_store().update(cx, |lsp_store, cx| { - for (_, lsp) in lsp_store.running_language_servers_for_local_buffer(buffer, cx) { - if "tombi".eq_ignore_ascii_case(lsp.name().as_ref()) { - return true; - } - } - false - }) - }) -} diff --git a/crates/editor/src/code_lens.rs b/crates/editor/src/code_lens.rs index 87d2426878..c1bf2525d9 100644 --- a/crates/editor/src/code_lens.rs +++ b/crates/editor/src/code_lens.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{iter, ops::Range, sync::Arc}; use collections::{HashMap, HashSet}; use futures::future::join_all; @@ -7,15 +7,17 @@ use itertools::Itertools; use language::{BufferId, ClientCommand}; use multi_buffer::{Anchor, MultiBufferRow, MultiBufferSnapshot, ToPoint as _}; use project::{CodeAction, TaskSourceKind}; +use settings::Settings as _; use task::TaskContext; +use text::Point; use ui::{Context, Window, div, prelude::*}; +use workspace::PreviewTabsSettings; use crate::{ - Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, SelectionEffects, + Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultibufferSelectionMode, SelectionEffects, actions::ToggleCodeLens, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, - hover_links::HoverLink, }; #[derive(Clone, Debug)] @@ -76,7 +78,6 @@ fn group_lenses_by_row( } fn render_code_lens_line( - buffer_id: BufferId, line_number: usize, lens: CodeLensLine, editor: WeakEntity, @@ -103,11 +104,11 @@ fn render_code_lens_line( let action = item.action.clone(); let editor_handle = editor.clone(); let position = lens.position; - let id = SharedString::from(format!("{buffer_id}:{line_number}:{i}")); + let id = (line_number as u64) << 32 | (i as u64); children.push( div() - .id(ElementId::Name(id)) + .id(ElementId::Integer(id)) .font(font.clone()) .text_size(font_size) .text_color(cx.app.theme().colors().text_muted) @@ -205,7 +206,7 @@ pub(super) fn try_handle_client_command( schedule_task(task_template, action, editor, workspace, window, cx) } Some(ClientCommand::ShowLocations) => { - try_show_references(arguments, action, editor, window, cx) + try_show_references(arguments, action, workspace, window, cx) } None => false, } @@ -260,7 +261,7 @@ fn schedule_task( fn try_show_references( arguments: &[serde_json::Value], action: &CodeAction, - editor: &mut Editor, + workspace: &gpui::Entity, window: &mut Window, cx: &mut Context, ) -> bool { @@ -275,18 +276,73 @@ fn try_show_references( } let server_id = action.server_id; - let nav_entry = editor.navigation_entry(editor.selections.newest_anchor().head(), cx); - let links = locations - .into_iter() - .map(|location| HoverLink::InlayHint(location, server_id)) - .collect(); - editor - .navigate_to_hover_links(None, links, nav_entry, false, window, cx) - .detach_and_log_err(cx); + let project = workspace.read(cx).project().clone(); + let workspace = workspace.clone(); + + cx.spawn_in(window, async move |_editor, cx| { + let mut buffer_locations = std::collections::HashMap::default(); + + for location in &locations { + let open_task = cx.update(|_, cx| { + project.update(cx, |project, cx| { + let uri: lsp::Uri = location.uri.clone(); + project.open_local_buffer_via_lsp(uri, server_id, cx) + }) + })?; + let buffer = open_task.await?; + + let range = range_from_lsp(location.range); + buffer_locations + .entry(buffer) + .or_insert_with(Vec::new) + .push(range); + } + + workspace.update_in(cx, |workspace, window, cx| { + let target = buffer_locations + .iter() + .flat_map(|(k, v)| iter::repeat(k.clone()).zip(v)) + .map(|(buffer, location)| { + buffer + .read(cx) + .text_for_range(location.clone()) + .collect::() + }) + .filter(|text| !text.contains('\n')) + .unique() + .take(3) + .join(", "); + let title = if target.is_empty() { + "References".to_owned() + } else { + format!("References to {target}") + }; + let allow_preview = + PreviewTabsSettings::get_global(cx).enable_preview_multibuffer_from_code_navigation; + Editor::open_locations_in_multibuffer( + workspace, + buffer_locations, + title, + false, + allow_preview, + MultibufferSelectionMode::First, + window, + cx, + ); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); true } +fn range_from_lsp(range: lsp::Range) -> Range { + let start = Point::new(range.start.line, range.start.character); + let end = Point::new(range.end.line, range.end.character); + start..end +} + impl Editor { pub(super) fn refresh_code_lenses( &mut self, @@ -413,7 +469,6 @@ impl Editor { height: Some(1), style: BlockStyle::Flex, render: Arc::new(render_code_lens_line( - buffer_id, line_number, lens_line, editor_handle.clone(), @@ -554,7 +609,6 @@ impl Editor { height: Some(1), style: BlockStyle::Flex, render: Arc::new(render_code_lens_line( - buffer_id, line_number, lens_line, editor_handle.clone(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9102fc428f..b01d1592ab 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3860,6 +3860,9 @@ impl Editor { cx.emit(EditorEvent::SelectionsChanged { local }); let selections = &self.selections.disjoint_anchors_arc(); + if selections.len() == 1 { + cx.emit(SearchEvent::ActiveMatchChanged) + } if local && let Some(buffer_snapshot) = buffer.as_singleton() { let inmemory_selections = selections .iter() @@ -9255,9 +9258,9 @@ impl Editor { })) .tooltip(move |_window, cx| { Tooltip::with_meta_in( - "Remove Bookmark", + "Remove bookmark", Some(&ToggleBookmark), - SharedString::from("Right-click for more options"), + SharedString::from("Right-click for more options."), &focus_handle, cx, ) @@ -9543,16 +9546,15 @@ impl Editor { }; let primary_action_text = "Unset breakpoint"; let focus_handle = self.focus_handle.clone(); - let has_context_menu = self.has_mouse_context_menu(); let meta = if is_rejected { SharedString::from("No executable code is associated with this line.") } else if !breakpoint.is_disabled() { SharedString::from(format!( - "{alt_as_text}-click to disable\nright-click for more options" + "{alt_as_text}click to disable,\nright-click for more options." )) } else { - SharedString::from("Right-click for more options") + SharedString::from("Right-click for more options.") }; IconButton::new(("breakpoint_indicator", row.0 as usize), icon) .icon_size(IconSize::XSmall) @@ -9582,16 +9584,14 @@ impl Editor { .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx); })) - .when(!has_context_menu, |button| { - button.tooltip(move |_window, cx| { - Tooltip::with_meta_in( - primary_action_text, - Some(&ToggleBreakpoint), - meta.clone(), - &focus_handle, - cx, - ) - }) + .tooltip(move |_window, cx| { + Tooltip::with_meta_in( + primary_action_text, + Some(&ToggleBreakpoint), + meta.clone(), + &focus_handle, + cx, + ) }) } @@ -9637,10 +9637,10 @@ impl Editor { }; match self { Intent::SetBookmark => format!( - "{alt_as_text}-click to add a breakpoint\nright-click for more options" + "{alt_as_text}click to add a breakpoint,\nright-click for more options." ), Intent::SetBreakpoint => format!( - "{alt_as_text}-click to add a bookmark\nright-click for more options" + "{alt_as_text}click to add a bookmark,\nright-click for more options." ), } } @@ -9667,7 +9667,6 @@ impl Editor { }; let focus_handle = self.focus_handle.clone(); - let has_context_menu = self.has_mouse_context_menu(); IconButton::new(("add_breakpoint_button", row.0 as usize), intent.icon()) .icon_size(IconSize::XSmall) .size(ui::ButtonSize::None) @@ -9696,16 +9695,14 @@ impl Editor { .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { editor.set_gutter_context_menu(row, Some(position), event.position(), window, cx); })) - .when(!has_context_menu, |button| { - button.tooltip(move |_window, cx| { - Tooltip::with_meta_in( - intent.as_str(), - Some(&ToggleBreakpoint), - intent.secondary_and_options(), - &focus_handle, - cx, - ) - }) + .tooltip(move |_window, cx| { + Tooltip::with_meta_in( + intent.as_str(), + Some(&ToggleBreakpoint), + intent.secondary_and_options(), + &focus_handle, + cx, + ) }) } @@ -16013,7 +16010,7 @@ impl Editor { } } - pub(crate) fn navigation_entry( + fn navigation_entry( &self, cursor_anchor: Anchor, cx: &mut Context, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 025cd4251a..6dfbf33402 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -3,7 +3,7 @@ use crate::{ JoinLines, code_context_menus::CodeContextMenu, edit_prediction_tests::FakeEditPredictionDelegate, - element::{StickyHeader, header_jump_data}, + element::StickyHeader, linked_editing_ranges::LinkedEditingRanges, runnables::RunnableTasks, scroll::scroll_amount::ScrollAmount, @@ -19335,112 +19335,6 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { }); } -#[gpui::test] -fn test_header_jump_data_uses_selection_excerpt(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - // 25-line buffer so excerpts at rows 1, 10, and 20 (each a 1-line range, - // expanded by 2 context lines) can't merge into a single excerpt. - let buffer_text = (0..25) - .map(|row| format!("line {row}")) - .collect::>() - .join("\n"); - let buffer = cx.new(|cx| Buffer::local(buffer_text, cx)); - let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(ReadWrite); - multibuffer.set_excerpts_for_path( - PathKey::sorted(0), - buffer.clone(), - [ - Point::new(1, 0)..Point::new(1, 0), - Point::new(10, 0)..Point::new(10, 0), - Point::new(20, 0)..Point::new(20, 0), - ], - 2, - cx, - ); - multibuffer - }); - - let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx)); - - editor.update_in(cx, |editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let display_snapshot = editor.display_snapshot(cx); - - // Ensure the three ranges landed in three separate excerpts. - let excerpts: Vec<_> = snapshot - .buffer_snapshot() - .excerpts_for_buffer(buffer_id) - .collect(); - assert_eq!(excerpts.len(), 3); - - // Place the cursor at the start of the third excerpt, expressed in - // terms of the underlying buffer. - let selection_buffer_row = 20; - let buffer_entity = editor.buffer().read(cx).buffer(buffer_id).unwrap(); - let selection_anchor = editor.buffer().update(cx, |multibuffer, cx| { - multibuffer - .buffer_point_to_anchor(&buffer_entity, Point::new(selection_buffer_row, 0), cx) - .expect("buffer row 20 maps to a multibuffer anchor") - }); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_anchor_ranges([selection_anchor..selection_anchor]) - }); - - let mut latest_selection_anchors: HashMap = HashMap::default(); - for selection in editor.selections.all_anchors(&display_snapshot).iter() { - let head = selection.head(); - if let Some((text_anchor, _)) = snapshot.buffer_snapshot().anchor_to_buffer_anchor(head) - { - latest_selection_anchors.insert(text_anchor.buffer_id, head); - } - } - - // The sticky buffer header represents the FIRST excerpt of its buffer, - // even when the cursor is in a later excerpt. That mismatch is the - // precondition for the regression. - let first_excerpt = snapshot - .buffer_snapshot() - .excerpt_boundaries_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len()) - .next() - .expect("multibuffer has at least one excerpt") - .next; - - let jump_data = header_jump_data( - &snapshot, - DisplayRow(0), - FILE_HEADER_HEIGHT + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, - &first_excerpt, - &latest_selection_anchors, - ); - - match jump_data { - JumpData::MultiBufferPoint { - position, - line_offset_from_top, - .. - } => { - assert_eq!( - position.row, selection_buffer_row, - "jump should target the cursor's buffer row, not the first excerpt's row" - ); - assert!( - line_offset_from_top < selection_buffer_row, - "line_offset_from_top ({line_offset_from_top}) should be measured from the \ - selection's excerpt, not the first excerpt; expected less than \ - selection_buffer_row ({selection_buffer_row})" - ); - } - JumpData::MultiBufferRow { .. } => { - panic!("expected MultiBufferPoint jump data when a selection is present") - } - } - }); -} - #[gpui::test] async fn test_extra_newline_insertion(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -32821,78 +32715,6 @@ async fn test_sticky_scroll(cx: &mut TestAppContext) { assert_eq!(sticky_headers(10.0), vec![]); } -#[gpui::test] -async fn test_sticky_scroll_with_decoration_prefix_in_item(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - let mut cx = EditorTestContext::new(cx).await; - - let language = Arc::new( - Language::new( - LanguageConfig { - name: "TypeScript".into(), - ..Default::default() - }, - Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), - ) - .with_outline_query( - r#" - (class_declaration - "class" @context - name: (_) @name) @item - "#, - ) - .expect("TypeScript outline query"), - ); - - let buffer = indoc! {" - ˇ@Decorator - class Foo { - x = 1; - y = 2; - z = 3; - w = 4; - } - "}; - cx.set_state(buffer); - cx.update_editor(|e, _, cx| { - e.buffer() - .read(cx) - .as_singleton() - .unwrap() - .update(cx, |buffer, cx| { - buffer.set_language(Some(language), 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 class_foo = Point { row: 1, column: 0 }; - - assert_eq!(sticky_headers(0.0), vec![]); - assert_eq!(sticky_headers(1.5), vec![(class_foo, 0.0)]); - assert_eq!(sticky_headers(2.5), vec![(class_foo, 0.0)]); - assert_eq!(sticky_headers(5.5), vec![(class_foo, -0.5)]); - assert_eq!(sticky_headers(6.0), vec![]); - assert_eq!(sticky_headers(7.0), vec![]); -} - #[gpui::test] async fn test_sticky_scroll_with_expanded_deleted_diff_hunks( executor: BackgroundExecutor, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1cefd2179b..0646bf7a06 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -895,8 +895,7 @@ impl EditorElement { let hitbox = &position_map.gutter_hitbox; if event.position.x <= hitbox.bounds.right() - gutter_right_padding - // Don't show the gutter_context_menu in collab notes - && editor.project.is_some() + && editor.collaboration_hub.is_none() { let point_for_position = position_map.point_for_position(event.position); editor.set_gutter_context_menu( @@ -1395,7 +1394,7 @@ impl EditorElement { indicator.is_active && start_row == valid_point.row() }); - let gutter_hover_button = if gutter_hovered + let breakpoint_indicator = if gutter_hovered && !is_on_diff_review_button_row && split_side != Some(SplitSide::Left) { @@ -1440,16 +1439,13 @@ impl EditorElement { editor.gutter_hover_button.1 = None; None } - } else if editor.has_mouse_context_menu() { - editor.gutter_hover_button.1 = None; - editor.gutter_hover_button.0 } else { editor.gutter_hover_button.1 = None; None }; - if &gutter_hover_button != &editor.gutter_hover_button.0 { - editor.gutter_hover_button.0 = gutter_hover_button; + if &breakpoint_indicator != &editor.gutter_hover_button.0 { + editor.gutter_hover_button.0 = breakpoint_indicator; cx.notify(); } @@ -4732,10 +4728,7 @@ impl EditorElement { let mut rows = Vec::::new(); for item in editor.sticky_headers.iter().flatten() { - let start_point = item - .source_range_for_text - .start - .to_point(snapshot.buffer_snapshot()); + let start_point = item.range.start.to_point(snapshot.buffer_snapshot()); let end_point = item.range.end.to_point(snapshot.buffer_snapshot()); let sticky_row = snapshot @@ -8355,34 +8348,21 @@ pub(crate) fn header_jump_data( ) -> JumpData { let multibuffer_snapshot = editor_snapshot.buffer_snapshot(); let buffer = first_excerpt.buffer(multibuffer_snapshot); - let (jump_anchor, jump_buffer, excerpt_start) = if let Some(anchor) = + let (jump_anchor, jump_buffer) = if let Some(anchor) = latest_selection_anchors.get(&first_excerpt.buffer_id()) && let Some((jump_anchor, selection_buffer)) = multibuffer_snapshot.anchor_to_buffer_anchor(*anchor) { - let jump_offset = text::ToOffset::to_offset(&jump_anchor, selection_buffer); - let selection_excerpt_start = multibuffer_snapshot - .excerpts_for_buffer(jump_anchor.buffer_id) - .find(|excerpt| { - let start = text::ToOffset::to_offset(&excerpt.context.start, selection_buffer); - let end = text::ToOffset::to_offset(&excerpt.context.end, selection_buffer); - start <= jump_offset && jump_offset <= end - }) - .map(|excerpt| excerpt.context.start) - .unwrap_or(first_excerpt.range.context.start); - (jump_anchor, selection_buffer, selection_excerpt_start) + (jump_anchor, selection_buffer) } else { - ( - first_excerpt.range.primary.start, - buffer, - first_excerpt.range.context.start, - ) + (first_excerpt.range.primary.start, buffer) }; + let excerpt_start = first_excerpt.range.context.start; let jump_position = language::ToPoint::to_point(&jump_anchor, jump_buffer); let rows_from_excerpt_start = if jump_anchor == excerpt_start { 0 } else { - let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, jump_buffer); + let excerpt_start_point = language::ToPoint::to_point(&excerpt_start, buffer); jump_position.row.saturating_sub(excerpt_start_point.row) }; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 125f09c966..1752aefc5e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1799,10 +1799,6 @@ impl SearchableItem for Editor { }); } } - - /// Takes the current cursor position and finds the next match in the - /// provided `direction`, the provide `count` number of times, wrapping - /// around if necessary. fn match_index_for_direction( &mut self, matches: &[Range], @@ -1813,48 +1809,45 @@ impl SearchableItem for Editor { _: &mut Window, cx: &mut Context, ) -> usize { - if count == 0 { - return current_index; - } - - let cursor = if self.selections.disjoint_anchors_arc().len() == 1 { + let buffer = self.buffer().read(cx).snapshot(cx); + let current_index_position = if self.selections.disjoint_anchors_arc().len() == 1 { self.selections.newest_anchor().head() } else { matches[current_index].start }; - let buffer = self.buffer().read(cx).snapshot(cx); - let new_idx = match direction { - Direction::Next => matches - .iter() - .position(|m| m.start.cmp(&cursor, &buffer).is_gt()) - .unwrap_or(0), - Direction::Prev => matches - .iter() - .rposition(|m| m.end.cmp(&cursor, &buffer).is_lt()) - .unwrap_or(matches.len() - 1), - } as isize; + let mut count = count % matches.len(); + if count == 0 { + return current_index; + } + match direction { + Direction::Next => { + if matches[current_index] + .start + .cmp(¤t_index_position, &buffer) + .is_gt() + { + count -= 1 + } - // We'll use `count - 1` because the first jump to the next or previous - // match already happens in the scenario above, when we find the next or - // previous match starting from the cursor position. - let count = count.saturating_sub(1); - let count = match direction { - Direction::Prev => -(count as isize), - Direction::Next => count as isize, - }; + (current_index + count) % matches.len() + } + Direction::Prev => { + if matches[current_index] + .end + .cmp(¤t_index_position, &buffer) + .is_lt() + { + count -= 1; + } - let new_idx = (new_idx + count) % matches.len() as isize; - let new_idx = if new_idx.is_negative() { - // We need a `matches.len() - 1` here in case `next_idx` has now been - // set to `0`, otherwise we'd end up returning `matches.len()`, which - // would be out of bounds. - new_idx + (matches.len() - 1) as isize - } else { - new_idx - }; - assert!(new_idx < matches.len() as isize); - new_idx as usize + if current_index >= count { + current_index - count + } else { + matches.len() - (count - current_index) + } + } + } } fn find_matches( diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 347383d017..43479f1713 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -433,17 +433,6 @@ impl SplittableEditor { self.lhs.as_ref().map(|s| &s.editor) } - pub fn update_editors( - &self, - cx: &mut Context, - f: impl Fn(&mut Editor, &mut Context), - ) { - if let Some(lhs) = &self.lhs { - lhs.editor.update(cx, &f); - } - self.rhs_editor.update(cx, &f); - } - pub fn diff_view_style(&self) -> DiffViewStyle { self.diff_view_style } @@ -457,18 +446,15 @@ impl SplittableEditor { render_diff_hunk_controls: RenderDiffHunkControlsFn, cx: &mut Context, ) { - self.update_editors(cx, |editor, cx| { + self.rhs_editor.update(cx, |editor, cx| { editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx); }); - } - pub fn disable_diff_hunk_controls(&self, cx: &mut Context) { - let empty_controls = Arc::new(|_, _: &_, _, _, _, _: &_, _: &mut _, _: &mut _| { - gpui::Empty.into_any_element() - }); - self.update_editors(cx, |editor, cx| { - editor.set_render_diff_hunk_controls(empty_controls.clone(), cx); - }); + if let Some(lhs) = &self.lhs { + lhs.editor.update(cx, |editor, cx| { + editor.set_render_diff_hunk_controls(render_diff_hunk_controls.clone(), cx); + }); + } } fn focused_side(&self) -> SplitSide { @@ -505,7 +491,6 @@ impl SplittableEditor { editor.set_expand_all_diff_hunks(cx); editor.disable_runnables(); editor.disable_inline_diagnostics(); - editor.disable_mouse_wheel_zoom(); editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx); editor.start_temporary_diff_override(); editor @@ -601,7 +586,6 @@ impl SplittableEditor { editor.disable_lsp_data(); editor.disable_runnables(); editor.disable_diagnostics(cx); - editor.disable_mouse_wheel_zoom(); editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx); editor }); @@ -2141,9 +2125,14 @@ mod tests { window, cx, ); - editor.update_editors(cx, |editor, cx| { + editor.rhs_editor.update(cx, |editor, cx| { editor.set_soft_wrap_mode(soft_wrap, cx); }); + if let Some(lhs) = &editor.lhs { + lhs.editor.update(cx, |editor, cx| { + editor.set_soft_wrap_mode(soft_wrap, cx); + }); + } editor }); (editor, cx) diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index f7f09956c1..67ebab6229 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -31,7 +31,6 @@ settings.workspace = true serde.workspace = true theme.workspace = true ui.workspace = true -ui_input.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index d75481f6f7..9a9cc983fa 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -13,8 +13,8 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy_nucleo::{PathMatch, PathMatchCandidate}; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, - StatefulInteractiveElement, Styled, Task, WeakEntity, Window, actions, rems, + KeyContext, Modifiers, ModifiersChangedEvent, ParentElement, Render, Styled, Task, WeakEntity, + Window, actions, rems, }; use open_path_prompt::{ OpenPathPrompt, @@ -37,10 +37,9 @@ use std::{ }, }; use ui::{ - ButtonLike, CommonAnimationExt, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, - ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*, + ButtonLike, ContextMenu, HighlightedLabel, Indicator, KeyBinding, ListItem, ListItemSpacing, + PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*, }; -use ui_input::ErasedEditor; use util::{ ResultExt, maybe, paths::{PathStyle, PathWithPosition}, @@ -1758,41 +1757,6 @@ impl PickerDelegate for FileFinderDelegate { ) } - fn render_editor( - &self, - editor: &Arc, - window: &mut Window, - cx: &mut Context>, - ) -> Div { - let has_search_query = self.latest_search_query.is_some(); - let is_project_scan_running = { - let worktree_store = self.project.read(cx).worktree_store(); - !worktree_store.read(cx).initial_scan_completed() - }; - - h_flex() - .flex_none() - .h_9() - .px_2p5() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(editor.render(window, cx)) - .when(is_project_scan_running && has_search_query, |this| { - this.child( - h_flex() - .id("project-scan-indicator") - .tooltip(Tooltip::text("Project Scan in Progress…")) - .child( - Icon::new(IconName::LoadCircle) - .color(Color::Accent) - .size(IconSize::Small) - .with_rotate_animation(2), - ), - ) - }) - } - fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { let focus_handle = self.focus_handle.clone(); diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index eefe2717f2..e7b8dcd4eb 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -36,7 +36,6 @@ thiserror.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true -telemetry.workspace = true tempfile.workspace = true text.workspace = true time.workspace = true diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 709b037c05..6694b19e37 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -72,133 +72,6 @@ pub trait Watcher: Send + Sync { fn remove(&self, path: &Path) -> Result<()>; } -/// Detect whether a path requires polling instead of native file watching. -/// -/// Returns `true` for filesystem types where inotify/FSEvents/ReadDirectoryChanges -/// silently fail to deliver events: 9P (WSL drvfs), NFS, CIFS/SMB, FUSE (sshfs), etc. -/// -/// Can be overridden with the `ZED_FILE_WATCHER_MODE` environment variable: -/// - `native` — always use native OS watcher -/// - `poll` — always use polling -/// - `auto` (default) — auto-detect based on filesystem type -pub fn requires_poll_watcher(path: &Path) -> bool { - match std::env::var("ZED_FILE_WATCHER_MODE") - .as_deref() - .unwrap_or("auto") - { - "native" => return false, - "poll" => return true, - _ => {} - } - - #[cfg(target_os = "linux")] - { - let path = effective_watch_path(path); - return detect_requires_poll_watcher_linux(&path); - } - - #[cfg(not(target_os = "linux"))] - { - let _ = path; - false - } -} - -pub fn effective_watch_path(path: &Path) -> PathBuf { - if path.exists() { - return path.to_path_buf(); - } - - for ancestor in path.ancestors() { - if ancestor.exists() { - return ancestor.to_path_buf(); - } - } - - path.to_path_buf() -} - -#[cfg(target_os = "linux")] -fn detect_requires_poll_watcher_linux(path: &Path) -> bool { - use std::ffi::CString; - use std::os::unix::ffi::OsStrExt; - - // Check filesystem type via statfs - let c_path = match CString::new(path.as_os_str().as_bytes()) { - Ok(p) => p, - Err(_) => return false, - }; - - let mut stat: libc::statfs = unsafe { std::mem::zeroed() }; - if unsafe { libc::statfs(c_path.as_ptr(), &mut stat) } != 0 { - return false; - } - - // Filesystem magic numbers where inotify does not deliver events. - // These are defined in linux/magic.h and statfs(2). - const V9FS_MAGIC: u64 = 0x0102_1997; // Plan 9 / WSL2 interop (drvfs) - const NFS_SUPER_MAGIC: u64 = 0x0000_6969; - const CIFS_MAGIC: u64 = 0xFF53_4D42; // CIFS/SMB - const SMB_SUPER_MAGIC: u64 = 0x0000_517B; - const SMB2_MAGIC: u64 = 0xFE53_4D42; - const FUSE_SUPER_MAGIC: u64 = 0x6573_5546; // FUSE (includes sshfs) - - let fs_type = (stat.f_type as u64) & 0xFFFF_FFFF; - if fs_type == V9FS_MAGIC - || fs_type == NFS_SUPER_MAGIC - || fs_type == CIFS_MAGIC - || fs_type == SMB_SUPER_MAGIC - || fs_type == SMB2_MAGIC - || fs_type == FUSE_SUPER_MAGIC - { - log::info!( - "Detected network/virtual filesystem (type 0x{:x}) at {}, using poll watcher", - fs_type, - path.display() - ); - return true; - } - - // Also check for WSL + /mnt// pattern as a fallback - // in case statfs returns an unexpected type for drvfs - if is_wsl_drvfs_path(path) { - log::info!( - "Detected WSL drvfs mount at {}, using poll watcher", - path.display() - ); - return true; - } - - false -} - -#[cfg(target_os = "linux")] -fn is_wsl_drvfs_path(path: &Path) -> bool { - // Only relevant inside WSL - if std::env::var_os("WSL_DISTRO_NAME").is_none() { - if let Ok(version) = std::fs::read_to_string("/proc/version") { - let v = version.to_lowercase(); - if !v.contains("microsoft") && !v.contains("wsl") { - return false; - } - } else { - return false; - } - } - - // Windows drives are mounted at /mnt/c, /mnt/d, etc. - let path_str = match path.to_str() { - Some(s) => s, - None => return false, - }; - if !path_str.starts_with("/mnt/") || path_str.len() < 6 { - return false; - } - let after_mnt = &path_str[5..]; - after_mnt.starts_with(|c: char| c.is_ascii_alphabetic()) - && (after_mnt.len() == 1 || after_mnt.as_bytes()[1] == b'/') -} - #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum PathEventKind { Removed, @@ -1197,34 +1070,19 @@ impl Fs for RealFs { use util::{ResultExt as _, paths::SanitizedPath}; let executor = self.executor.clone(); - let use_poll = requires_poll_watcher(path); - let watch_path = effective_watch_path(path); - let (tx, rx) = smol::channel::unbounded(); let pending_paths: Arc>> = Default::default(); + let watcher = Arc::new(fs_watcher::FsWatcher::new(tx, pending_paths.clone())); - let mode = if use_poll { - log::info!( - "Using poll watcher ({}ms interval) for {}", - fs_watcher::poll_interval().as_millis(), - path.display() - ); - telemetry::event!("fs_watcher_poll", path = path.display().to_string()); - fs_watcher::WatcherMode::Poll - } else { - fs_watcher::WatcherMode::Native - }; - let watcher: Arc = Arc::new(fs_watcher::FsWatcher::new( - tx.clone(), - pending_paths.clone(), - mode, - )); - - if let Err(e) = watcher.add(&watch_path) { + // If the path doesn't exist yet (e.g. settings.json), watch the parent dir to learn when it's created. + if let Err(e) = watcher.add(path) + && let Some(parent) = path.parent() + && let Err(parent_e) = watcher.add(parent) + { log::warn!( - "Failed to watch {} using {}:\n{e}", + "Failed to watch {} and its parent directory {}:\n{e}\n{parent_e}", path.display(), - watch_path.display() + parent.display() ); } diff --git a/crates/fs/src/fs_watcher.rs b/crates/fs/src/fs_watcher.rs index 909424558b..02a6b08781 100644 --- a/crates/fs/src/fs_watcher.rs +++ b/crates/fs/src/fs_watcher.rs @@ -4,38 +4,27 @@ use std::{ collections::{BTreeMap, HashMap}, ops::DerefMut, path::Path, - sync::{Arc, LazyLock, OnceLock}, - time::Duration, + sync::{Arc, OnceLock}, }; use util::{ResultExt, paths::SanitizedPath}; use crate::{PathEvent, PathEventKind, Watcher}; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub enum WatcherMode { - #[default] - Native, - Poll, -} - pub struct FsWatcher { tx: smol::channel::Sender<()>, pending_path_events: Arc>>, registrations: Mutex, WatcherRegistrationId>>, - mode: WatcherMode, } impl FsWatcher { pub fn new( tx: smol::channel::Sender<()>, pending_path_events: Arc>>, - mode: WatcherMode, ) -> Self { Self { tx, pending_path_events, registrations: Default::default(), - mode, } } } @@ -48,10 +37,11 @@ impl Drop for FsWatcher { std::mem::swap(old.deref_mut(), &mut registrations); } - let global_watcher = global_watcher(); - for (_, registration) in registrations { - global_watcher.remove(registration); - } + let _ = global(|g| { + for (_, registration) in registrations { + g.remove(registration); + } + }); } } @@ -59,10 +49,13 @@ impl Watcher for FsWatcher { fn add(&self, path: &std::path::Path) -> anyhow::Result<()> { log::trace!("watcher add: {path:?}"); let tx = self.tx.clone(); - let pending_path_events = self.pending_path_events.clone(); + let pending_paths = self.pending_path_events.clone(); - if (self.mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos"))) - && let Some((watched_path, _)) = self + #[cfg(any(target_os = "windows", target_os = "macos"))] + { + // Return early if an ancestor of this path was already being watched. + // saves a huge amount of memory + if let Some((watched_path, _)) = self .registrations .lock() .range::(( @@ -70,36 +63,86 @@ impl Watcher for FsWatcher { std::ops::Bound::Included(path), )) .next_back() - && path.starts_with(watched_path.as_ref()) - { - log::trace!( - "path to watch is covered by existing registration: {path:?}, {watched_path:?}" - ); - return Ok(()); + && path.starts_with(watched_path.as_ref()) + { + log::trace!( + "path to watch is covered by existing registration: {path:?}, {watched_path:?}" + ); + return Ok(()); + } } - - if self.registrations.lock().contains_key(path) { - log::trace!("path to watch is already watched: {path:?}"); - return Ok(()); + #[cfg(target_os = "linux")] + { + if self.registrations.lock().contains_key(path) { + log::trace!("path to watch is already watched: {path:?}"); + return Ok(()); + } } let root_path = SanitizedPath::new_arc(path); let path: Arc = path.into(); + #[cfg(any(target_os = "windows", target_os = "macos"))] + let mode = notify::RecursiveMode::Recursive; + #[cfg(target_os = "linux")] + let mode = notify::RecursiveMode::NonRecursive; + let registration_path = path.clone(); - let registration_id = global_watcher().add( - path.clone(), - self.mode, - move |result: Result<¬ify::Event, ¬ify::Error>| match result { - Ok(event) => { + let registration_id = global({ + let watch_path = path.clone(); + let callback_path = path; + |g| { + g.add(watch_path, mode, move |event: ¬ify::Event| { log::trace!("watcher received event: {event:?}"); - push_notify_event(&tx, &pending_path_events, &root_path, path.as_ref(), event); - } - Err(error) => { - push_notify_error(&tx, &pending_path_events, path.as_ref(), error); - } - }, - )?; + let kind = match event.kind { + EventKind::Create(_) => Some(PathEventKind::Created), + EventKind::Modify(_) => Some(PathEventKind::Changed), + EventKind::Remove(_) => Some(PathEventKind::Removed), + _ => None, + }; + let mut path_events = event + .paths + .iter() + .filter_map(|event_path| { + let event_path = SanitizedPath::new(event_path); + event_path.starts_with(&root_path).then(|| PathEvent { + path: event_path.as_path().to_path_buf(), + kind, + }) + }) + .collect::>(); + + let is_rescan_event = event.need_rescan(); + if is_rescan_event { + log::warn!( + "filesystem watcher lost sync for {callback_path:?}; scheduling rescan" + ); + // we only keep the first event per path below, this ensures it will be the rescan event + // we'll remove any existing pending events for the same reason once we have the lock below + path_events.retain(|p| &p.path != callback_path.as_ref()); + path_events.push(PathEvent { + path: callback_path.to_path_buf(), + kind: Some(PathEventKind::Rescan), + }); + } + + if !path_events.is_empty() { + path_events.sort(); + let mut pending_paths = pending_paths.lock(); + if pending_paths.is_empty() { + tx.try_send(()).ok(); + } + coalesce_pending_rescans(&mut pending_paths, &mut path_events); + util::extend_sorted( + &mut *pending_paths, + path_events, + usize::MAX, + |a, b| a.path.cmp(&b.path), + ); + } + }) + } + })??; self.registrations .lock() @@ -114,85 +157,10 @@ impl Watcher for FsWatcher { return Ok(()); }; - global_watcher().remove(registration); - Ok(()) + global(|w| w.remove(registration)) } } -fn enqueue_path_events( - tx: &smol::channel::Sender<()>, - pending_path_events: &Arc>>, - mut path_events: Vec, -) { - if path_events.is_empty() { - return; - } - - path_events.sort(); - let mut pending_paths = pending_path_events.lock(); - if pending_paths.is_empty() { - tx.try_send(()).ok(); - } - coalesce_pending_rescans(&mut pending_paths, &mut path_events); - util::extend_sorted(&mut *pending_paths, path_events, usize::MAX, |a, b| { - a.path.cmp(&b.path) - }); -} - -fn push_notify_event( - tx: &smol::channel::Sender<()>, - pending_path_events: &Arc>>, - root_path: &SanitizedPath, - watched_root: &Path, - event: ¬ify::Event, -) { - let kind = match event.kind { - EventKind::Create(_) => Some(PathEventKind::Created), - EventKind::Modify(_) => Some(PathEventKind::Changed), - EventKind::Remove(_) => Some(PathEventKind::Removed), - _ => None, - }; - let mut path_events = event - .paths - .iter() - .filter_map(|event_path| { - let event_path = SanitizedPath::new(event_path); - event_path.starts_with(root_path).then(|| PathEvent { - path: event_path.as_path().to_path_buf(), - kind, - }) - }) - .collect::>(); - - if event.need_rescan() { - log::warn!("filesystem watcher lost sync for {watched_root:?}; scheduling rescan"); - path_events.retain(|path_event| path_event.path != watched_root); - path_events.push(PathEvent { - path: watched_root.to_path_buf(), - kind: Some(PathEventKind::Rescan), - }); - } - - enqueue_path_events(tx, pending_path_events, path_events); -} - -fn push_notify_error( - tx: &smol::channel::Sender<()>, - pending_path_events: &Arc>>, - watched_root: &Path, - error: ¬ify::Error, -) { - log::warn!("watcher error for {watched_root:?}: {error}"); - enqueue_path_events( - tx, - pending_path_events, - vec![PathEvent { - path: watched_root.to_path_buf(), - kind: Some(PathEventKind::Rescan), - }], - ); -} - fn coalesce_pending_rescans(pending_paths: &mut Vec, path_events: &mut Vec) { if !path_events .iter() @@ -247,34 +215,29 @@ fn is_covered_rescan(kind: Option, path: &Path, ancestor: &Path) pub struct WatcherRegistrationId(u32); struct WatcherRegistrationState { - callback: Arc Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync>, + callback: Arc, path: Arc, - mode: WatcherMode, } struct WatcherState { watchers: HashMap, - native_path_registrations: HashMap, u32>, - poll_path_registrations: HashMap, u32>, + path_registrations: HashMap, u32>, last_registration: WatcherRegistrationId, } -impl WatcherState { - fn path_registrations(&mut self, mode: WatcherMode) -> &mut HashMap, u32> { - match mode { - WatcherMode::Native => &mut self.native_path_registrations, - WatcherMode::Poll => &mut self.poll_path_registrations, - } - } -} - pub struct GlobalWatcher { state: Mutex, - // DANGER: never keep state lock while holding watcher lock - // two mutexes because calling watcher.add triggers watcher.event, which needs watchers. - native_watcher: Mutex>, - poll_watcher: Mutex>, + // DANGER: never keep the state lock while holding the watcher lock + // two mutexes because calling watcher.add triggers an watcher.event, which needs watchers. + #[cfg(target_os = "linux")] + watcher: Mutex, + #[cfg(target_os = "freebsd")] + watcher: Mutex, + #[cfg(target_os = "windows")] + watcher: Mutex, + #[cfg(target_os = "macos")] + watcher: Mutex, } impl GlobalWatcher { @@ -282,17 +245,27 @@ impl GlobalWatcher { fn add( &self, path: Arc, - mode: WatcherMode, - cb: impl for<'a> Fn(Result<&'a notify::Event, &'a notify::Error>) + Send + Sync + 'static, + mode: notify::RecursiveMode, + cb: impl Fn(¬ify::Event) + Send + Sync + 'static, ) -> anyhow::Result { - let mut state = self.state.lock(); - let registrations_for_mode = state.path_registrations(mode); - let path_already_covered = - path_already_covered(path.as_ref(), registrations_for_mode, mode); + use notify::Watcher; - if !path_already_covered && !registrations_for_mode.contains_key(&path) { + let mut state = self.state.lock(); + + // Check if this path is already covered by an existing watched ancestor path. + // On macOS and Windows, watching is recursive, so we don't need to watch + // child paths if an ancestor is already being watched. + #[cfg(any(target_os = "windows", target_os = "macos"))] + let path_already_covered = state.path_registrations.keys().any(|existing| { + path.starts_with(existing.as_ref()) && path.as_ref() != existing.as_ref() + }); + + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + let path_already_covered = false; + + if !path_already_covered && !state.path_registrations.contains_key(&path) { drop(state); - self.watch(&path, mode)?; + self.watcher.lock().watch(&path, mode)?; state = self.state.lock(); } @@ -302,191 +275,39 @@ impl GlobalWatcher { let registration_state = WatcherRegistrationState { callback: Arc::new(cb), path: path.clone(), - mode, }; state.watchers.insert(id, registration_state); - *state.path_registrations(mode).entry(path).or_insert(0) += 1; + *state.path_registrations.entry(path).or_insert(0) += 1; Ok(id) } pub fn remove(&self, id: WatcherRegistrationId) { + use notify::Watcher; let mut state = self.state.lock(); let Some(registration_state) = state.watchers.remove(&id) else { return; }; - let path_registrations = state.path_registrations(registration_state.mode); - let Some(count) = path_registrations.get_mut(®istration_state.path) else { + let Some(count) = state.path_registrations.get_mut(®istration_state.path) else { return; }; *count -= 1; if *count == 0 { - path_registrations.remove(®istration_state.path); - let path_still_covered = path_already_covered( - registration_state.path.as_ref(), - path_registrations, - registration_state.mode, - ); + state.path_registrations.remove(®istration_state.path); - if !path_still_covered { - drop(state); - self.unwatch(®istration_state.path, registration_state.mode) - .log_err(); - } - } - } - - fn watch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> { - use notify::Watcher; - - match mode { - WatcherMode::Native => { - self.ensure_native_watcher()?; - self.native_watcher - .lock() - .as_mut() - .expect("native watcher initialized") - .watch( - path, - if cfg!(any(target_os = "windows", target_os = "macos")) { - notify::RecursiveMode::Recursive - } else { - notify::RecursiveMode::NonRecursive - }, - )?; - } - WatcherMode::Poll => { - self.ensure_poll_watcher()?; - self.poll_watcher - .lock() - .as_mut() - .expect("poll watcher initialized") - .watch(path, notify::RecursiveMode::Recursive)?; - } - } - - Ok(()) - } - - fn unwatch(&self, path: &Path, mode: WatcherMode) -> anyhow::Result<()> { - use notify::Watcher; - - match mode { - WatcherMode::Native => { - if let Some(watcher) = self.native_watcher.lock().as_mut() { - watcher.unwatch(path)?; - } - } - WatcherMode::Poll => { - if let Some(watcher) = self.poll_watcher.lock().as_mut() { - watcher.unwatch(path)?; - } - } - } - - Ok(()) - } - - fn ensure_native_watcher(&self) -> anyhow::Result<()> { - if self.native_watcher.lock().is_some() { - return Ok(()); - } - - let watcher = notify::recommended_watcher(handle_native_event)?; - *self.native_watcher.lock() = Some(watcher); - Ok(()) - } - - fn ensure_poll_watcher(&self) -> anyhow::Result<()> { - if self.poll_watcher.lock().is_some() { - return Ok(()); - } - - let config = notify::Config::default().with_poll_interval(*POLL_INTERVAL); - let watcher = notify::PollWatcher::new(handle_poll_event, config)?; - *self.poll_watcher.lock() = Some(watcher); - Ok(()) - } -} - -fn path_already_covered( - path: &Path, - path_registrations: &HashMap, u32>, - mode: WatcherMode, -) -> bool { - (mode == WatcherMode::Poll || cfg!(any(target_os = "windows", target_os = "macos"))) - && path_registrations - .keys() - .any(|existing| path.starts_with(existing.as_ref()) && path != existing.as_ref()) -} - -static POLL_INTERVAL: LazyLock = LazyLock::new(|| { - let poll_ms: u64 = std::env::var("ZED_FILE_WATCHER_POLL_MS") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(2000) - .clamp(500, 30000); - Duration::from_millis(poll_ms) -}); - -pub fn poll_interval() -> Duration { - *POLL_INTERVAL -} - -static FS_WATCHER_INSTANCE: OnceLock = OnceLock::new(); - -fn global_watcher() -> &'static GlobalWatcher { - FS_WATCHER_INSTANCE.get_or_init(|| GlobalWatcher { - state: Mutex::new(WatcherState { - watchers: Default::default(), - native_path_registrations: Default::default(), - poll_path_registrations: Default::default(), - last_registration: Default::default(), - }), - native_watcher: Mutex::new(None), - poll_watcher: Mutex::new(None), - }) -} - -fn handle_native_event(event: Result) { - handle_event(WatcherMode::Native, event); -} - -fn handle_poll_event(event: Result) { - handle_event(WatcherMode::Poll, event); -} - -fn handle_event(mode: WatcherMode, event: Result) { - log::trace!("global handle event for {mode:?}: {event:?}"); - - let callbacks = { - let state = global_watcher().state.lock(); - state - .watchers - .values() - .filter(|registration| registration.mode == mode) - .map(|registration| registration.callback.clone()) - .collect::>() - }; - - match event { - Ok(event) => { - if matches!(event.kind, EventKind::Access(_)) { - return; - } - for callback in callbacks { - callback(Ok(&event)); - } - } - Err(error) => { - for callback in callbacks { - callback(Err(&error)); - } + drop(state); + self.watcher + .lock() + .unwatch(®istration_state.path) + .log_err(); } } } +static FS_WATCHER_INSTANCE: OnceLock> = + OnceLock::new(); + #[cfg(test)] mod tests { use super::*; @@ -578,8 +399,45 @@ mod tests { } } +fn handle_event(event: Result) { + log::trace!("global handle event: {event:?}"); + // Filter out access events, which could lead to a weird bug on Linux after upgrading notify + // https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832 + let Some(event) = event + .log_err() + .filter(|event| !matches!(event.kind, EventKind::Access(_))) + else { + return; + }; + global::<()>(move |watcher| { + let callbacks = { + let state = watcher.state.lock(); + state + .watchers + .values() + .map(|r| r.callback.clone()) + .collect::>() + }; + for callback in callbacks { + callback(&event); + } + }) + .log_err(); +} + pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { - let global_watcher = global_watcher(); - global_watcher.ensure_native_watcher()?; - Ok(f(global_watcher)) + let result = FS_WATCHER_INSTANCE.get_or_init(|| { + notify::recommended_watcher(handle_event).map(|file_watcher| GlobalWatcher { + state: Mutex::new(WatcherState { + watchers: Default::default(), + path_registrations: Default::default(), + last_registration: Default::default(), + }), + watcher: Mutex::new(file_watcher), + }) + }); + match result { + Ok(g) => Ok(f(g)), + Err(e) => Err(anyhow::anyhow!("{e}")), + } } diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 1e2d7f79dd..5a9350f8ae 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -83,7 +83,6 @@ pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } rand.workspace = true settings = { workspace = true, features = ["test-support"] } -task.workspace = true unindent.workspace = true workspace = { workspace = true, features = ["test-support"] } zlog.workspace = true diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index b119d243ad..6982923161 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -627,6 +627,9 @@ impl PickerDelegate for BranchListDelegate { let focus_handle = self.focus_handle.clone(); let editor = editor.as_any().downcast_ref::>().unwrap(); + let show_inline_filter = + self.editor_position() == PickerEditorPosition::End || !self.show_footer; + v_flex() .when( self.editor_position() == PickerEditorPosition::End, @@ -639,34 +642,32 @@ impl PickerDelegate for BranchListDelegate { .h_9() .px_2p5() .child(editor.clone()) - .when( - self.editor_position() == PickerEditorPosition::End, - |this| { - let tooltip_label = match self.branch_filter { - BranchFilter::All => "Filter Remote Branches", - BranchFilter::Remote => "Show All Branches", - }; + .when(show_inline_filter, |this| { + let tooltip_label = match self.branch_filter { + BranchFilter::All => "Filter Remote Branches", + BranchFilter::Remote => "Show All Branches", + }; - this.gap_1().justify_between().child({ - IconButton::new("filter-remotes", IconName::Filter) - .toggle_state(self.branch_filter == BranchFilter::Remote) - .tooltip(move |_, cx| { - Tooltip::for_action_in( - tooltip_label, - &branch_picker::FilterRemotes, - &focus_handle, - cx, - ) - }) - .on_click(|_click, window, cx| { - window.dispatch_action( - branch_picker::FilterRemotes.boxed_clone(), - cx, - ); - }) - }) - }, - ), + this.gap_1().justify_between().child({ + IconButton::new("filter-remotes", IconName::Filter) + .toggle_state(self.branch_filter == BranchFilter::Remote) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in( + tooltip_label, + &branch_picker::FilterRemotes, + &focus_handle, + cx, + ) + }) + .on_click(|_click, window, cx| { + window.dispatch_action( + branch_picker::FilterRemotes.boxed_clone(), + cx, + ); + }) + }) + }), ) .when( self.editor_position() == PickerEditorPosition::Start, diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 921bcd1960..ad6d960a30 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,6 +1,5 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style}; -use crate::git_panel_settings::GitPanelSettings; use git::repository::CommitOptions; use git::{Amend, Commit, GenerateCommitMessage, Signoff}; use panel::panel_button; @@ -540,18 +539,6 @@ impl Render for CommitModal { let border_radius = properties.modal_border_radius; let editor_focus_handle = self.commit_editor.focus_handle(cx); - let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length; - let title_exceeds_limit = if max_title_length > 0 { - self.commit_editor - .read(cx) - .text(cx) - .lines() - .next() - .is_some_and(|title| title.len() > max_title_length) - } else { - false - }; - v_flex() .id("commit-modal") .key_context("GitCommit") @@ -580,9 +567,6 @@ impl Render for CommitModal { this.toggle_branch_selector(window, cx); }), ) - .w(width) - .h_112() - .p(container_padding) .elevation_3(cx) .overflow_hidden() .flex_none() @@ -591,50 +575,30 @@ impl Render for CommitModal { .rounded(px(border_radius)) .border_1() .border_color(cx.theme().colors().border) + .w(width) + .p(container_padding) .child( v_flex() .id("editor-container") - .cursor_text() + .justify_between() .p_2() .size_full() .gap_2() - .justify_between() .rounded(properties.editor_border_radius()) .overflow_hidden() + .cursor_text() .bg(cx.theme().colors().editor_background) .border_1() - .border_color(if title_exceeds_limit { - cx.theme().status().warning_border - } else { - cx.theme().colors().border_variant - }) + .border_color(cx.theme().colors().border_variant) .on_click(cx.listener(move |_, _: &ClickEvent, window, cx| { window.focus(&editor_focus_handle, cx); })) - .child(self.render_commit_editor(window, cx)) - .when(title_exceeds_limit, |this| { - this.child( - h_flex() - .absolute() - .bottom_12() - .w_full() - .py_1() - .px_2() - .gap_1() - .justify_center() - .child( - Icon::new(IconName::Warning) - .size(IconSize::XSmall) - .color(Color::Warning), - ) - .child( - Label::new(format!( - "Commit message title exceeds {max_title_length}-character limit." - )) - .size(LabelSize::Small), - ), - ) - }) + .child( + div() + .flex_1() + .size_full() + .child(self.render_commit_editor(window, cx)), + ) .child(self.render_footer(window, cx)), ) } diff --git a/crates/git_ui/src/file_diff_view.rs b/crates/git_ui/src/file_diff_view.rs index e313700dd8..6fe3d9484b 100644 --- a/crates/git_ui/src/file_diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -2,7 +2,7 @@ use anyhow::Result; use buffer_diff::BufferDiff; -use editor::{Editor, EditorEvent, EditorSettings, MultiBuffer, SplittableEditor}; +use editor::{Editor, EditorEvent, MultiBuffer}; use futures::{FutureExt, select_biased}; use gpui::{ AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle, @@ -10,7 +10,6 @@ use gpui::{ }; use language::{Buffer, HighlightedText, LanguageRegistry}; use project::Project; -use settings::Settings; use std::{ any::{Any, TypeId}, path::PathBuf, @@ -27,7 +26,7 @@ use workspace::{ }; pub struct FileDiffView { - editor: Entity, + editor: Entity, old_buffer: Entity, new_buffer: Entity, buffer_changes_tx: watch::Sender<()>, @@ -58,14 +57,12 @@ impl FileDiffView { let buffer_diff = build_buffer_diff(&old_buffer, &new_buffer, languages, cx).await?; workspace.update_in(cx, |workspace, window, cx| { - let workspace_entity = cx.entity(); let diff_view = cx.new(|cx| { FileDiffView::new( old_buffer, new_buffer, buffer_diff, project.clone(), - workspace_entity, window, cx, ) @@ -86,7 +83,6 @@ impl FileDiffView { new_buffer: Entity, diff: Entity, project: Entity, - workspace: Entity, window: &mut Window, cx: &mut Context, ) -> Self { @@ -96,19 +92,16 @@ impl FileDiffView { multibuffer }); let editor = cx.new(|cx| { - let splittable = SplittableEditor::new( - EditorSettings::get_global(cx).diff_view_style, - multibuffer.clone(), - project.clone(), - workspace, - window, + let mut editor = + Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); + editor.start_temporary_diff_override(); + editor.disable_diagnostics(cx); + editor.set_expand_all_diff_hunks(cx); + editor.set_render_diff_hunk_controls( + Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), cx, ); - splittable.rhs_editor().update(cx, |editor, _| { - editor.start_temporary_diff_override(); - }); - splittable.disable_diff_hunk_controls(cx); - splittable + editor }); let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(()); @@ -275,19 +268,22 @@ impl Item for FileDiffView { } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.editor.deactivated(window, cx); + self.editor + .update(cx, |editor, cx| editor.deactivated(window, cx)); } fn act_as_type<'a>( &'a self, type_id: TypeId, self_handle: &'a Entity, - cx: &'a App, + _: &'a App, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.clone().into()) + } else if type_id == TypeId::of::() { + Some(self.editor.clone().into()) } else { - self.editor.act_as_type(type_id, cx) + None } } @@ -309,10 +305,8 @@ impl Item for FileDiffView { _: &mut Window, cx: &mut Context, ) { - self.editor.update(cx, |editor, cx| { - editor.rhs_editor().update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }) + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); }); } @@ -322,11 +316,8 @@ impl Item for FileDiffView { window: &mut Window, cx: &mut Context, ) -> bool { - self.editor.update(cx, |editor, cx| { - editor - .rhs_editor() - .update(cx, |editor, cx| editor.navigate(data, window, cx)) - }) + self.editor + .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { @@ -344,14 +335,13 @@ impl Item for FileDiffView { cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { - editor.rhs_editor().update(cx, |editor, cx| { - editor.added_to_workspace(workspace, window, cx) - }) + editor.added_to_workspace(workspace, window, cx) }); } fn can_save(&self, cx: &App) -> bool { - self.editor.read(cx).rhs_editor().read(cx).can_save(cx) + // The editor handles the new buffer, so delegate to it + self.editor.read(cx).can_save(cx) } fn save( @@ -361,7 +351,9 @@ impl Item for FileDiffView { window: &mut Window, cx: &mut Context, ) -> Task> { - self.editor.save(options, project, window, cx) + // Delegate saving to the editor, which manages the new buffer + self.editor + .update(cx, |editor, cx| editor.save(options, project, window, cx)) } } @@ -375,10 +367,9 @@ impl Render for FileDiffView { mod tests { use super::*; use editor::test::editor_test_context::assert_state_with_diff; - use gpui::BorrowAppContext; use gpui::TestAppContext; use project::{FakeFs, Fs, Project}; - use settings::{DiffViewStyle, SettingsStore}; + use settings::SettingsStore; use std::path::PathBuf; use unindent::unindent; use util::path; @@ -388,11 +379,6 @@ mod tests { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.editor.diff_view_style = Some(DiffViewStyle::Unified); - }); - }); theme_settings::init(theme::LoadThemes::JustBase, cx); }); } @@ -432,9 +418,7 @@ mod tests { // Verify initial diff assert_state_with_diff( - &diff_view.read_with(cx, |diff_view, cx| { - diff_view.editor.read(cx).rhs_editor().clone() - }), + &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), cx, &unindent( " @@ -469,9 +453,7 @@ mod tests { // The diff now reflects the changes to the new file cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); assert_state_with_diff( - &diff_view.read_with(cx, |diff_view, cx| { - diff_view.editor.read(cx).rhs_editor().clone() - }), + &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), cx, &unindent( " @@ -506,9 +488,7 @@ mod tests { // The diff now reflects the changes to the new file cx.executor().advance_clock(RECALCULATE_DIFF_DEBOUNCE); assert_state_with_diff( - &diff_view.read_with(cx, |diff_view, cx| { - diff_view.editor.read(cx).rhs_editor().clone() - }), + &diff_view.read_with(cx, |diff_view, _| diff_view.editor.clone()), cx, &unindent( " @@ -572,10 +552,8 @@ mod tests { .unwrap(); diff_view.update_in(cx, |diff_view, window, cx| { - diff_view.editor.update(cx, |splittable, cx| { - splittable.rhs_editor().update(cx, |editor, cx| { - editor.insert("modified ", window, cx); - }); + diff_view.editor.update(cx, |editor, cx| { + editor.insert("modified ", window, cx); }); }); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 8257b3cf4f..6f5463d65a 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -4343,18 +4343,6 @@ impl GitPanel { editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32 }); - let max_title_length = GitPanelSettings::get_global(cx).commit_title_max_length; - let title_exceeds_limit = if max_title_length > 0 { - self.commit_editor - .read(cx) - .text(cx) - .lines() - .next() - .is_some_and(|title| title.len() > max_title_length) - } else { - false - }; - let footer = v_flex() .child(PanelRepoFooter::new( display_name, @@ -4362,41 +4350,15 @@ impl GitPanel { head_commit, Some(git_panel), )) - .when(title_exceeds_limit, |this| { - this.child( - h_flex() - .px_2() - .py_1() - .gap_1() - .border_t_1() - .border_color(cx.theme().status().warning_border) - .bg(cx.theme().status().warning_background.opacity(0.5)) - .child( - Icon::new(IconName::Warning) - .size(IconSize::XSmall) - .color(Color::Warning), - ) - .child( - Label::new(format!( - "Commit message title exceeds {max_title_length}-character limit." - )) - .size(LabelSize::Small), - ), - ) - }) .child( panel_editor_container(window, cx) .id("commit-editor-container") - .cursor_text() .relative() .w_full() .h(max_height + footer_size) .border_t_1() - .border_color(if title_exceeds_limit { - cx.theme().status().warning_border - } else { - cx.theme().colors().border - }) + .border_color(cx.theme().colors().border) + .cursor_text() .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { window.focus(&this.commit_editor.focus_handle(cx), cx); })) @@ -4977,46 +4939,57 @@ impl GitPanel { let toggle_state = self.header_state(header.header); let section = header.header; let weak = cx.weak_entity(); + let show_checkbox_persistently = !matches!(&toggle_state, ToggleState::Unselected); h_flex() .id(id) - .cursor_pointer() - .group(group_name) + .group(group_name.clone()) .h(self.list_item_height()) .w_full() + .items_center() .pl_3() .pr_1() - .gap_2() - .justify_between() - .hover(|s| s.bg(cx.theme().colors().ghost_element_hover)) + .gap_1p5() .border_1() .border_r_2() .child( - Label::new(header.title()) - .color(Color::Muted) - .size(LabelSize::Small), + h_flex().flex_1().child( + Label::new(header.title()) + .color(Color::Muted) + .size(LabelSize::Small) + .line_height_style(LineHeightStyle::UiLabel) + .single_line(), + ), ) .child( - Checkbox::new(checkbox_id, toggle_state) - .disabled(!has_write_access) - .fill() - .elevation(ElevationIndex::Surface), - ) - .on_click(move |_, window, cx| { - if !has_write_access { - return; - } + div() + .flex_none() + .cursor_pointer() + .child( + Checkbox::new(checkbox_id, toggle_state) + .disabled(!has_write_access) + .fill() + .elevation(ElevationIndex::Surface) + .on_click_ext(move |_, _, window, cx| { + if !has_write_access { + return; + } - weak.update(cx, |this, cx| { - this.toggle_staged_for_entry( - &GitListEntry::Header(GitHeaderEntry { header: section }), - window, - cx, - ); - cx.stop_propagation(); - }) - .ok(); - }) + weak.update(cx, |this, cx| { + this.toggle_staged_for_entry( + &GitListEntry::Header(GitHeaderEntry { header: section }), + window, + cx, + ); + cx.stop_propagation(); + }) + .ok(); + }), + ) + .when(!show_checkbox_persistently, |this| { + this.visible_on_hover(group_name) + }), + ) .into_any_element() } @@ -6150,6 +6123,10 @@ impl RenderOnce for PanelRepoFooter { util::truncate_and_trailoff(branch_name.trim_ascii(), branch_display_len) }; + let repo_selector_trigger = Button::new("repo-selector", truncated_repo_name) + .size(ButtonSize::None) + .label_size(LabelSize::Small); + let repo_selector = PopoverMenu::new("repository-switcher") .menu({ let project = project; @@ -6159,9 +6136,8 @@ impl RenderOnce for PanelRepoFooter { } }) .trigger_with_tooltip( - Button::new("repo-selector", truncated_repo_name) - .size(ButtonSize::None) - .label_size(LabelSize::Small) + repo_selector_trigger + .when(single_repo, |this| this.disabled(true).color(Color::Muted)) .truncate(true), move |_, cx| { if single_repo { @@ -6203,7 +6179,7 @@ impl RenderOnce for PanelRepoFooter { }); h_flex() - .h_9() + .h(px(36.)) .w_full() .px_2() .justify_between() @@ -6220,14 +6196,14 @@ impl RenderOnce for PanelRepoFooter { Color::Muted }, )) - .when(!single_repo, |this| { - this.child(repo_selector).when(show_separator, |this| { - this.child( - Label::new("/").size(LabelSize::Small).color(Color::Custom( - cx.theme().colors().text_muted.opacity(0.4), - )), - ) - }) + .child(repo_selector) + .when(show_separator, |this| { + this.child( + div() + .text_sm() + .text_color(cx.theme().colors().icon_muted.opacity(0.5)) + .child("/"), + ) }) .child(branch_selector), ) diff --git a/crates/git_ui/src/git_panel_settings.rs b/crates/git_ui/src/git_panel_settings.rs index caf4b22dc4..9ccbced249 100644 --- a/crates/git_ui/src/git_panel_settings.rs +++ b/crates/git_ui/src/git_panel_settings.rs @@ -30,7 +30,6 @@ pub struct GitPanelSettings { pub diff_stats: bool, pub show_count_badge: bool, pub starts_open: bool, - pub commit_title_max_length: usize, } #[derive(Default)] @@ -77,7 +76,6 @@ impl Settings for GitPanelSettings { diff_stats: git_panel.diff_stats.unwrap(), show_count_badge: git_panel.show_count_badge.unwrap(), starts_open: git_panel.starts_open.unwrap(), - commit_title_max_length: git_panel.commit_title_max_length.unwrap(), } } } diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 4a300a5257..9f5b65d756 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -359,9 +359,13 @@ impl ProjectDiff { ); match branch_diff.read(cx).diff_base() { DiffBase::Head => {} - DiffBase::Merge { .. } => diff_display_editor.disable_diff_hunk_controls(cx), + DiffBase::Merge { .. } => diff_display_editor.set_render_diff_hunk_controls( + Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), + cx, + ), } diff_display_editor.rhs_editor().update(cx, |editor, cx| { + editor.disable_diagnostics(cx); editor.set_show_diff_review_button(true, cx); match branch_diff.read(cx).diff_base() { diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index 739c314880..fe2add8177 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -178,9 +178,14 @@ impl TextDiffView { window, cx, ); - splittable.disable_diff_hunk_controls(cx); - splittable.rhs_editor().update(cx, |editor, _cx| { + splittable.set_render_diff_hunk_controls( + Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), + cx, + ); + splittable.rhs_editor().update(cx, |editor, cx| { editor.start_temporary_diff_override(); + editor.disable_diagnostics(cx); + editor.set_expand_all_diff_hunks(cx); }); splittable }); diff --git a/crates/git_ui/src/worktree_service.rs b/crates/git_ui/src/worktree_service.rs index ba411cb064..c568b007f7 100644 --- a/crates/git_ui/src/worktree_service.rs +++ b/crates/git_ui/src/worktree_service.rs @@ -787,232 +787,23 @@ async fn open_worktree_workspace( window_handle.update(cx, |multi_workspace, window, cx| { multi_workspace.activate(new_workspace.clone(), source_for_transfer, window, cx); - if is_creating_new_worktree { - new_workspace.update(cx, |workspace, cx| { - workspace.run_create_worktree_tasks(window, cx); + new_workspace.update(cx, |workspace, cx| { + workspace.run_create_worktree_tasks(window, cx); + }); + })?; - if let Some(dock_position) = focused_dock { + if is_creating_new_worktree { + if let Some(dock_position) = focused_dock { + window_handle.update(cx, |_multi_workspace, window, cx| { + new_workspace.update(cx, |workspace, cx| { let dock = workspace.dock_at_position(dock_position); if let Some(panel) = dock.read(cx).active_panel() { panel.panel_focus_handle(cx).focus(window, cx); } - } - }); + }); + })?; } - })?; + } anyhow::Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use fs::Fs; - use gpui::{App, Task, TestAppContext}; - use language::language_settings::AllLanguageSettings; - use project::project_settings::ProjectSettings; - use project::task_store::{TaskSettingsLocation, TaskStore}; - use project::{FakeFs, WorktreeSettings}; - use serde_json::json; - use settings::{SettingsLocation, SettingsStore}; - use std::path::{Path, PathBuf}; - use std::process::ExitStatus; - use std::sync::Mutex; - use task::SpawnInTerminal; - use theme::LoadThemes; - use util::path; - use util::rel_path::rel_path; - use workspace::{TerminalProvider, WorkspaceSettings}; - - struct CountingTerminalProvider { - spawned_task_labels: Arc>>, - } - - impl TerminalProvider for CountingTerminalProvider { - fn spawn( - &self, - task: SpawnInTerminal, - _window: &mut ui::Window, - _cx: &mut App, - ) -> Task>> { - self.spawned_task_labels - .lock() - .expect("terminal spawn mutex should not be poisoned") - .push(task.label); - Task::ready(Some(Ok(ExitStatus::default()))) - } - } - - fn init_test(cx: &mut TestAppContext) { - zlog::init_test(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme_settings::init(LoadThemes::JustBase, cx); - AllLanguageSettings::register(cx); - editor::init(cx); - ProjectSettings::register(cx); - WorktreeSettings::register(cx); - WorkspaceSettings::register(cx); - TaskStore::init(None); - }); - } - - fn install_counting_provider_and_worktree_hook( - workspace: &Entity, - spawned_task_labels: &Arc>>, - main_project_root: &Path, - hook_tasks_json: &str, - cx: &mut App, - ) { - workspace.update(cx, |workspace, cx| { - workspace.set_terminal_provider(CountingTerminalProvider { - spawned_task_labels: spawned_task_labels.clone(), - }); - - let project = workspace.project().clone(); - let Some(worktree) = project.read(cx).worktrees(cx).next() else { - return; - }; - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let worktree_root = worktree.abs_path().to_path_buf(); - if worktree_root == main_project_root { - return; - } - - let Some(task_inventory) = project - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - else { - return; - }; - task_inventory.update(cx, |inventory, _| { - inventory - .update_file_based_tasks( - TaskSettingsLocation::Worktree(SettingsLocation { - worktree_id, - path: rel_path(".zed"), - }), - Some(hook_tasks_json), - ) - .expect("should inject create_worktree hook tasks for linked worktree"); - }); - }); - } - - #[gpui::test] - async fn test_create_worktree_hook_does_not_run_when_switching_back_to_main_worktree( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let hook_tasks_json = r#"[{"label":"setup worktree","command":"echo","hide":"never","hooks":["create_worktree"]}]"#; - let fs = FakeFs::new(cx.background_executor.clone()); - cx.update(|cx| ::set_global(fs.clone(), cx)); - fs.insert_tree( - "/root", - json!({ - "project": { - ".git": {}, - ".zed": { - "tasks.json": hook_tasks_json, - }, - "src": { - "main.rs": "fn main() {}", - }, - }, - }), - ) - .await; - - let main_project_root = PathBuf::from(path!("/root/project")); - let project = Project::test(fs.clone(), [main_project_root.as_path()], cx).await; - project - .update(cx, |project, cx| project.git_scans_complete(cx)) - .await; - - let (multi_workspace, cx) = - cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); - - let spawned_task_labels = Arc::new(Mutex::new(Vec::new())); - multi_workspace.update(cx, |multi_workspace, cx| { - multi_workspace.retain_active_workspace(cx); - let active_workspace = multi_workspace.workspace().clone(); - install_counting_provider_and_worktree_hook( - &active_workspace, - &spawned_task_labels, - &main_project_root, - hook_tasks_json, - cx, - ); - }); - - let main_workspace = - multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone()); - main_workspace.update_in(cx, |workspace, window, cx| { - handle_create_worktree( - workspace, - &zed_actions::CreateWorktree { - worktree_name: Some("feature".to_string()), - branch_target: NewWorktreeBranchTarget::CurrentBranch, - }, - window, - None, - cx, - ); - }); - cx.run_until_parked(); - - let active_workspace = - multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone()); - cx.update(|_, cx| { - install_counting_provider_and_worktree_hook( - &active_workspace, - &spawned_task_labels, - &main_project_root, - hook_tasks_json, - cx, - ); - }); - active_workspace.update_in(cx, |workspace, window, cx| { - workspace.run_create_worktree_tasks(window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - spawned_task_labels - .lock() - .expect("terminal spawn mutex should not be poisoned") - .as_slice(), - ["setup worktree"], - "create_worktree hook should run once for the created linked worktree" - ); - - active_workspace.update_in(cx, |workspace, window, cx| { - handle_switch_worktree( - workspace, - &zed_actions::SwitchWorktree { - path: main_project_root.clone(), - display_name: "project".to_string(), - }, - window, - None, - cx, - ); - }); - cx.run_until_parked(); - - assert_eq!( - spawned_task_labels - .lock() - .expect("terminal spawn mutex should not be poisoned") - .as_slice(), - ["setup worktree"], - "switching back to the main worktree should not rerun create_worktree hooks" - ); - } -} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 3642d46a33..3453364a20 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1680,7 +1680,8 @@ impl App { self.globals_by_type .get(&TypeId::of::()) .map(|any_state| any_state.downcast_ref::().unwrap()) - .unwrap_or_else(|| panic!("no state of type {} exists", type_name::())) + .with_context(|| format!("no state of type {} exists", type_name::())) + .unwrap() } /// Access the global of the given type if a value has been assigned. @@ -1698,7 +1699,8 @@ impl App { self.globals_by_type .get_mut(&global_type) .and_then(|any_state| any_state.downcast_mut::()) - .unwrap_or_else(|| panic!("no state of type {} exists", type_name::())) + .with_context(|| format!("no state of type {} exists", type_name::())) + .unwrap() } /// Access the global of the given type mutably. A default value is assigned if a global of this type has not @@ -1733,7 +1735,7 @@ impl App { *self .globals_by_type .remove(&global_type) - .unwrap_or_else(|| panic!("no global added for {}", type_name::())) + .unwrap_or_else(|| panic!("no global added for {}", std::any::type_name::())) .downcast() .unwrap() } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index df6403460d..bdda213dfd 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -738,7 +738,7 @@ pub trait InteractiveElement: Sized { fn key_context(mut self, key_context: C) -> Self where C: TryInto, - E: std::fmt::Display, + E: Debug, { if let Some(key_context) = key_context.try_into().log_err() { self.interactivity().key_context = Some(key_context); diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index ab253472ad..f66f584478 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -1,7 +1,7 @@ use crate::{App, PlatformDispatcher, PlatformScheduler}; use futures::channel::mpsc; use futures::prelude::*; -use gpui_util::{TryFutureExt, TryFutureExtBacktrace}; +use gpui_util::TryFutureExt; use scheduler::Instant; use scheduler::Scheduler; use std::{ @@ -84,7 +84,7 @@ impl Task { impl Task> where T: 'static, - E: 'static + std::fmt::Display, + E: 'static + Debug, { /// Run the task to completion in the background and log any errors that occur. #[track_caller] @@ -96,22 +96,6 @@ where } } -impl Task> -where - T: 'static, - E: 'static + std::fmt::Debug, -{ - /// Like [`Self::detach_and_log_err`], but uses `{:?}` formatting on failure so `anyhow::Error` - /// values emit their full backtrace. Prefer `detach_and_log_err` unless a backtrace is wanted. - #[track_caller] - pub fn detach_and_log_err_with_backtrace(self, cx: &App) { - let location = *core::panic::Location::caller(); - cx.foreground_executor() - .spawn(self.log_tracked_err_with_backtrace(location)) - .detach(); - } -} - impl std::future::Future for Task { type Output = T; diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 437ef48cb1..9c6ffdbe3f 100644 --- a/crates/gpui/src/text_system/line.rs +++ b/crates/gpui/src/text_system/line.rs @@ -208,16 +208,8 @@ impl ShapedLine { } // Split text - let left_text = if byte_index == self.text.len() { - self.text.clone() - } else { - SharedString::new(&self.text[..byte_index]) - }; - let right_text = if byte_index == 0 { - self.text.clone() - } else { - SharedString::new(&self.text[byte_index..]) - }; + let left_text = SharedString::new(self.text[..byte_index].to_string()); + let right_text = SharedString::new(self.text[byte_index..].to_string()); let left_width = x_offset; let right_width = self.layout.width - left_width; diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 3b8ca0f450..10f4aab0db 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -2237,13 +2237,7 @@ impl Dispatch for WaylandClientStatePtr { let paths: SmallVec<[_; 2]> = file_list .lines() .filter_map(|path| Url::parse(path).log_err()) - .filter_map(|url| match url.to_file_path() { - Ok(url) => Some(url), - Err(()) => { - log::error!("Failed turn {url:?} into a file path"); - None - } - }) + .filter_map(|url| url.to_file_path().log_err()) .collect(); let position = Point::new(x.into(), y.into()); diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 07d1596a2f..70c0392d43 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -908,13 +908,7 @@ impl X11Client { let paths: SmallVec<[_; 2]> = file_list .lines() .filter_map(|path| Url::parse(path).log_err()) - .filter_map(|url| match url.to_file_path() { - Ok(url) => Some(url), - Err(()) => { - log::error!("Failed turn {url:?} into a file path"); - None - } - }) + .filter_map(|url| url.to_file_path().log_err()) .collect(); let input = PlatformInput::FileDrop(FileDropEvent::Entered { position: state.xdnd_state.position, diff --git a/crates/gpui_util/src/lib.rs b/crates/gpui_util/src/lib.rs index eac1e2559a..ae56c2ccd0 100644 --- a/crates/gpui_util/src/lib.rs +++ b/crates/gpui_util/src/lib.rs @@ -79,12 +79,6 @@ pub trait ResultExt { type Ok; fn log_err(self) -> Option; - /// Like [`ResultExt::log_err`], but uses `{:?}` formatting so `anyhow::Error` values emit their - /// full backtrace. Reach for this only when a backtrace is genuinely wanted — most call sites - /// should stick with `log_err` / `warn_on_err`, whose output is a single chained error message. - fn log_err_with_backtrace(self) -> Option - where - E: std::fmt::Debug; /// Assert that this result should never be an error in development or tests. fn debug_assert_ok(self, reason: &str) -> Self; fn warn_on_err(self) -> Option; @@ -96,7 +90,7 @@ pub trait ResultExt { impl ResultExt for Result where - E: std::fmt::Display, + E: std::fmt::Debug, { type Ok = T; @@ -105,28 +99,10 @@ where self.log_with_level(log::Level::Error) } - #[track_caller] - fn log_err_with_backtrace(self) -> Option - where - E: std::fmt::Debug, - { - match self { - Ok(value) => Some(value), - Err(error) => { - log_error_with_caller( - *Location::caller(), - DebugAsDisplay(&error), - log::Level::Error, - ); - None - } - } - } - #[track_caller] fn debug_assert_ok(self, reason: &str) -> Self { if let Err(error) = &self { - debug_panic!("{reason} - {error:#}"); + debug_panic!("{reason} - {error:?}"); } self } @@ -157,7 +133,7 @@ where fn log_error_with_caller(caller: core::panic::Location<'_>, error: E, level: log::Level) where - E: std::fmt::Display, + E: std::fmt::Debug, { #[cfg(not(windows))] let file = caller.file(); @@ -180,7 +156,7 @@ where &log::Record::builder() .target(module_path.as_deref().unwrap_or("")) .module_path(file.as_deref()) - .args(format_args!("{:#}", error)) + .args(format_args!("{:?}", error)) .file(Some(caller.file())) .line(Some(caller.line())) .level(level) @@ -188,20 +164,10 @@ where ); } -pub fn log_err(error: &E) { +pub fn log_err(error: &E) { log_error_with_caller(*Location::caller(), error, log::Level::Error); } -// Forces `{:?}` formatting through a `Display`-bounded logging helper so `anyhow::Error` emits a -// backtrace instead of the single-line chained message produced by its `Display`/`{:#}` forms. -struct DebugAsDisplay<'a, E>(&'a E); - -impl std::fmt::Display for DebugAsDisplay<'_, E> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0) - } -} - pub trait TryFutureExt { fn log_err(self) -> LogErrorFuture where @@ -219,25 +185,10 @@ pub trait TryFutureExt { Self: Sized; } -/// `{:?}`-formatting companion to [`TryFutureExt`]; emits a backtrace for `anyhow::Error`. Prefer -/// [`TryFutureExt`] unless a backtrace is genuinely wanted. -pub trait TryFutureExtBacktrace { - fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture - where - Self: Sized; - - fn log_tracked_err_with_backtrace( - self, - location: core::panic::Location<'static>, - ) -> LogErrorWithBacktraceFuture - where - Self: Sized; -} - impl TryFutureExt for F where F: Future>, - E: std::fmt::Display, + E: std::fmt::Debug, { #[track_caller] fn log_err(self) -> LogErrorFuture @@ -272,38 +223,13 @@ where } } -impl TryFutureExtBacktrace for F -where - F: Future>, - E: std::fmt::Debug, -{ - #[track_caller] - fn log_err_with_backtrace(self) -> LogErrorWithBacktraceFuture - where - Self: Sized, - { - let location = Location::caller(); - LogErrorWithBacktraceFuture(self, log::Level::Error, *location) - } - - fn log_tracked_err_with_backtrace( - self, - location: core::panic::Location<'static>, - ) -> LogErrorWithBacktraceFuture - where - Self: Sized, - { - LogErrorWithBacktraceFuture(self, log::Level::Error, location) - } -} - #[must_use] pub struct LogErrorFuture(F, log::Level, core::panic::Location<'static>); impl Future for LogErrorFuture where F: Future>, - E: std::fmt::Display, + E: std::fmt::Debug, { type Output = Option; @@ -324,33 +250,6 @@ where } } -#[must_use] -pub struct LogErrorWithBacktraceFuture(F, log::Level, core::panic::Location<'static>); - -impl Future for LogErrorWithBacktraceFuture -where - F: Future>, - E: std::fmt::Debug, -{ - type Output = Option; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let level = self.1; - let location = self.2; - let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; - match inner.poll(cx) { - Poll::Ready(output) => Poll::Ready(match output { - Ok(output) => Some(output), - Err(error) => { - log_error_with_caller(location, DebugAsDisplay(&error), level); - None - } - }), - Poll::Pending => Poll::Pending, - } - } -} - pub struct UnwrapFuture(F); impl Future for UnwrapFuture diff --git a/crates/grammars/src/cpp/indents.scm b/crates/grammars/src/cpp/indents.scm index ebd5afb7c7..0b55631e5c 100644 --- a/crates/grammars/src/cpp/indents.scm +++ b/crates/grammars/src/cpp/indents.scm @@ -13,14 +13,6 @@ "{" "}" @end) @indent -(field_declaration_list - (access_specifier) @start - "}" @end) @indent - -(field_declaration_list - (access_specifier) - (access_specifier) @outdent) - (_ "(" ")" @end) @indent diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e15568affc..3770e4ccf1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,8 +1,8 @@ pub mod row_chunk; use crate::{ - ByteContent, DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig, - PLAIN_TEXT, RunnableCapture, RunnableTag, TextObject, TreeSitterOptions, analyze_byte_content, + DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig, PLAIN_TEXT, + RunnableCapture, RunnableTag, TextObject, TreeSitterOptions, diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup}, language_settings::{AutoIndentMode, LanguageSettings}, outline::OutlineItem, @@ -1579,21 +1579,16 @@ impl Buffer { let target_encoding = force_encoding.unwrap_or(current_encoding); - let bytes = load_bytes_task.await?; - - anyhow::ensure!( - analyze_byte_content(&bytes) != ByteContent::Binary, - "Binary files are not supported" - ); - let is_unicode = target_encoding == encoding_rs::UTF_8 || target_encoding == encoding_rs::UTF_16LE || target_encoding == encoding_rs::UTF_16BE; let (new_text, has_bom, encoding_used) = if force_encoding.is_some() && !is_unicode { + let bytes = load_bytes_task.await?; let (cow, _had_errors) = target_encoding.decode_without_bom_handling(&bytes); (cow.into_owned(), false, target_encoding) } else { + let bytes = load_bytes_task.await?; let (cow, used_enc, _had_errors) = target_encoding.decode(&bytes); let actual_has_bom = if used_enc == encoding_rs::UTF_8 { diff --git a/crates/language/src/file_content.rs b/crates/language/src/file_content.rs deleted file mode 100644 index bb543478e5..0000000000 --- a/crates/language/src/file_content.rs +++ /dev/null @@ -1,158 +0,0 @@ -pub const FILE_ANALYSIS_BYTES: usize = 1024; - -#[derive(Debug, PartialEq)] -pub enum ByteContent { - Utf16Le, - Utf16Be, - Binary, - Unknown, -} - -// Heuristic check using null byte distribution plus a generic text-likeness -// heuristic. This prefers UTF-16 when many bytes are NUL and otherwise -// distinguishes between text-like and binary-like content. -pub fn analyze_byte_content(bytes: &[u8]) -> ByteContent { - if bytes.len() < 2 { - return ByteContent::Unknown; - } - - if is_known_binary_header(bytes) { - return ByteContent::Binary; - } - - let limit = bytes.len().min(FILE_ANALYSIS_BYTES); - let mut even_null_count = 0usize; - let mut odd_null_count = 0usize; - let mut non_text_like_count = 0usize; - - for (i, &byte) in bytes[..limit].iter().enumerate() { - if byte == 0 { - if i % 2 == 0 { - even_null_count += 1; - } else { - odd_null_count += 1; - } - non_text_like_count += 1; - continue; - } - - let is_text_like = match byte { - b'\t' | b'\n' | b'\r' | 0x0C => true, - 0x20..=0x7E => true, - // Treat bytes that are likely part of UTF-8 or single-byte encodings as text-like. - 0x80..=0xBF | 0xC2..=0xF4 => true, - _ => false, - }; - - if !is_text_like { - non_text_like_count += 1; - } - } - - let total_null_count = even_null_count + odd_null_count; - - // If there are no NUL bytes at all, this is overwhelmingly likely to be text. - if total_null_count == 0 { - return ByteContent::Unknown; - } - - let has_significant_nulls = total_null_count >= limit / 16; - let nulls_skew_to_even = even_null_count > odd_null_count * 4; - let nulls_skew_to_odd = odd_null_count > even_null_count * 4; - - if has_significant_nulls { - let sample = &bytes[..limit]; - - // UTF-16BE ASCII: [0x00, char] — nulls at even positions (high byte first) - // UTF-16LE ASCII: [char, 0x00] — nulls at odd positions (low byte first) - - if nulls_skew_to_even && is_plausible_utf16_text(sample, false) { - return ByteContent::Utf16Be; - } - - if nulls_skew_to_odd && is_plausible_utf16_text(sample, true) { - return ByteContent::Utf16Le; - } - - return ByteContent::Binary; - } - - if non_text_like_count * 100 < limit * 8 { - ByteContent::Unknown - } else { - ByteContent::Binary - } -} - -fn is_known_binary_header(bytes: &[u8]) -> bool { - bytes.starts_with(b"%PDF-") // PDF - || bytes.starts_with(b"PK\x03\x04") // ZIP local header - || bytes.starts_with(b"PK\x05\x06") // ZIP end of central directory - || bytes.starts_with(b"PK\x07\x08") // ZIP spanning/splitting - || bytes.starts_with(b"\x89PNG\r\n\x1a\n") // PNG - || bytes.starts_with(b"\xFF\xD8\xFF") // JPEG - || bytes.starts_with(b"GIF87a") // GIF87a - || bytes.starts_with(b"GIF89a") // GIF89a - || bytes.starts_with(b"IWAD") // Doom IWAD archive - || bytes.starts_with(b"PWAD") // Doom PWAD archive - || bytes.starts_with(b"RIFF") // WAV, AVI, WebP - || bytes.starts_with(b"OggS") // OGG (Vorbis, Opus, FLAC) - || bytes.starts_with(b"fLaC") // FLAC - || bytes.starts_with(b"ID3") // MP3 with ID3v2 tag - || bytes.starts_with(b"\xFF\xFB") // MP3 frame sync (MPEG1 Layer3) - || bytes.starts_with(b"\xFF\xFA") // MP3 frame sync (MPEG1 Layer3) - || bytes.starts_with(b"\xFF\xF3") // MP3 frame sync (MPEG2 Layer3) - || bytes.starts_with(b"\xFF\xF2") // MP3 frame sync (MPEG2 Layer3) -} - -// Null byte skew alone is not enough to identify UTF-16 -- binary formats with -// small 16-bit values (like PCM audio) produce the same pattern. Decode the -// bytes as UTF-16 and reject if too many code units land in control character -// ranges or form unpaired surrogates, which real text almost never contains. -fn is_plausible_utf16_text(bytes: &[u8], little_endian: bool) -> bool { - let mut suspicious_count = 0usize; - let mut total = 0usize; - - let mut i = 0; - while let Some(code_unit) = read_u16(bytes, i, little_endian) { - total += 1; - - match code_unit { - 0x0009 | 0x000A | 0x000C | 0x000D => {} - // C0/C1 control characters and non-characters - 0x0000..=0x001F | 0x007F..=0x009F | 0xFFFE | 0xFFFF => suspicious_count += 1, - 0xD800..=0xDBFF => { - let next_offset = i + 2; - let has_low_surrogate = read_u16(bytes, next_offset, little_endian) - .is_some_and(|next| (0xDC00..=0xDFFF).contains(&next)); - if has_low_surrogate { - total += 1; - i += 2; - } else { - suspicious_count += 1; - } - } - // Lone low surrogate without a preceding high surrogate - 0xDC00..=0xDFFF => suspicious_count += 1, - _ => {} - } - - i += 2; - } - - if total == 0 { - return false; - } - - // Real UTF-16 text has near-zero control characters; binary data with - // small 16-bit values typically exceeds 5%. 2% provides a safe margin. - suspicious_count * 100 < total * 2 -} - -fn read_u16(bytes: &[u8], offset: usize, little_endian: bool) -> Option { - let pair = [*bytes.get(offset)?, *bytes.get(offset + 1)?]; - if little_endian { - return Some(u16::from_le_bytes(pair)); - } - Some(u16::from_be_bytes(pair)) -} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ea8a0c69a1..505fbb4d7d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -9,7 +9,6 @@ mod buffer; mod diagnostic; mod diagnostic_set; -mod file_content; mod language_registry; pub mod language_settings; @@ -92,7 +91,6 @@ pub use buffer::Operation; pub use buffer::*; pub use diagnostic::{Diagnostic, DiagnosticSourceKind}; pub use diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup}; -pub use file_content::{ByteContent, FILE_ANALYSIS_BYTES, analyze_byte_content}; pub use language_registry::{ AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry, QUERY_FILENAME_PREFIXES, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 701b363d9e..adc874b666 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -16,10 +16,10 @@ use itertools::{Either, Itertools}; use settings::{DocumentFoldingRanges, DocumentSymbols, IntoGpui, SemanticTokens}; pub use settings::{ - AutoIndentMode, CompletionSettingsContent, EditPredictionDataCollectionChoice, - EditPredictionPromptFormat, EditPredictionProvider, EditPredictionsMode, FormatOnSave, - Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LineEndingSetting, - LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, + AutoIndentMode, CompletionSettingsContent, EditPredictionPromptFormat, EditPredictionProvider, + EditPredictionsMode, FormatOnSave, Formatter, FormatterList, InlayHintKind, + LanguageSettingsContent, LineEndingSetting, LspInsertMode, RewrapBehavior, + ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom}; use shellexpand; @@ -478,11 +478,6 @@ pub struct EditPredictionSettings { pub ollama: Option, pub open_ai_compatible_api: Option, pub examples_dir: Option>, - /// Controls whether training data collection is enabled. - /// - /// `Default` means the value stored in the legacy KV store is used as a fallback, - /// preserving existing users' choices without a migration. - pub allow_data_collection: EditPredictionDataCollectionChoice, } impl EditPredictionSettings { @@ -872,7 +867,6 @@ impl settings::Settings for AllLanguageSettings { ollama: ollama_settings, open_ai_compatible_api: openai_compatible_settings, examples_dir: edit_predictions.examples_dir, - allow_data_collection: edit_predictions.allow_data_collection.unwrap_or_default(), }, defaults: default_language_settings, languages, diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index db50f5161e..ef9fbae113 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -365,9 +365,7 @@ impl LanguageModel for CopilotChatLanguageModel { if model.supports_adaptive_thinking() { if anthropic_request.thinking.is_some() { - anthropic_request.thinking = Some(anthropic::Thinking::Adaptive { - display: Some(anthropic::AdaptiveThinkingDisplay::Summarized), - }); + anthropic_request.thinking = Some(anthropic::Thinking::Adaptive); anthropic_request.output_config = effort.map(|effort| anthropic::OutputConfig { effort: Some(effort), diff --git a/crates/language_models_cloud/src/language_models_cloud.rs b/crates/language_models_cloud/src/language_models_cloud.rs index da5659bf50..adae72068c 100644 --- a/crates/language_models_cloud/src/language_models_cloud.rs +++ b/crates/language_models_cloud/src/language_models_cloud.rs @@ -408,9 +408,7 @@ impl LanguageModel for CloudLanguageModel(|store, cx| { - store.update_user_settings(cx, |s| { - s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); - }); - }); - }); - let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into()); - - cx.new(|cx| { - let mut buffer = Buffer::local("", cx).with_language(language, cx); - - buffer.edit( - [( - 0..0, - r#" - class Foo { - public: - void bar(); - private: - int x; - }; - "# - .unindent(), - )], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - r#" - class Foo { - public: - void bar(); - private: - int x; - }; - "# - .unindent(), - "members after access specifiers should be indented one level deeper than the specifier" - ); - - buffer - }); - } - - #[gpui::test] - async fn test_cpp_autoindent_access_specifier_next_line(cx: &mut TestAppContext) { - cx.update(|cx| { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |s| { - s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); - }); - }); - }); - let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into()); - - cx.new(|cx| { - let mut buffer = Buffer::local("", cx).with_language(language, cx); - - buffer.edit( - [( - 0..0, - r#" - class Foo { - public: - void bar(); - void baz(); - private: - int x; - }; - "# - .unindent(), - )], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - r#" - class Foo { - public: - void bar(); - void baz(); - private: - int x; - }; - "# - .unindent(), - "members after access specifiers should be indented one level deeper than the specifier" - ); - - buffer - }); - } - - #[gpui::test] - async fn test_cpp_autoindent_nested_class_access_specifiers(cx: &mut TestAppContext) { - cx.update(|cx| { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |s| { - s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); - }); - }); - }); - let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into()); - - cx.new(|cx| { - let mut buffer = Buffer::local("", cx).with_language(language, cx); - - buffer.edit( - [( - 0..0, - r#" - class Outer { - public: - class Inner { - public: - void inner_pub(); - private: - int inner_priv; - }; - private: - int outer_priv; - }; - "# - .unindent(), - )], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - r#" - class Outer { - public: - class Inner { - public: - void inner_pub(); - private: - int inner_priv; - }; - private: - int outer_priv; - }; - "# - .unindent(), - "nested class access specifiers should indent independently at each nesting level" - ); - - buffer - }); - } - - #[gpui::test] - async fn test_cpp_autoindent_consecutive_access_specifiers(cx: &mut TestAppContext) { - cx.update(|cx| { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |s| { - s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); - }); - }); - }); - let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into()); - - cx.new(|cx| { - let mut buffer = Buffer::local("", cx).with_language(language, cx); - - buffer.edit( - [( - 0..0, - r#" - class Foo { - public: - protected: - private: - int x; - }; - "# - .unindent(), - )], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - r#" - class Foo { - public: - protected: - private: - int x; - }; - "# - .unindent(), - "consecutive access specifiers with no members between them should all align at class level" - ); - - buffer - }); - } - - #[gpui::test] - async fn test_cpp_autoindent_indented_access_specifiers(cx: &mut TestAppContext) { - cx.update(|cx| { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |s| { - s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); - }); - }); - }); - let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into()); - - cx.new(|cx| { - let mut buffer = Buffer::local("", cx).with_language(language, cx); - - buffer.edit( - [( - 0..0, - r#" - class Foo { - int default_member; - public: - void pub_method(); - private: - int priv_member; - }; - "# - .unindent(), - )], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - r#" - class Foo { - int default_member; - public: - void pub_method(); - private: - int priv_member; - }; - "# - .unindent(), - "access specifiers should be indented one level inside class braces" - ); - - buffer - }); - } - - #[gpui::test] - async fn test_cpp_autoindent_access_specifier_with_method_bodies(cx: &mut TestAppContext) { - cx.update(|cx| { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |s| { - s.project.all_languages.defaults.tab_size = NonZeroU32::new(2); - }); - }); - }); - let language = crate::language("cpp", tree_sitter_cpp::LANGUAGE.into()); - - cx.new(|cx| { - let mut buffer = Buffer::local("", cx).with_language(language, cx); - - buffer.edit( - [( - 0..0, - r#" - class Foo { - public: - void bar() { - if (x) - y++; - } - private: - int get_x() { - return x; - } - int x; - }; - "# - .unindent(), - )], - Some(AutoindentMode::EachLine), - cx, - ); - assert_eq!( - buffer.text(), - r#" - class Foo { - public: - void bar() { - if (x) - y++; - } - private: - int get_x() { - return x; - } - int x; - }; - "# - .unindent(), - "method bodies inside access specifier sections should compose brace and specifier indent" - ); - - buffer - }); - } - #[gpui::test] async fn test_cpp_autoindent_basic(cx: &mut TestAppContext) { cx.update(|cx| { diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 27ece268d6..2b7379b0b4 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -999,6 +999,7 @@ impl SearchableItem for MarkdownPreviewView { markdown.set_active_search_highlight(Some(index), cx); markdown.request_autoscroll_to_source_index(start, cx); }); + cx.emit(SearchEvent::ActiveMatchChanged); } } diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index cff9da17ac..eba5b30961 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -838,7 +838,7 @@ impl Picker { el.with_width_from_item(Some(widest_item)) }) .flex_grow() - .py(DynamicSpacing::Base04.rems(cx)) + .py_1() .track_scroll(&scroll_handle) .into_any_element(), ElementContainer::List(state) => list( @@ -849,7 +849,7 @@ impl Picker { ) .with_sizing_behavior(sizing_behavior) .flex_grow() - .py(DynamicSpacing::Base04.rems(cx)) + .py_2() .into_any_element(), } } @@ -1123,16 +1123,13 @@ impl Render for Picker { .when(self.delegate.match_count() == 0, |el| { el.when_some(self.delegate.no_matches_text(window, cx), |el, text| { el.child( - v_flex() - .flex_grow() - .py(DynamicSpacing::Base04.rems(cx)) - .child( - ListItem::new("empty_state") - .inset(true) - .spacing(ListItemSpacing::Sparse) - .disabled(true) - .child(Label::new(text).color(Color::Muted)), - ), + v_flex().flex_grow().py_2().child( + ListItem::new("empty_state") + .inset(true) + .spacing(ListItemSpacing::Sparse) + .disabled(true) + .child(Label::new(text).color(Color::Muted)), + ), ) }) }) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index a61eab730e..4986912b57 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -6688,6 +6688,9 @@ impl LspStore { .into_response() .context("resolve completion")?; + // We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve. + // Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion + let mut completions = completions.borrow_mut(); let completion = &mut completions[completion_index]; if let CompletionSource::Lsp { @@ -6706,40 +6709,6 @@ impl LspStore { ); **lsp_completion = resolved_completion; *resolved = true; - - // We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not supposed to change during resolve. - // Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion - // - // We still re-derive new_text here as a workaround for the specific - // VS Code TypeScript completion resolve flow that vtsls wraps: - // https://github.com/microsoft/vscode/blob/838b48504cd9a2338e2ca9e854da9cec990c4d57/extensions/typescript-language-features/src/languageFeatures/completions.ts#L218 - // - // Some servers (e.g. vtsls with completeFunctionCalls) update - // insertText/textEdit during resolve to add snippet content like - // function call parentheses. - // - // vtsls resolve flow: - // https://github.com/yioneko/vtsls/blob/fecf52324a30e72dfab1537047556076720c1a5f/packages/service/src/service/completion.ts#L228-L244 - // vtsls converter (isSnippet / insertTextFormat): - // https://github.com/yioneko/vtsls/blob/28e075105d7711d635ebf8aefc971bb8e1d2fe65/packages/service/src/utils/converter.ts#L149-L200 - // - // NB: We only update the text content here, NOT the replace/insert - // ranges on `Completion`. Those ranges were converted to anchors from - // the original response and stay valid across buffer edits. The LSP - // ranges in the resolved text_edit are stale when completions are - // cached across keystrokes (see #34094). - let resolved_new_text = lsp_completion - .text_edit - .as_ref() - .map(|edit| match edit { - lsp::CompletionTextEdit::Edit(e) => e.new_text.clone(), - lsp::CompletionTextEdit::InsertAndReplace(e) => e.new_text.clone(), - }) - .or_else(|| lsp_completion.insert_text.clone()); - if let Some(mut resolved_new_text) = resolved_new_text { - LineEnding::normalize(&mut resolved_new_text); - completion.new_text = resolved_new_text; - } } Ok(()) } diff --git a/crates/project/src/project_search.rs b/crates/project/src/project_search.rs index a191467016..921ad7e26a 100644 --- a/crates/project/src/project_search.rs +++ b/crates/project/src/project_search.rs @@ -221,7 +221,6 @@ impl Search { query.clone(), input_paths_tx, sorted_search_results_tx, - tx.clone(), )) .boxed_local(), Self::open_buffers( @@ -413,21 +412,12 @@ impl Search { query: Arc, tx: Sender, results: Sender>, - results_tx: Sender, ) -> impl AsyncFnOnce(&mut AsyncApp) { async move |cx| { _ = maybe!(async move { let gitignored_tracker = PathInclusionMatcher::new(query.clone()); let include_ignored = query.include_ignored(); for worktree in worktrees { - let scan_complete = worktree.read_with(cx, |worktree, _| { - worktree.as_local().map(|local| local.scan_complete()) - }); - if let Some(scan_complete) = scan_complete { - _ = results_tx.send(SearchResult::WaitingForScan).await; - scan_complete.await; - } - let (mut snapshot, worktree_settings) = worktree .read_with(cx, |this, _| { Some((this.snapshot(), this.as_local()?.settings())) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index b04bf12897..5f30ef2aa1 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -25,7 +25,6 @@ pub enum SearchResult { ranges: Vec>, }, LimitReached, - WaitingForScan, } #[derive(Clone, Copy, PartialEq)] diff --git a/crates/project/src/task_store.rs b/crates/project/src/task_store.rs index 34beb9a8e1..5b91a3a890 100644 --- a/crates/project/src/task_store.rs +++ b/crates/project/src/task_store.rs @@ -130,7 +130,7 @@ impl TaskStore { .payload .task_variables .into_iter() - .filter_map(|(k, v)| Some((k.parse().ok()?, v))), + .filter_map(|(k, v)| Some((k.parse().log_err()?, v))), ); let snapshot = location.buffer.read(cx).snapshot(); diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index b010c90115..05e29d0017 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -6278,39 +6278,6 @@ async fn test_dirty_buffer_reloads_after_undo(cx: &mut gpui::TestAppContext) { }); } -#[gpui::test] -async fn test_buffer_file_change_to_binary_fails(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "file.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let buffer = project - .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file.txt"), cx)) - .await - .unwrap(); - - fs.write( - path!("/dir/file.txt").as_ref(), - b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01", - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - - // Test that existing buffer is left untouched - buffer.read_with(cx, |buffer, _| { - assert_eq!(buffer.text(), ""); - }); -} - #[gpui::test] async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -12313,7 +12280,7 @@ async fn search( SearchResult::Buffer { buffer, ranges } => { results.entry(buffer).or_insert(ranges); } - SearchResult::LimitReached | SearchResult::WaitingForScan => {} + SearchResult::LimitReached => {} } } Ok(results diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index b8df94c023..5cca4767a2 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1020,11 +1020,8 @@ impl PickerDelegate for RecentProjectsDelegate { .collect() }; - if !matched_folders.is_empty() { - entries.push(ProjectPickerEntry::Header("Current Folders".into())); - for (index, positions) in matched_folders { - entries.push(ProjectPickerEntry::OpenFolder { index, positions }); - } + for (index, positions) in matched_folders { + entries.push(ProjectPickerEntry::OpenFolder { index, positions }); } } @@ -1285,7 +1282,7 @@ impl PickerDelegate for RecentProjectsDelegate { .child( IconButton::new(("remove-folder", worktree_id.to_usize()), IconName::Close) .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Remove Folder from Project")) + .tooltip(Tooltip::text("Remove Folder from Workspace")) .on_click(cx.listener(move |picker, _, window, cx| { let Some(workspace) = picker.delegate.workspace.upgrade() else { return; @@ -1317,16 +1314,18 @@ impl PickerDelegate for RecentProjectsDelegate { .child( h_flex() .id("open_folder_item") + .gap_3() .w_full() - .gap_2p5() + .overflow_hidden() .when(self.has_any_non_local_projects, |this| { this.child(Icon::new(icon).color(Color::Muted)) }) .child( v_flex() - .min_w_0() + .flex_1() .child( h_flex() + .min_w_0() .gap_1() .child(HighlightedLabel::new( name.to_string(), @@ -1336,7 +1335,8 @@ impl PickerDelegate for RecentProjectsDelegate { this.child( Label::new(branch) .color(Color::Muted) - .truncate(), + .truncate() + .flex_1(), ) }) .when(is_active, |this| { @@ -1359,7 +1359,7 @@ impl PickerDelegate for RecentProjectsDelegate { this.tooltip(move |_, cx| { if let Some(branch) = tooltip_branch.clone() { Tooltip::with_meta( - format!("{}/{}", name, branch), + branch, None, tooltip_path.clone(), cx, @@ -1384,7 +1384,6 @@ impl PickerDelegate for RecentProjectsDelegate { .map(|p| p.compact().to_string_lossy().to_string()) .collect(); let tooltip_path: SharedString = ordered_paths.join("\n").into(); - let icon = icon_for_remote_connection(self.project_connection_options.as_ref()); let mut path_start_offset = 0; let (match_labels, path_highlights): (Vec<_>, Vec<_>) = paths @@ -1439,10 +1438,7 @@ impl PickerDelegate for RecentProjectsDelegate { .child( h_flex() .id("open_project_info_container") - .gap_2p5() - .when(self.has_any_non_local_projects, |this| { - this.child(Icon::new(icon).color(Color::Muted)) - }) + .gap_3() .child({ let mut highlighted = highlighted_match; if !self.render_paths { @@ -1489,12 +1485,6 @@ impl PickerDelegate for RecentProjectsDelegate { }) .unzip(); - let tooltip_title = if paths.len() > 1 { - "Add Folders to this Project" - } else { - "Add Folder to this Project" - }; - let prefix = match &location { SerializedWorkspaceLocation::Remote(options) => { Some(SharedString::from(options.display_name())) @@ -1519,9 +1509,9 @@ impl PickerDelegate for RecentProjectsDelegate { .icon_size(IconSize::Small) .tooltip(move |_, cx| { Tooltip::with_meta( - tooltip_title, + "Add Folders to this Project", None, - "As a multi-root folder", + "As a multi-root folder project", cx, ) }) @@ -1584,7 +1574,7 @@ impl PickerDelegate for RecentProjectsDelegate { .child( h_flex() .id("project_info_container") - .gap_2p5() + .gap_3() .flex_grow() .when(self.has_any_non_local_projects, |this| { this.child(Icon::new(icon).color(Color::Muted)) @@ -1832,7 +1822,7 @@ impl PickerDelegate for RecentProjectsDelegate { menu.context(focus_handle) .when(show_add_to_workspace, |menu| { menu.action( - "Add Folder to this Project", + "Add to this Workspace", AddToWorkspace.boxed_clone(), ) .separator() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8170d303eb..9dcef1b60e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1399,7 +1399,6 @@ impl BufferSearchBar { } let new_match_index = searchable_item .match_index_for_direction(matches, index, direction, count, *token, window, cx); - self.active_match_index = Some(new_match_index); searchable_item.update_matches(matches, Some(new_match_index), *token, window, cx); searchable_item.activate_match(new_match_index, matches, *token, window, cx); @@ -2258,8 +2257,8 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(0)); }); - // Park the cursor in between matches and ensure that going to the previous match - // selects the closest match to the left of the cursor. + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. editor.update_in(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ @@ -2268,6 +2267,7 @@ mod tests { }); }); search_bar.update_in(cx, |search_bar, window, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); search_bar.select_prev_match(&SelectPreviousMatch, window, cx); assert_eq!( editor.update(cx, |editor, cx| editor @@ -2280,8 +2280,8 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(0)); }); - // Park the cursor in between matches and ensure that going to the next match - // selects the closest match to the right of the cursor. + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. editor.update_in(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ @@ -2290,6 +2290,7 @@ mod tests { }); }); search_bar.update_in(cx, |search_bar, window, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); search_bar.select_next_match(&SelectNextMatch, window, cx); assert_eq!( editor.update(cx, |editor, cx| editor @@ -2302,8 +2303,8 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(1)); }); - // Park the cursor after the last match and ensure that going to the previous match - // selects the last match. + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. editor.update_in(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ @@ -2312,6 +2313,7 @@ mod tests { }); }); search_bar.update_in(cx, |search_bar, window, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); search_bar.select_prev_match(&SelectPreviousMatch, window, cx); assert_eq!( editor.update(cx, |editor, cx| editor @@ -2324,8 +2326,8 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(2)); }); - // Park the cursor after the last match and ensure that going to the next match - // wraps around and selects the first match. + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. editor.update_in(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ @@ -2334,6 +2336,7 @@ mod tests { }); }); search_bar.update_in(cx, |search_bar, window, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); search_bar.select_next_match(&SelectNextMatch, window, cx); assert_eq!( editor.update(cx, |editor, cx| editor @@ -2347,7 +2350,7 @@ mod tests { }); // Park the cursor before the first match and ensure that going to the previous match - // wraps around and selects the last match. + // selects the last match. editor.update_in(cx, |editor, window, cx| { editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_display_ranges([ diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index c79937ab7a..a4ea449ffc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -237,7 +237,6 @@ pub struct ProjectSearch { search_id: usize, no_results: Option, limit_reached: bool, - waiting_for_scan: bool, search_history_cursor: SearchHistoryCursor, search_included_history_cursor: SearchHistoryCursor, search_excluded_history_cursor: SearchHistoryCursor, @@ -300,7 +299,6 @@ impl ProjectSearch { search_id: 0, no_results: None, limit_reached: false, - waiting_for_scan: false, search_history_cursor: Default::default(), search_included_history_cursor: Default::default(), search_excluded_history_cursor: Default::default(), @@ -325,7 +323,6 @@ impl ProjectSearch { search_id: self.search_id, no_results: self.no_results, limit_reached: self.limit_reached, - waiting_for_scan: false, search_history_cursor: self.search_history_cursor.clone(), search_included_history_cursor: self.search_included_history_cursor.clone(), search_excluded_history_cursor: self.search_excluded_history_cursor.clone(), @@ -425,17 +422,15 @@ impl ProjectSearch { .update(cx, |excerpts, cx| excerpts.clear(cx)); project_search.no_results = Some(true); project_search.limit_reached = false; - project_search.waiting_for_scan = false; }) .ok()?; let mut limit_reached = false; while let Some(results) = matches.next().await { - let (buffers_with_ranges, has_reached_limit, is_waiting_for_scan) = cx + let (buffers_with_ranges, has_reached_limit) = cx .background_executor() .spawn(async move { let mut limit_reached = false; - let mut waiting_for_scan = false; let mut buffers_with_ranges = Vec::with_capacity(results.len()); for result in results { match result { @@ -445,23 +440,12 @@ impl ProjectSearch { project::search::SearchResult::LimitReached => { limit_reached = true; } - project::search::SearchResult::WaitingForScan => { - waiting_for_scan = true; - } } } - (buffers_with_ranges, limit_reached, waiting_for_scan) + (buffers_with_ranges, limit_reached) }) .await; limit_reached |= has_reached_limit; - if is_waiting_for_scan { - project_search - .update(cx, |project_search, cx| { - project_search.waiting_for_scan = true; - cx.notify(); - }) - .ok()?; - } let mut new_ranges = project_search .update(cx, |project_search, cx| { project_search.excerpts.update(cx, |excerpts, cx| { @@ -499,7 +483,6 @@ impl ProjectSearch { project_search.no_results = Some(false); } project_search.limit_reached = limit_reached; - project_search.waiting_for_scan = false; project_search.pending_search.take(); cx.notify(); }) @@ -533,11 +516,8 @@ impl Render for ProjectSearchView { let model = self.entity.read(cx); let has_no_results = model.no_results.unwrap_or(false); let is_search_underway = model.pending_search.is_some(); - let is_waiting_for_scan = model.waiting_for_scan; - let heading_text = if is_waiting_for_scan { - "Loading project…" - } else if is_search_underway { + let heading_text = if is_search_underway { "Searching…" } else if has_no_results { "No Results" diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index b9accd6b83..56dbb14131 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -182,14 +182,6 @@ pub struct EditPredictionSettingsContent { pub open_ai_compatible_api: Option, /// The directory where manually captured edit prediction examples are stored. pub examples_dir: Option>, - /// Controls whether Zed may collect training data when using Zed's Edit Predictions. - /// Data is only ever captured for files in projects that are detected as open source. - /// - /// - `"default"`: use the preference previously set via the status-bar toggle, - /// or false if no preference has been stored. - /// - `"yes"`: allow data collection for files in open-source projects. - /// - `"no"`: never allow data collection. - pub allow_data_collection: Option, } #[with_fallible_options] @@ -326,33 +318,6 @@ pub struct OllamaEditPredictionSettingsContent { pub prompt_format: Option, } -/// Controls whether Zed collects training data when using Zed's Edit Predictions. -#[derive( - Copy, - Clone, - Debug, - Default, - Eq, - PartialEq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionDataCollectionChoice { - /// Use the preference previously set via the status-bar toggle, or false - /// if no preference has been stored. - #[default] - Default, - /// Allow Zed to collect training data from open-source projects. - Yes, - /// Never allow training data collection. - No, -} - /// The mode in which edit predictions should be displayed. #[derive( Copy, diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 704b13683b..aa3dc0d81a 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -647,12 +647,6 @@ pub struct GitPanelSettingsContent { /// /// Default: false pub starts_open: Option, - - /// Maximum length of the commit message title before a warning is shown. - /// Set to 0 to disable. - /// - /// Default: 72 - pub commit_title_max_length: Option, } #[derive( diff --git a/crates/settings_content/src/theme.rs b/crates/settings_content/src/theme.rs index 3197ae0602..623e463cfb 100644 --- a/crates/settings_content/src/theme.rs +++ b/crates/settings_content/src/theme.rs @@ -1036,9 +1036,6 @@ pub struct ThemeColorsContent { /// Background color for Vim yank highlight. #[serde(rename = "vim.yank.background")] pub vim_yank_background: Option, - /// Foreground color for Helix jump labels. - #[serde(rename = "vim.helix_jump_label.foreground")] - pub vim_helix_jump_label_foreground: Option, /// Background color for Vim Helix Normal mode indicator. #[serde(rename = "vim.helix_normal.background")] pub vim_helix_normal_background: Option, diff --git a/crates/settings_ui/src/components/ollama_model_picker.rs b/crates/settings_ui/src/components/ollama_model_picker.rs index b39b2fc61e..bc6ae06cb4 100644 --- a/crates/settings_ui/src/components/ollama_model_picker.rs +++ b/crates/settings_ui/src/components/ollama_model_picker.rs @@ -185,11 +185,10 @@ pub fn render_ollama_model_picker( field.json_path, window, cx, - move |settings, app| { + move |settings, _cx| { (field.write)( settings, Some(settings::OllamaModelName(model_name.to_string())), - app, ); }, ) diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 17d89b2f76..d784f8a712 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -6,7 +6,6 @@ use settings::{ }; use std::sync::{Arc, OnceLock}; use strum::{EnumMessage, IntoDiscriminant as _, VariantArray}; -use theme::SystemAppearance; use ui::IntoElement; use crate::{ @@ -120,7 +119,7 @@ fn general_page(cx: &App) -> SettingsPage { .when_closing_with_no_tabs .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.when_closing_with_no_tabs = value; }, }), @@ -135,7 +134,7 @@ fn general_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.workspace.on_last_window_closed.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.on_last_window_closed = value; }, }), @@ -150,7 +149,7 @@ fn general_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.workspace.use_system_path_prompts.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.use_system_path_prompts = value; }, }), @@ -163,7 +162,7 @@ fn general_page(cx: &App) -> SettingsPage { field: Box::new(SettingField { json_path: Some("use_system_prompts"), pick: |settings_content| settings_content.workspace.use_system_prompts.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.use_system_prompts = value; }, }), @@ -176,7 +175,7 @@ fn general_page(cx: &App) -> SettingsPage { field: Box::new(SettingField { json_path: Some("redact_private_values"), pick: |settings_content| settings_content.editor.redact_private_values.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.redact_private_values = value; }, }), @@ -192,7 +191,7 @@ fn general_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.project.worktree.private_files.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.worktree.private_files = value; }, } @@ -212,7 +211,7 @@ fn general_page(cx: &App) -> SettingsPage { .cli_default_open_behavior .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.cli_default_open_behavior = value; }, }), @@ -238,7 +237,7 @@ fn general_page(cx: &App) -> SettingsPage { .as_ref() .and_then(|session| session.trust_all_worktrees.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .session .get_or_insert_default() @@ -265,7 +264,7 @@ fn general_page(cx: &App) -> SettingsPage { .as_ref() .and_then(|session| session.restore_unsaved_buffers.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .session .get_or_insert_default() @@ -281,7 +280,7 @@ fn general_page(cx: &App) -> SettingsPage { field: Box::new(SettingField { json_path: Some("restore_on_startup"), pick: |settings_content| settings_content.workspace.restore_on_startup.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.restore_on_startup = value; }, }), @@ -302,7 +301,7 @@ fn general_page(cx: &App) -> SettingsPage { SettingField { json_path: Some("preview_channel_settings"), pick: |settings_content| Some(settings_content), - write: |_settings_content, _value, _app: &App| {}, + write: |_settings_content, _value| {}, } .unimplemented(), ), @@ -316,7 +315,7 @@ fn general_page(cx: &App) -> SettingsPage { SettingField { json_path: Some("settings_profiles"), pick: |settings_content| Some(settings_content), - write: |_settings_content, _value, _app: &App| {}, + write: |_settings_content, _value| {}, } .unimplemented(), ), @@ -339,7 +338,7 @@ fn general_page(cx: &App) -> SettingsPage { .as_ref() .and_then(|telemetry| telemetry.diagnostics.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .telemetry .get_or_insert_default() @@ -360,7 +359,7 @@ fn general_page(cx: &App) -> SettingsPage { .as_ref() .and_then(|telemetry| telemetry.metrics.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.telemetry.get_or_insert_default().metrics = value; }, }), @@ -379,7 +378,7 @@ fn general_page(cx: &App) -> SettingsPage { field: Box::new(SettingField { json_path: Some("auto_update"), pick: |settings_content| settings_content.auto_update.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.auto_update = value; }, }), @@ -423,7 +422,7 @@ fn appearance_page() -> SettingsPage { .as_ref()? .discriminant() as usize]) }, - write: |settings_content, value, app: &App| { + write: |settings_content, value| { let Some(value) = value else { settings_content.theme.theme = None; return; @@ -437,13 +436,7 @@ fn appearance_page() -> SettingsPage { match mode { theme_settings::ThemeAppearanceMode::Light => light.clone(), theme_settings::ThemeAppearanceMode::Dark => dark.clone(), - theme_settings::ThemeAppearanceMode::System => { - if SystemAppearance::global(app).is_light() { - light.clone() - } else { - dark.clone() - } - } + theme_settings::ThemeAppearanceMode::System => dark.clone(), // no cx, can't determine correct choice } }, }; @@ -484,7 +477,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -512,7 +505,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -538,7 +531,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -564,7 +557,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -597,7 +590,7 @@ fn appearance_page() -> SettingsPage { .as_ref()? .discriminant() as usize]) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { settings_content.theme.icon_theme = None; return; @@ -654,7 +647,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -682,7 +675,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -708,7 +701,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -734,7 +727,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -764,7 +757,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("buffer_font_family"), pick: |settings_content| settings_content.theme.buffer_font_family.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.buffer_font_family = value; }, }), @@ -777,7 +770,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("buffer_font_size"), pick: |settings_content| settings_content.theme.buffer_font_size.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.buffer_font_size = value; }, }), @@ -790,7 +783,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("buffer_font_weight"), pick: |settings_content| settings_content.theme.buffer_font_weight.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.buffer_font_weight = value; }, }), @@ -814,7 +807,7 @@ fn appearance_page() -> SettingsPage { as usize], ) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { settings_content.theme.buffer_line_height = None; return; @@ -869,7 +862,7 @@ fn appearance_page() -> SettingsPage { Some(settings::BufferLineHeight::Custom(value)) => Some(value), _ => None, }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -896,7 +889,7 @@ fn appearance_page() -> SettingsPage { pick: |settings_content| { settings_content.theme.buffer_font_features.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.buffer_font_features = value; }, } @@ -914,7 +907,7 @@ fn appearance_page() -> SettingsPage { pick: |settings_content| { settings_content.theme.buffer_font_fallbacks.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.buffer_font_fallbacks = value; }, } @@ -934,7 +927,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("ui_font_family"), pick: |settings_content| settings_content.theme.ui_font_family.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.ui_font_family = value; }, }), @@ -947,7 +940,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("ui_font_size"), pick: |settings_content| settings_content.theme.ui_font_size.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.ui_font_size = value; }, }), @@ -960,7 +953,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("ui_font_weight"), pick: |settings_content| settings_content.theme.ui_font_weight.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.ui_font_weight = value; }, }), @@ -975,7 +968,7 @@ fn appearance_page() -> SettingsPage { SettingField { json_path: Some("ui_font_features"), pick: |settings_content| settings_content.theme.ui_font_features.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.ui_font_features = value; }, } @@ -991,7 +984,7 @@ fn appearance_page() -> SettingsPage { SettingField { json_path: Some("ui_font_fallbacks"), pick: |settings_content| settings_content.theme.ui_font_fallbacks.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.ui_font_fallbacks = value; }, } @@ -1017,7 +1010,7 @@ fn appearance_page() -> SettingsPage { .as_ref() .or(settings_content.theme.ui_font_size.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.agent_ui_font_size = value; }, }), @@ -1036,7 +1029,7 @@ fn appearance_page() -> SettingsPage { .as_ref() .or(settings_content.theme.buffer_font_size.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.agent_buffer_font_size = value; }, }), @@ -1057,7 +1050,7 @@ fn appearance_page() -> SettingsPage { pick: |settings_content| { settings_content.workspace.text_rendering_mode.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.text_rendering_mode = value; }, }), @@ -1076,7 +1069,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("multi_cursor_modifier"), pick: |settings_content| settings_content.editor.multi_cursor_modifier.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.multi_cursor_modifier = value; }, }), @@ -1089,7 +1082,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("cursor_blink"), pick: |settings_content| settings_content.editor.cursor_blink.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.cursor_blink = value; }, }), @@ -1102,7 +1095,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("cursor_shape"), pick: |settings_content| settings_content.editor.cursor_shape.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.cursor_shape = value; }, }), @@ -1115,7 +1108,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("hide_mouse"), pick: |settings_content| settings_content.editor.hide_mouse.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.hide_mouse = value; }, }), @@ -1134,7 +1127,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("unnecessary_code_fade"), pick: |settings_content| settings_content.theme.unnecessary_code_fade.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.theme.unnecessary_code_fade = value; }, }), @@ -1149,7 +1142,7 @@ fn appearance_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.current_line_highlight.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.current_line_highlight = value; }, }), @@ -1162,7 +1155,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("selection_highlight"), pick: |settings_content| settings_content.editor.selection_highlight.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.selection_highlight = value; }, }), @@ -1175,7 +1168,7 @@ fn appearance_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("rounded_selection"), pick: |settings_content| settings_content.editor.rounded_selection.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.rounded_selection = value; }, }), @@ -1193,7 +1186,7 @@ fn appearance_page() -> SettingsPage { .minimum_contrast_for_highlights .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.minimum_contrast_for_highlights = value; }, }), @@ -1219,7 +1212,7 @@ fn appearance_page() -> SettingsPage { .show_wrap_guides .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project .all_languages @@ -1245,7 +1238,7 @@ fn appearance_page() -> SettingsPage { .wrap_guides .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.all_languages.defaults.wrap_guides = value; }, } @@ -1309,7 +1302,7 @@ fn keymap_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("base_keymap"), pick: |settings_content| settings_content.base_keymap.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.base_keymap = value; }, }), @@ -1383,7 +1376,7 @@ fn editor_page() -> SettingsPage { as usize], ) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { settings_content.workspace.autosave = None; return; @@ -1439,7 +1432,7 @@ fn editor_page() -> SettingsPage { }) => Some(milliseconds), _ => None, }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { settings_content.workspace.autosave = None; return; @@ -1476,7 +1469,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|settings| settings.enabled.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.which_key.get_or_insert_default().enabled = value; }, }), @@ -1494,7 +1487,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|settings| settings.delay_ms.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.which_key.get_or_insert_default().delay_ms = value; }, }), @@ -1515,7 +1508,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.double_click_in_multibuffer.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.double_click_in_multibuffer = value; }, }), @@ -1528,7 +1521,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("expand_excerpt_lines"), pick: |settings_content| settings_content.editor.expand_excerpt_lines.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.expand_excerpt_lines = value; }, }), @@ -1541,7 +1534,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("excerpt_context_lines"), pick: |settings_content| settings_content.editor.excerpt_context_lines.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.excerpt_context_lines = value; }, }), @@ -1561,7 +1554,7 @@ fn editor_page() -> SettingsPage { outline_panel.expand_outlines_with_depth.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -1577,7 +1570,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("diff_view_style"), pick: |settings_content| settings_content.editor.diff_view_style.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.diff_view_style = value; }, }), @@ -1592,7 +1585,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.minimum_split_diff_width.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.minimum_split_diff_width = value; }, }), @@ -1613,7 +1606,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.scroll_beyond_last_line.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.scroll_beyond_last_line = value; }, }), @@ -1628,7 +1621,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.vertical_scroll_margin.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.vertical_scroll_margin = value; }, }), @@ -1643,7 +1636,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.horizontal_scroll_margin.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.horizontal_scroll_margin = value; }, }), @@ -1656,7 +1649,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("scroll_sensitivity"), pick: |settings_content| settings_content.editor.scroll_sensitivity.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.scroll_sensitivity = value; }, }), @@ -1669,7 +1662,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("mouse_wheel_zoom"), pick: |settings_content| settings_content.editor.mouse_wheel_zoom.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.mouse_wheel_zoom = value; }, }), @@ -1684,7 +1677,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.fast_scroll_sensitivity.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.fast_scroll_sensitivity = value; }, }), @@ -1697,7 +1690,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("autoscroll_on_clicks"), pick: |settings_content| settings_content.editor.autoscroll_on_clicks.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.autoscroll_on_clicks = value; }, }), @@ -1716,7 +1709,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|sticky_scroll| sticky_scroll.enabled.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .sticky_scroll @@ -1739,7 +1732,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("auto_signature_help"), pick: |settings_content| settings_content.editor.auto_signature_help.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.auto_signature_help = value; }, }), @@ -1757,7 +1750,7 @@ fn editor_page() -> SettingsPage { .show_signature_help_after_edits .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.show_signature_help_after_edits = value; }, }), @@ -1770,7 +1763,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("snippet_sort_order"), pick: |settings_content| settings_content.editor.snippet_sort_order.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.snippet_sort_order = value; }, }), @@ -1789,7 +1782,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("hover_popover_enabled"), pick: |settings_content| settings_content.editor.hover_popover_enabled.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.hover_popover_enabled = value; }, }), @@ -1803,7 +1796,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("hover_popover_delay"), pick: |settings_content| settings_content.editor.hover_popover_delay.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.hover_popover_delay = value; }, }), @@ -1816,7 +1809,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("hover_popover_sticky"), pick: |settings_content| settings_content.editor.hover_popover_sticky.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.hover_popover_sticky = value; }, }), @@ -1832,7 +1825,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.hover_popover_hiding_delay.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.hover_popover_hiding_delay = value; }, }), @@ -1857,7 +1850,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|drag_and_drop| drag_and_drop.enabled.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .drag_and_drop_selection @@ -1880,7 +1873,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|drag_and_drop| drag_and_drop.delay.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .drag_and_drop_selection @@ -1909,7 +1902,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|gutter| gutter.line_numbers.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .gutter @@ -1926,7 +1919,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("relative_line_numbers"), pick: |settings_content| settings_content.editor.relative_line_numbers.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.relative_line_numbers = value; }, }), @@ -1945,7 +1938,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|gutter| gutter.runnables.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .gutter @@ -1968,7 +1961,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|gutter| gutter.breakpoints.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .gutter @@ -1991,7 +1984,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|gutter| gutter.bookmarks.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .gutter @@ -2014,7 +2007,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|gutter| gutter.folds.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.gutter.get_or_insert_default().folds = value; }, }), @@ -2033,7 +2026,7 @@ fn editor_page() -> SettingsPage { .as_ref() .and_then(|gutter| gutter.min_line_number_digits.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .gutter @@ -2050,7 +2043,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("inline_code_actions"), pick: |settings_content| settings_content.editor.inline_code_actions.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.inline_code_actions = value; }, }), @@ -2071,7 +2064,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.scrollbar.as_ref()?.show.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2090,7 +2083,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.scrollbar.as_ref()?.cursors.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2114,7 +2107,7 @@ fn editor_page() -> SettingsPage { .git_diff .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2138,7 +2131,7 @@ fn editor_page() -> SettingsPage { .search_results .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2162,7 +2155,7 @@ fn editor_page() -> SettingsPage { .selected_text .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2186,7 +2179,7 @@ fn editor_page() -> SettingsPage { .selected_symbol .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2210,7 +2203,7 @@ fn editor_page() -> SettingsPage { .diagnostics .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2236,7 +2229,7 @@ fn editor_page() -> SettingsPage { .horizontal .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2264,7 +2257,7 @@ fn editor_page() -> SettingsPage { .vertical .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2291,7 +2284,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.minimap.as_ref()?.show.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.minimap.get_or_insert_default().show = value; }, }), @@ -2311,7 +2304,7 @@ fn editor_page() -> SettingsPage { .display_in .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .minimap @@ -2330,7 +2323,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.minimap.as_ref()?.thumb.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .minimap @@ -2354,7 +2347,7 @@ fn editor_page() -> SettingsPage { .thumb_border .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .minimap @@ -2378,7 +2371,7 @@ fn editor_page() -> SettingsPage { .and_then(|minimap| minimap.current_line_highlight.as_ref()) .or(settings_content.editor.current_line_highlight.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .minimap @@ -2402,7 +2395,7 @@ fn editor_page() -> SettingsPage { .max_width_columns .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .minimap @@ -2432,7 +2425,7 @@ fn editor_page() -> SettingsPage { .breadcrumbs .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .toolbar @@ -2456,7 +2449,7 @@ fn editor_page() -> SettingsPage { .quick_actions .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .toolbar @@ -2480,7 +2473,7 @@ fn editor_page() -> SettingsPage { .selections_menu .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .toolbar @@ -2504,7 +2497,7 @@ fn editor_page() -> SettingsPage { .agent_review .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .toolbar @@ -2528,7 +2521,7 @@ fn editor_page() -> SettingsPage { .code_actions .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .toolbar @@ -2551,7 +2544,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("vim.default_mode"), pick: |settings_content| settings_content.vim.as_ref()?.default_mode.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.vim.get_or_insert_default().default_mode = value; }, }), @@ -2570,7 +2563,7 @@ fn editor_page() -> SettingsPage { .toggle_relative_line_numbers .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2588,7 +2581,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.vim.as_ref()?.use_system_clipboard.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2606,7 +2599,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.vim.as_ref()?.use_smartcase_find.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2622,7 +2615,7 @@ fn editor_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("vim.gdefault"), pick: |settings_content| settings_content.vim.as_ref()?.gdefault.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.vim.get_or_insert_default().gdefault = value; }, }), @@ -2641,7 +2634,7 @@ fn editor_page() -> SettingsPage { .highlight_on_yank_duration .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2659,7 +2652,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.vim.as_ref()?.use_regex_search.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2683,7 +2676,7 @@ fn editor_page() -> SettingsPage { .normal .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2709,7 +2702,7 @@ fn editor_page() -> SettingsPage { .insert .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2735,7 +2728,7 @@ fn editor_page() -> SettingsPage { .replace .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2761,7 +2754,7 @@ fn editor_page() -> SettingsPage { .visual .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .vim .get_or_insert_default() @@ -2782,7 +2775,7 @@ fn editor_page() -> SettingsPage { pick: |settings_content| { settings_content.vim.as_ref()?.custom_digraphs.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.vim.get_or_insert_default().custom_digraphs = value; }, } @@ -2829,7 +2822,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.project.all_languages.file_types.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.all_languages.file_types = value; }, } @@ -2852,7 +2845,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.editor.diagnostics_max_severity.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.diagnostics_max_severity = value; }, }), @@ -2871,7 +2864,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .include_warnings .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -2901,7 +2894,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .enabled .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -2927,7 +2920,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .update_debounce_ms .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -2953,7 +2946,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .padding .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -2979,7 +2972,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .min_column .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -3011,7 +3004,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .enabled .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -3038,7 +3031,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { .debounce_ms .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .diagnostics .get_or_insert_default() @@ -3064,7 +3057,7 @@ fn languages_and_tools_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.editor.lsp_highlight_debounce.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.lsp_highlight_debounce = value; }, }), @@ -3134,7 +3127,7 @@ fn search_and_files_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.search.as_ref()?.whole_word.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .search @@ -3158,7 +3151,7 @@ fn search_and_files_page() -> SettingsPage { .case_sensitive .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .search @@ -3175,7 +3168,7 @@ fn search_and_files_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("use_smartcase_search"), pick: |settings_content| settings_content.editor.use_smartcase_search.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.use_smartcase_search = value; }, }), @@ -3195,7 +3188,7 @@ fn search_and_files_page() -> SettingsPage { .include_ignored .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .search @@ -3214,7 +3207,7 @@ fn search_and_files_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.search.as_ref()?.regex.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.search.get_or_insert_default().regex = value; }, }), @@ -3227,7 +3220,7 @@ fn search_and_files_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("search_wrap"), pick: |settings_content| settings_content.editor.search_wrap.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.search_wrap = value; }, }), @@ -3246,7 +3239,7 @@ fn search_and_files_page() -> SettingsPage { .as_ref() .and_then(|search| search.center_on_match.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .search @@ -3268,7 +3261,7 @@ fn search_and_files_page() -> SettingsPage { .seed_search_query_from_cursor .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.seed_search_query_from_cursor = value; }, }), @@ -3294,7 +3287,7 @@ fn search_and_files_page() -> SettingsPage { .include_ignored .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .file_finder .get_or_insert_default() @@ -3312,7 +3305,7 @@ fn search_and_files_page() -> SettingsPage { pick: |settings_content| { settings_content.file_finder.as_ref()?.file_icons.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .file_finder .get_or_insert_default() @@ -3334,7 +3327,7 @@ fn search_and_files_page() -> SettingsPage { .modal_max_width .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .file_finder .get_or_insert_default() @@ -3356,7 +3349,7 @@ fn search_and_files_page() -> SettingsPage { .skip_focus_for_active_in_search .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .file_finder .get_or_insert_default() @@ -3385,7 +3378,7 @@ fn search_and_files_page() -> SettingsPage { .file_scan_exclusions .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.worktree.file_scan_exclusions = value; }, } @@ -3407,7 +3400,7 @@ fn search_and_files_page() -> SettingsPage { .file_scan_inclusions .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.worktree.file_scan_inclusions = value; }, } @@ -3424,7 +3417,7 @@ fn search_and_files_page() -> SettingsPage { pick: |settings_content| { settings_content.workspace.restore_on_file_reopen.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.restore_on_file_reopen = value; }, }), @@ -3439,7 +3432,7 @@ fn search_and_files_page() -> SettingsPage { pick: |settings_content| { settings_content.workspace.close_on_file_delete.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.close_on_file_delete = value; }, }), @@ -3467,7 +3460,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.project_panel.as_ref()?.button.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -3489,7 +3482,7 @@ fn window_and_layout_page() -> SettingsPage { .active_language_button .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .status_bar .get_or_insert_default() @@ -3511,7 +3504,7 @@ fn window_and_layout_page() -> SettingsPage { .active_encoding_button .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .status_bar .get_or_insert_default() @@ -3533,7 +3526,7 @@ fn window_and_layout_page() -> SettingsPage { .cursor_position_button .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .status_bar .get_or_insert_default() @@ -3549,7 +3542,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("terminal.button"), pick: |settings_content| settings_content.terminal.as_ref()?.button.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().button = value; }, }), @@ -3562,7 +3555,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("diagnostics.button"), pick: |settings_content| settings_content.diagnostics.as_ref()?.button.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.diagnostics.get_or_insert_default().button = value; }, }), @@ -3577,7 +3570,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.editor.search.as_ref()?.button.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .search @@ -3594,7 +3587,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("debugger.button"), pick: |settings_content| settings_content.debugger.as_ref()?.button.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.debugger.get_or_insert_default().button = value; }, }), @@ -3613,7 +3606,7 @@ fn window_and_layout_page() -> SettingsPage { .show_active_file .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .status_bar .get_or_insert_default() @@ -3641,7 +3634,7 @@ fn window_and_layout_page() -> SettingsPage { .show_branch_status_icon .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3663,7 +3656,7 @@ fn window_and_layout_page() -> SettingsPage { .show_branch_name .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3685,7 +3678,7 @@ fn window_and_layout_page() -> SettingsPage { .show_project_items .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3707,7 +3700,7 @@ fn window_and_layout_page() -> SettingsPage { .show_onboarding_banner .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3725,7 +3718,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.title_bar.as_ref()?.show_sign_in.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3743,7 +3736,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.title_bar.as_ref()?.show_user_menu.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3765,7 +3758,7 @@ fn window_and_layout_page() -> SettingsPage { .show_user_picture .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3783,7 +3776,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.title_bar.as_ref()?.show_menus.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3812,7 +3805,7 @@ fn window_and_layout_page() -> SettingsPage { as usize], ) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { settings_content .title_bar @@ -3892,7 +3885,7 @@ fn window_and_layout_page() -> SettingsPage { } _ => DEFAULT_EMPTY_STRING, }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .title_bar .get_or_insert_default() @@ -3921,7 +3914,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("tab_bar.show"), pick: |settings_content| settings_content.tab_bar.as_ref()?.show.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.tab_bar.get_or_insert_default().show = value; }, }), @@ -3934,7 +3927,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("tabs.git_status"), pick: |settings_content| settings_content.tabs.as_ref()?.git_status.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.tabs.get_or_insert_default().git_status = value; }, }), @@ -3947,7 +3940,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("tabs.file_icons"), pick: |settings_content| settings_content.tabs.as_ref()?.file_icons.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.tabs.get_or_insert_default().file_icons = value; }, }), @@ -3962,7 +3955,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.tabs.as_ref()?.close_position.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.tabs.get_or_insert_default().close_position = value; }, }), @@ -3979,7 +3972,7 @@ fn window_and_layout_page() -> SettingsPage { SettingField { json_path: Some("max_tabs"), pick: |settings_content| settings_content.workspace.max_tabs.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.max_tabs = value; }, } @@ -3999,7 +3992,7 @@ fn window_and_layout_page() -> SettingsPage { .show_nav_history_buttons .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .tab_bar .get_or_insert_default() @@ -4021,7 +4014,7 @@ fn window_and_layout_page() -> SettingsPage { .show_tab_bar_buttons .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .tab_bar .get_or_insert_default() @@ -4043,7 +4036,7 @@ fn window_and_layout_page() -> SettingsPage { .show_pinned_tabs_in_separate_row .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .tab_bar .get_or_insert_default() @@ -4067,7 +4060,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.tabs.as_ref()?.activate_on_close.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .tabs .get_or_insert_default() @@ -4085,7 +4078,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.tabs.as_ref()?.show_diagnostics.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .tabs .get_or_insert_default() @@ -4103,7 +4096,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.tabs.as_ref()?.show_close_button.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .tabs .get_or_insert_default() @@ -4127,7 +4120,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.preview_tabs.as_ref()?.enabled.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4149,7 +4142,7 @@ fn window_and_layout_page() -> SettingsPage { .enable_preview_from_project_panel .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4171,7 +4164,7 @@ fn window_and_layout_page() -> SettingsPage { .enable_preview_from_file_finder .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4193,7 +4186,7 @@ fn window_and_layout_page() -> SettingsPage { .enable_preview_from_multibuffer .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4215,7 +4208,7 @@ fn window_and_layout_page() -> SettingsPage { .enable_preview_multibuffer_from_code_navigation .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4237,7 +4230,7 @@ fn window_and_layout_page() -> SettingsPage { .enable_preview_file_from_code_navigation .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4259,7 +4252,7 @@ fn window_and_layout_page() -> SettingsPage { .enable_keep_preview_on_code_navigation .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .preview_tabs .get_or_insert_default() @@ -4281,7 +4274,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("bottom_dock_layout"), pick: |settings_content| settings_content.workspace.bottom_dock_layout.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.bottom_dock_layout = value; }, }), @@ -4302,7 +4295,7 @@ fn window_and_layout_page() -> SettingsPage { .left_padding .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .workspace .centered_layout @@ -4326,7 +4319,7 @@ fn window_and_layout_page() -> SettingsPage { .right_padding .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .workspace .centered_layout @@ -4348,7 +4341,7 @@ fn window_and_layout_page() -> SettingsPage { .as_ref() .and_then(|s| s.enabled.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .workspace .focus_follows_mouse @@ -4371,7 +4364,7 @@ fn window_and_layout_page() -> SettingsPage { .as_ref() .and_then(|s| s.debounce_ms.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .workspace .focus_follows_mouse @@ -4397,7 +4390,7 @@ fn window_and_layout_page() -> SettingsPage { pick: |settings_content| { settings_content.workspace.use_system_window_tabs.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.use_system_window_tabs = value; }, }), @@ -4410,7 +4403,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("window_decorations"), pick: |settings_content| settings_content.workspace.window_decorations.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.window_decorations = value; }, }), @@ -4436,7 +4429,7 @@ fn window_and_layout_page() -> SettingsPage { .inactive_opacity .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .workspace .active_pane_modifiers @@ -4460,7 +4453,7 @@ fn window_and_layout_page() -> SettingsPage { .border_size .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .workspace .active_pane_modifiers @@ -4477,7 +4470,7 @@ fn window_and_layout_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("zoomed_padding"), pick: |settings_content| settings_content.workspace.zoomed_padding.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.zoomed_padding = value; }, }), @@ -4501,7 +4494,7 @@ fn window_and_layout_page() -> SettingsPage { .pane_split_direction_vertical .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.pane_split_direction_vertical = value; }, }), @@ -4519,7 +4512,7 @@ fn window_and_layout_page() -> SettingsPage { .pane_split_direction_horizontal .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.pane_split_direction_horizontal = value; }, }), @@ -4555,7 +4548,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("project_panel.dock"), pick: |settings_content| settings_content.project_panel.as_ref()?.dock.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project_panel.get_or_insert_default().dock = value; }, }), @@ -4574,7 +4567,7 @@ fn panels_page() -> SettingsPage { .default_width .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4596,7 +4589,7 @@ fn panels_page() -> SettingsPage { .hide_gitignore .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4618,7 +4611,7 @@ fn panels_page() -> SettingsPage { .entry_spacing .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4636,7 +4629,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.project_panel.as_ref()?.file_icons.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4658,7 +4651,7 @@ fn panels_page() -> SettingsPage { .folder_icons .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4676,7 +4669,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.project_panel.as_ref()?.git_status.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4698,7 +4691,7 @@ fn panels_page() -> SettingsPage { .indent_size .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4720,7 +4713,7 @@ fn panels_page() -> SettingsPage { .auto_reveal_entries .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4742,7 +4735,7 @@ fn panels_page() -> SettingsPage { .starts_open .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4764,7 +4757,7 @@ fn panels_page() -> SettingsPage { .auto_fold_dirs .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4786,7 +4779,7 @@ fn panels_page() -> SettingsPage { .bold_folder_labels .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4812,7 +4805,7 @@ fn panels_page() -> SettingsPage { .as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4838,7 +4831,7 @@ fn panels_page() -> SettingsPage { .horizontal_scroll .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4862,7 +4855,7 @@ fn panels_page() -> SettingsPage { .show_diagnostics .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4884,7 +4877,7 @@ fn panels_page() -> SettingsPage { .diagnostic_badges .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4906,7 +4899,7 @@ fn panels_page() -> SettingsPage { .git_status_indicator .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4928,7 +4921,7 @@ fn panels_page() -> SettingsPage { .sticky_scroll .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4953,7 +4946,7 @@ fn panels_page() -> SettingsPage { .show .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4976,7 +4969,7 @@ fn panels_page() -> SettingsPage { .drag_and_drop .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -4994,7 +4987,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.project_panel.as_ref()?.hide_root.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5016,7 +5009,7 @@ fn panels_page() -> SettingsPage { .hide_hidden .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5034,7 +5027,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.project_panel.as_ref()?.sort_mode.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5051,7 +5044,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.project_panel.as_ref()?.sort_order.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5076,7 +5069,7 @@ fn panels_page() -> SettingsPage { .on_create .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5102,7 +5095,7 @@ fn panels_page() -> SettingsPage { .on_paste .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5128,7 +5121,7 @@ fn panels_page() -> SettingsPage { .on_drop .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project_panel .get_or_insert_default() @@ -5149,7 +5142,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.project.worktree.hidden_files.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.worktree.hidden_files = value; }, } @@ -5170,7 +5163,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("terminal.dock"), pick: |settings_content| settings_content.terminal.as_ref()?.dock.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().dock = value; }, }), @@ -5183,7 +5176,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("terminal.flexible"), pick: |settings_content| settings_content.terminal.as_ref()?.flexible.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().flexible = value; }, }), @@ -5202,7 +5195,7 @@ fn panels_page() -> SettingsPage { .show_count_badge .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -5226,7 +5219,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.outline_panel.as_ref()?.button.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5242,7 +5235,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("outline_panel.dock"), pick: |settings_content| settings_content.outline_panel.as_ref()?.dock.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.outline_panel.get_or_insert_default().dock = value; }, }), @@ -5261,7 +5254,7 @@ fn panels_page() -> SettingsPage { .default_width .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5279,7 +5272,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.outline_panel.as_ref()?.file_icons.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5301,7 +5294,7 @@ fn panels_page() -> SettingsPage { .folder_icons .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5319,7 +5312,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.outline_panel.as_ref()?.git_status.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5341,7 +5334,7 @@ fn panels_page() -> SettingsPage { .indent_size .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5363,7 +5356,7 @@ fn panels_page() -> SettingsPage { .auto_reveal_entries .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5385,7 +5378,7 @@ fn panels_page() -> SettingsPage { .auto_fold_dirs .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5410,7 +5403,7 @@ fn panels_page() -> SettingsPage { .show .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .outline_panel .get_or_insert_default() @@ -5424,7 +5417,7 @@ fn panels_page() -> SettingsPage { ] } - fn git_panel_section() -> [SettingsPageItem; 15] { + fn git_panel_section() -> [SettingsPageItem; 14] { [ SettingsPageItem::SectionHeader("Git Panel"), SettingsPageItem::SettingItem(SettingItem { @@ -5433,7 +5426,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("git_panel.button"), pick: |settings_content| settings_content.git_panel.as_ref()?.button.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git_panel.get_or_insert_default().button = value; }, }), @@ -5446,7 +5439,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("git_panel.dock"), pick: |settings_content| settings_content.git_panel.as_ref()?.dock.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git_panel.get_or_insert_default().dock = value; }, }), @@ -5461,7 +5454,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.default_width.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5479,7 +5472,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.status_style.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5501,7 +5494,7 @@ fn panels_page() -> SettingsPage { .fallback_branch_name .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5519,7 +5512,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.sort_by_path.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5541,7 +5534,7 @@ fn panels_page() -> SettingsPage { .collapse_untracked_diff .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5559,7 +5552,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.tree_view.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git_panel.get_or_insert_default().tree_view = value; }, }), @@ -5574,7 +5567,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.file_icons.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5592,7 +5585,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.folder_icons.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5610,7 +5603,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.git_panel.as_ref()?.diff_stats.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5632,7 +5625,7 @@ fn panels_page() -> SettingsPage { .show_count_badge .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5642,28 +5635,6 @@ fn panels_page() -> SettingsPage { metadata: None, files: USER, }), - SettingsPageItem::SettingItem(SettingItem { - title: "Commit Title Max Length", - description: "Maximum length of the commit message title before a warning is shown. Set to 0 to disable.", - field: Box::new(SettingField { - json_path: Some("git_panel.commit_title_max_length"), - pick: |settings_content| { - settings_content - .git_panel - .as_ref()? - .commit_title_max_length - .as_ref() - }, - write: |settings_content, value, _app: &App| { - settings_content - .git_panel - .get_or_insert_default() - .commit_title_max_length = value; - }, - }), - metadata: None, - files: USER, - }), SettingsPageItem::SettingItem(SettingItem { title: "Scroll Bar", description: "How and when the scrollbar should be displayed.", @@ -5680,7 +5651,7 @@ fn panels_page() -> SettingsPage { .as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git_panel .get_or_insert_default() @@ -5704,7 +5675,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("debugger.dock"), pick: |settings_content| settings_content.debugger.as_ref()?.dock.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.debugger.get_or_insert_default().dock = value; }, }), @@ -5729,7 +5700,7 @@ fn panels_page() -> SettingsPage { .button .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .collaboration_panel .get_or_insert_default() @@ -5747,7 +5718,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.collaboration_panel.as_ref()?.dock.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .collaboration_panel .get_or_insert_default() @@ -5769,7 +5740,7 @@ fn panels_page() -> SettingsPage { .default_width .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .collaboration_panel .get_or_insert_default() @@ -5791,7 +5762,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("agent.button"), pick: |settings_content| settings_content.agent.as_ref()?.button.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.agent.get_or_insert_default().button = value; }, }), @@ -5804,7 +5775,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("agent.dock"), pick: |settings_content| settings_content.agent.as_ref()?.dock.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.agent.get_or_insert_default().dock = value; }, }), @@ -5817,7 +5788,7 @@ fn panels_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("agent.flexible"), pick: |settings_content| settings_content.agent.as_ref()?.flexible.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.agent.get_or_insert_default().flexible = value; }, }), @@ -5832,7 +5803,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.default_width.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.agent.get_or_insert_default().default_width = value; }, }), @@ -5847,7 +5818,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.default_height.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -5871,7 +5842,7 @@ fn panels_page() -> SettingsPage { .limit_content_width .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -5899,7 +5870,7 @@ fn panels_page() -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.max_content_width.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -5943,7 +5914,7 @@ fn debugger_page() -> SettingsPage { .stepping_granularity .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .debugger .get_or_insert_default() @@ -5965,7 +5936,7 @@ fn debugger_page() -> SettingsPage { .save_breakpoints .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .debugger .get_or_insert_default() @@ -5981,7 +5952,7 @@ fn debugger_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("debugger.timeout"), pick: |settings_content| settings_content.debugger.as_ref()?.timeout.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.debugger.get_or_insert_default().timeout = value; }, }), @@ -6000,7 +5971,7 @@ fn debugger_page() -> SettingsPage { .log_dap_communications .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .debugger .get_or_insert_default() @@ -6022,7 +5993,7 @@ fn debugger_page() -> SettingsPage { .format_dap_log_messages .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .debugger .get_or_insert_default() @@ -6063,7 +6034,7 @@ fn terminal_page() -> SettingsPage { .discriminant() as usize ]) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { if let Some(terminal) = settings_content.terminal.as_mut() { terminal.project.shell = None; @@ -6138,7 +6109,7 @@ fn terminal_page() -> SettingsPage { Some(settings::Shell::Program(program)) => Some(program), _ => None, }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -6169,7 +6140,7 @@ fn terminal_page() -> SettingsPage { _ => None, } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -6202,7 +6173,7 @@ fn terminal_page() -> SettingsPage { _ => None, } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -6236,7 +6207,7 @@ fn terminal_page() -> SettingsPage { _ => None, } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { match settings_content .terminal .get_or_insert_default() @@ -6275,7 +6246,7 @@ fn terminal_page() -> SettingsPage { .discriminant() as usize ]) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { if let Some(terminal) = settings_content.terminal.as_mut() { terminal.project.working_directory = None; @@ -6343,7 +6314,7 @@ fn terminal_page() -> SettingsPage { _ => None, } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let value = value.unwrap_or_default(); match settings_content .terminal @@ -6369,7 +6340,7 @@ fn terminal_page() -> SettingsPage { SettingField { json_path: Some("terminal.env"), pick: |settings_content| settings_content.terminal.as_ref()?.project.env.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().project.env = value; }, } @@ -6385,7 +6356,7 @@ fn terminal_page() -> SettingsPage { SettingField { json_path: Some("terminal.detect_venv"), pick: |settings_content| settings_content.terminal.as_ref()?.project.detect_venv.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6416,7 +6387,7 @@ fn terminal_page() -> SettingsPage { .and_then(|terminal| terminal.font_size.as_ref()) .or(settings_content.theme.buffer_font_size.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().font_size = value; }, }), @@ -6435,7 +6406,7 @@ fn terminal_page() -> SettingsPage { .and_then(|terminal| terminal.font_family.as_ref()) .or(settings_content.theme.buffer_font_family.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6458,7 +6429,7 @@ fn terminal_page() -> SettingsPage { .and_then(|terminal| terminal.font_fallbacks.as_ref()) .or(settings_content.theme.buffer_font_fallbacks.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6478,7 +6449,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.font_weight.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6501,7 +6472,7 @@ fn terminal_page() -> SettingsPage { .and_then(|terminal| terminal.font_features.as_ref()) .or(settings_content.theme.buffer_font_features.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6528,7 +6499,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.line_height.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6548,7 +6519,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.cursor_shape.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6564,7 +6535,7 @@ fn terminal_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("terminal.blinking"), pick: |settings_content| settings_content.terminal.as_ref()?.blinking.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().blinking = value; }, }), @@ -6583,7 +6554,7 @@ fn terminal_page() -> SettingsPage { .alternate_scroll .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6605,7 +6576,7 @@ fn terminal_page() -> SettingsPage { .minimum_contrast .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6629,7 +6600,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.option_as_meta.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6647,7 +6618,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.copy_on_select.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6669,7 +6640,7 @@ fn terminal_page() -> SettingsPage { .keep_selection_on_copy .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6685,7 +6656,7 @@ fn terminal_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("terminal.bell"), pick: |settings_content| settings_content.terminal.as_ref()?.bell.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.terminal.get_or_insert_default().bell = value; }, }), @@ -6706,7 +6677,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.default_width.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6724,7 +6695,7 @@ fn terminal_page() -> SettingsPage { pick: |settings_content| { settings_content.terminal.as_ref()?.default_height.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6752,7 +6723,7 @@ fn terminal_page() -> SettingsPage { .max_scroll_history_lines .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6774,7 +6745,7 @@ fn terminal_page() -> SettingsPage { .scroll_multiplier .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6804,7 +6775,7 @@ fn terminal_page() -> SettingsPage { .breadcrumbs .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6838,7 +6809,7 @@ fn terminal_page() -> SettingsPage { .as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .terminal .get_or_insert_default() @@ -6888,7 +6859,7 @@ fn version_control_page() -> SettingsPage { .disable_git .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -6927,7 +6898,7 @@ fn version_control_page() -> SettingsPage { .enable_status .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -6953,7 +6924,7 @@ fn version_control_page() -> SettingsPage { .enable_diff .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -6979,7 +6950,7 @@ fn version_control_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("git.git_gutter"), pick: |settings_content| settings_content.git.as_ref()?.git_gutter.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git.get_or_insert_default().git_gutter = value; }, }), @@ -6995,7 +6966,7 @@ fn version_control_page() -> SettingsPage { pick: |settings_content| { settings_content.git.as_ref()?.gutter_debounce.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git.get_or_insert_default().gutter_debounce = value; }, }), @@ -7022,7 +6993,7 @@ fn version_control_page() -> SettingsPage { .enabled .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7048,7 +7019,7 @@ fn version_control_page() -> SettingsPage { .delay_ms .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7074,7 +7045,7 @@ fn version_control_page() -> SettingsPage { .padding .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7100,7 +7071,7 @@ fn version_control_page() -> SettingsPage { .min_column .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7126,7 +7097,7 @@ fn version_control_page() -> SettingsPage { .show_commit_summary .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7158,7 +7129,7 @@ fn version_control_page() -> SettingsPage { .show_avatar .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7190,7 +7161,7 @@ fn version_control_page() -> SettingsPage { .show_author_name .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .git .get_or_insert_default() @@ -7214,7 +7185,7 @@ fn version_control_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("git.hunk_style"), pick: |settings_content| settings_content.git.as_ref()?.hunk_style.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git.get_or_insert_default().hunk_style = value; }, }), @@ -7227,7 +7198,7 @@ fn version_control_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("git.path_style"), pick: |settings_content| settings_content.git.as_ref()?.path_style.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.git.get_or_insert_default().path_style = value; }, }), @@ -7260,7 +7231,7 @@ fn collaboration_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("calls.mute_on_join"), pick: |settings_content| settings_content.calls.as_ref()?.mute_on_join.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.calls.get_or_insert_default().mute_on_join = value; }, }), @@ -7275,7 +7246,7 @@ fn collaboration_page() -> SettingsPage { pick: |settings_content| { settings_content.calls.as_ref()?.share_on_join.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.calls.get_or_insert_default().share_on_join = value; }, }), @@ -7309,7 +7280,7 @@ fn collaboration_page() -> SettingsPage { .as_ref() .or(DEFAULT_EMPTY_AUDIO_OUTPUT) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .audio .get_or_insert_default() @@ -7332,7 +7303,7 @@ fn collaboration_page() -> SettingsPage { .as_ref() .or(DEFAULT_EMPTY_AUDIO_INPUT) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .audio .get_or_insert_default() @@ -7361,7 +7332,7 @@ fn ai_page(cx: &App) -> SettingsPage { field: Box::new(SettingField { json_path: Some("disable_ai"), pick: |settings_content| settings_content.project.disable_ai.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.disable_ai = value; }, }), @@ -7374,7 +7345,7 @@ fn ai_page(cx: &App) -> SettingsPage { field: Box::new(SettingField { json_path: Some("agent.sidebar_side"), pick: |settings_content| settings_content.agent.as_ref()?.sidebar_side.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.agent.get_or_insert_default().sidebar_side = value; }, }), @@ -7410,7 +7381,7 @@ fn ai_page(cx: &App) -> SettingsPage { .new_thread_location .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7430,7 +7401,7 @@ fn ai_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.single_file_review.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7448,7 +7419,7 @@ fn ai_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.enable_feedback.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7470,7 +7441,7 @@ fn ai_page(cx: &App) -> SettingsPage { .notify_when_agent_waiting .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7492,7 +7463,7 @@ fn ai_page(cx: &App) -> SettingsPage { .play_sound_when_agent_done .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7510,7 +7481,7 @@ fn ai_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.expand_edit_card.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7532,7 +7503,7 @@ fn ai_page(cx: &App) -> SettingsPage { .expand_terminal_card .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7554,7 +7525,7 @@ fn ai_page(cx: &App) -> SettingsPage { .thinking_display .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7576,7 +7547,7 @@ fn ai_page(cx: &App) -> SettingsPage { .cancel_generation_on_terminal_stop .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7598,7 +7569,7 @@ fn ai_page(cx: &App) -> SettingsPage { .use_modifier_to_send .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7620,7 +7591,7 @@ fn ai_page(cx: &App) -> SettingsPage { .message_editor_min_lines .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7638,7 +7609,7 @@ fn ai_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.show_turn_stats.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7656,7 +7627,7 @@ fn ai_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.agent.as_ref()?.show_merge_conflict_indicator.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .agent .get_or_insert_default() @@ -7682,7 +7653,7 @@ fn ai_page(cx: &App) -> SettingsPage { pick: |settings_content| { settings_content.project.context_server_timeout.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.project.context_server_timeout = value; }, }), @@ -7707,7 +7678,7 @@ fn ai_page(cx: &App) -> SettingsPage { .mode .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project .all_languages @@ -7743,7 +7714,7 @@ fn network_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("proxy"), pick: |settings_content| settings_content.proxy.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.proxy = value; }, }), @@ -7759,7 +7730,7 @@ fn network_page() -> SettingsPage { field: Box::new(SettingField { json_path: Some("server_url"), pick: |settings_content| settings_content.server_url.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.server_url = value; }, }), @@ -7827,7 +7798,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.tab_size.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.tab_size = value; }) @@ -7846,7 +7817,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.hard_tabs.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.hard_tabs = value; }) @@ -7865,7 +7836,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.auto_indent.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.auto_indent = value; }) @@ -7884,7 +7855,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.auto_indent_on_paste.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.auto_indent_on_paste = value; }) @@ -7909,7 +7880,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.soft_wrap.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.soft_wrap = value; }) @@ -7928,7 +7899,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.show_wrap_guides.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.show_wrap_guides = value; }) @@ -7947,7 +7918,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.preferred_line_length.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.preferred_line_length = value; }) @@ -7967,7 +7938,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.wrap_guides.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -7992,7 +7963,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.allow_rewrap.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.allow_rewrap = value; }) @@ -8020,7 +7991,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { .and_then(|indent_guides| indent_guides.enabled.as_ref()) }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.indent_guides.get_or_insert_default().enabled = value; }) @@ -8042,7 +8013,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { .and_then(|indent_guides| indent_guides.line_width.as_ref()) }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.indent_guides.get_or_insert_default().line_width = value; }) @@ -8064,7 +8035,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { .and_then(|indent_guides| indent_guides.active_line_width.as_ref()) }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .indent_guides @@ -8089,7 +8060,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { .and_then(|indent_guides| indent_guides.coloring.as_ref()) }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.indent_guides.get_or_insert_default().coloring = value; }) @@ -8110,7 +8081,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { }) }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .indent_guides @@ -8140,7 +8111,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.format_on_save.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8164,7 +8135,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.remove_trailing_whitespace_on_save.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.remove_trailing_whitespace_on_save = value; }) @@ -8183,7 +8154,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.ensure_final_newline_on_save.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.ensure_final_newline_on_save = value; }) @@ -8202,7 +8173,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.line_ending.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.line_ending = value; }) @@ -8225,7 +8196,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.formatter.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8250,7 +8221,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.use_on_type_format.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.use_on_type_format = value; }) @@ -8270,7 +8241,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.code_actions_on_format.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8301,7 +8272,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.use_autoclose.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.use_autoclose = value; }) @@ -8320,7 +8291,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.use_auto_surround.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.use_auto_surround = value; }) @@ -8339,7 +8310,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.always_treat_brackets_as_autoclosed.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.always_treat_brackets_as_autoclosed = value; }) @@ -8359,7 +8330,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.jsx_tag_auto_close.as_ref()?.enabled.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.jsx_tag_auto_close.get_or_insert_default().enabled = value; }) @@ -8384,7 +8355,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.show_whitespaces.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.show_whitespaces = value; }) @@ -8404,7 +8375,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.whitespace_map.as_ref()?.space.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8430,7 +8401,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.whitespace_map.as_ref()?.tab.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8461,7 +8432,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.show_completions_on_input.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.show_completions_on_input = value; }) @@ -8480,7 +8451,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.show_completion_documentation.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.show_completion_documentation = value; }) @@ -8499,7 +8470,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.completions.as_ref()?.words.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.completions.get_or_insert_default().words = value; }) @@ -8518,7 +8489,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.completions.as_ref()?.words_min_length.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .completions @@ -8538,7 +8509,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { pick: |settings_content| { settings_content.editor.completion_menu_scrollbar.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.completion_menu_scrollbar = value; }, }), @@ -8553,7 +8524,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { pick: |settings_content| { settings_content.editor.completion_detail_alignment.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.completion_detail_alignment = value; }, }), @@ -8576,7 +8547,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.enabled.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.inlay_hints.get_or_insert_default().enabled = value; }) @@ -8595,7 +8566,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.show_value_hints.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .inlay_hints @@ -8617,7 +8588,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.show_type_hints.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.inlay_hints.get_or_insert_default().show_type_hints = value; }) @@ -8636,7 +8607,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.show_parameter_hints.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .inlay_hints @@ -8658,7 +8629,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.show_other_hints.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .inlay_hints @@ -8680,7 +8651,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.show_background.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.inlay_hints.get_or_insert_default().show_background = value; }) @@ -8699,7 +8670,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.edit_debounce_ms.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .inlay_hints @@ -8721,7 +8692,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.inlay_hints.as_ref()?.scroll_debounce_ms.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .inlay_hints @@ -8750,7 +8721,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { .as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8784,7 +8755,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.tasks.as_ref()?.enabled.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.tasks.get_or_insert_default().enabled = value; }) @@ -8804,7 +8775,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.tasks.as_ref()?.variables.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8829,7 +8800,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.tasks.as_ref()?.prefer_lsp.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.tasks.get_or_insert_default().prefer_lsp = value; }) @@ -8854,7 +8825,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.word_diff_enabled.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.word_diff_enabled = value; }) @@ -8874,7 +8845,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.debuggers.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -8895,7 +8866,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { field: Box::new(SettingField { json_path: Some("languages.$(language).editor.middle_click_paste"), pick: |settings_content| settings_content.editor.middle_click_paste.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.middle_click_paste = value; }, }), @@ -8912,7 +8883,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.extend_comment_on_newline.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.extend_comment_on_newline = value; }) @@ -8931,7 +8902,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { language.colorize_brackets.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.colorize_brackets = value; }) @@ -8946,7 +8917,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { field: Box::new(SettingField { json_path: Some("modeline_lines"), pick: |settings_content| settings_content.modeline_lines.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.modeline_lines = value; }, }), @@ -8969,7 +8940,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { .as_ref() .and_then(|image_viewer| image_viewer.unit.as_ref()) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.image_viewer.get_or_insert_default().unit = value; }, }), @@ -8989,7 +8960,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { message_editor.auto_replace_emoji_shortcode.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .message_editor .get_or_insert_default() @@ -9005,7 +8976,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { field: Box::new(SettingField { json_path: Some("drop_target_size"), pick: |settings_content| settings_content.workspace.drop_target_size.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.workspace.drop_target_size = value; }, }), @@ -9023,7 +8994,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { field: Box::new(SettingField { json_path: Some("code_lens"), pick: |settings_content| settings_content.editor.code_lens.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.code_lens = value; }, }), @@ -9037,7 +9008,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { field: Box::new(SettingField { json_path: Some("lsp_document_colors"), pick: |settings_content| settings_content.editor.lsp_document_colors.as_ref(), - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.lsp_document_colors = value; }, }), @@ -9094,7 +9065,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.enable_language_server.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.enable_language_server = value; }) @@ -9114,7 +9085,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.language_servers.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -9139,7 +9110,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.linked_edits.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.linked_edits = value; }) @@ -9156,7 +9127,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { pick: |settings_content| { settings_content.editor.go_to_definition_fallback.as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content.editor.go_to_definition_fallback = value; }, }), @@ -9187,7 +9158,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { .semantic_tokens .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .project .all_languages @@ -9208,7 +9179,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.document_folding_ranges.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.document_folding_ranges = value; }) @@ -9227,7 +9198,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.document_symbols.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.document_symbols = value; }) @@ -9252,7 +9223,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.completions.as_ref()?.lsp.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.completions.get_or_insert_default().lsp = value; }) @@ -9271,7 +9242,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.completions.as_ref()?.lsp_fetch_timeout_ms.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language .completions @@ -9293,7 +9264,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.completions.as_ref()?.lsp_insert_mode.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.completions.get_or_insert_default().lsp_insert_mode = value; }) @@ -9319,7 +9290,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.debuggers.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -9350,7 +9321,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.prettier.as_ref()?.allowed.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.prettier.get_or_insert_default().allowed = value; }) @@ -9369,7 +9340,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.prettier.as_ref()?.parser.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.prettier.get_or_insert_default().parser = value; }) @@ -9389,7 +9360,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.prettier.as_ref()?.plugins.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -9415,7 +9386,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { language.prettier.as_ref()?.options.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut( settings_content, value, @@ -9441,7 +9412,7 @@ fn non_editor_language_settings_data() -> Box<[SettingsPageItem]> { ) } -fn edit_prediction_language_settings_section() -> [SettingsPageItem; 5] { +fn edit_prediction_language_settings_section() -> [SettingsPageItem; 4] { [ SettingsPageItem::SectionHeader("Edit Predictions"), SettingsPageItem::SubPageLink(SubPageLink { @@ -9453,32 +9424,6 @@ fn edit_prediction_language_settings_section() -> [SettingsPageItem; 5] { files: USER, render: render_edit_prediction_setup_page }), - SettingsPageItem::SettingItem(SettingItem { - title: "Data Collection", - description: "Controls whether Zed may collect training data when using Zed's Edit Predictions. Data is only collected for files in projects detected as open source. The default value uses the preference previously set via the status-bar toggle, or false if no preference has been stored.", - field: Box::new(SettingField { - json_path: Some("edit_predictions.allow_data_collection"), - pick: |settings_content| { - settings_content - .project - .all_languages - .edit_predictions - .as_ref()? - .allow_data_collection - .as_ref() - }, - write: |settings_content, value, _app| { - settings_content - .project - .all_languages - .edit_predictions - .get_or_insert_default() - .allow_data_collection = value; - }, - }), - metadata: None, - files: USER, - }), SettingsPageItem::SettingItem(SettingItem { title: "Show Edit Predictions", description: "Controls whether edit predictions are shown immediately or manually.", @@ -9489,7 +9434,7 @@ fn edit_prediction_language_settings_section() -> [SettingsPageItem; 5] { language.show_edit_predictions.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.show_edit_predictions = value; }) @@ -9509,7 +9454,7 @@ fn edit_prediction_language_settings_section() -> [SettingsPageItem; 5] { language.edit_predictions_disabled_in.as_ref() }) }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { language_settings_field_mut(settings_content, value, |language, value| { language.edit_predictions_disabled_in = value; }) @@ -9544,11 +9489,7 @@ where /// Updates the `vim_mode` setting, disabling `helix_mode` if present and /// `vim_mode` is being enabled. -fn write_vim_mode(settings: &mut SettingsContent, value: Option, _app: &App) { - write_vim_mode_inner(settings, value); -} - -fn write_vim_mode_inner(settings: &mut SettingsContent, value: Option) { +fn write_vim_mode(settings: &mut SettingsContent, value: Option) { if value == Some(true) && settings.helix_mode == Some(true) { settings.helix_mode = Some(false); } @@ -9557,11 +9498,7 @@ fn write_vim_mode_inner(settings: &mut SettingsContent, value: Option) { /// Updates the `helix_mode` setting, disabling `vim_mode` if present and /// `helix_mode` is being enabled. -fn write_helix_mode(settings: &mut SettingsContent, value: Option, _app: &App) { - write_helix_mode_inner(settings, value); -} - -fn write_helix_mode_inner(settings: &mut SettingsContent, value: Option) { +fn write_helix_mode(settings: &mut SettingsContent, value: Option) { if value == Some(true) && settings.vim_mode == Some(true) { settings.vim_mode = Some(false); } @@ -9577,38 +9514,38 @@ mod tests { // Enabling vim mode while `vim_mode` and `helix_mode` are not yet set // should only update the `vim_mode` setting. let mut settings = SettingsContent::default(); - write_vim_mode_inner(&mut settings, Some(true)); + write_vim_mode(&mut settings, Some(true)); assert_eq!(settings.vim_mode, Some(true)); assert_eq!(settings.helix_mode, None); // Enabling helix mode while `vim_mode` and `helix_mode` are not yet set // should only update the `helix_mode` setting. let mut settings = SettingsContent::default(); - write_helix_mode_inner(&mut settings, Some(true)); + write_helix_mode(&mut settings, Some(true)); assert_eq!(settings.helix_mode, Some(true)); assert_eq!(settings.vim_mode, None); // Disabling helix mode should only touch `helix_mode` setting when // `vim_mode` is not set. - write_helix_mode_inner(&mut settings, Some(false)); + write_helix_mode(&mut settings, Some(false)); assert_eq!(settings.helix_mode, Some(false)); assert_eq!(settings.vim_mode, None); // Enabling vim mode should update `vim_mode` but leave `helix_mode` // untouched. - write_vim_mode_inner(&mut settings, Some(true)); + write_vim_mode(&mut settings, Some(true)); assert_eq!(settings.vim_mode, Some(true)); assert_eq!(settings.helix_mode, Some(false)); // Enabling helix mode should update `helix_mode` and disable // `vim_mode`. - write_helix_mode_inner(&mut settings, Some(true)); + write_helix_mode(&mut settings, Some(true)); assert_eq!(settings.helix_mode, Some(true)); assert_eq!(settings.vim_mode, Some(false)); // Enabling vim mode should update `vim_mode` and disable // `helix_mode`. - write_vim_mode_inner(&mut settings, Some(true)); + write_vim_mode(&mut settings, Some(true)); assert_eq!(settings.vim_mode, Some(true)); assert_eq!(settings.helix_mode, Some(false)); } diff --git a/crates/settings_ui/src/pages/audio_input_output_setup.rs b/crates/settings_ui/src/pages/audio_input_output_setup.rs index 5730dd6fa2..7ae8f0c781 100644 --- a/crates/settings_ui/src/pages/audio_input_output_setup.rs +++ b/crates/settings_ui/src/pages/audio_input_output_setup.rs @@ -121,8 +121,8 @@ fn render_settings_audio_device_dropdown> + From Box<[SettingsPageItem]> { .api_url .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -406,7 +406,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> { .model .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -439,7 +439,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> { .prompt_format .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -469,7 +469,7 @@ fn ollama_settings() -> Box<[SettingsPageItem]> { .max_output_tokens .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -504,7 +504,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> { .api_url .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -537,7 +537,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> { .model .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -570,7 +570,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> { .prompt_format .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -600,7 +600,7 @@ fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> { .max_output_tokens .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -635,7 +635,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> { .api_url .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -668,7 +668,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> { .max_tokens .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages @@ -698,7 +698,7 @@ fn codestral_settings() -> Box<[SettingsPageItem]> { .model .as_ref() }, - write: |settings, value, _app: &App| { + write: |settings, value| { settings .project .all_languages diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index d6696cc703..cfc0906d5b 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -100,7 +100,7 @@ struct FocusFile(pub u32); struct SettingField { pick: fn(&SettingsContent) -> Option<&T>, - write: fn(&mut SettingsContent, Option, &App), + write: fn(&mut SettingsContent, Option), /// A json-path-like string that gives a unique-ish string that identifies /// where in the JSON the setting is defined. @@ -149,7 +149,7 @@ impl SettingField { fn unimplemented(self) -> SettingField { SettingField { pick: |_| Some(&UnimplementedSettingField), - write: |_, _, _| unreachable!(), + write: |_, _| unreachable!(), json_path: self.json_path, } } @@ -232,8 +232,8 @@ impl AnySettingField for SettingFi None, window, cx, - move |settings, app| { - (this.write)(settings, value_to_set, app); + move |settings, _| { + (this.write)(settings, value_to_set); }, ) // todo(settings_ui): Don't log err @@ -504,7 +504,6 @@ fn init_renderers(cx: &mut App) { .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) .add_basic_renderer::(render_editable_number_field) .add_basic_renderer::(render_editable_number_field) .add_basic_renderer::(render_editable_number_field) @@ -4091,8 +4090,8 @@ fn render_text_field + Into + AsRef + Clone>( field.json_path, window, cx, - move |settings, app| { - (field.write)(settings, new_text.map(Into::into), app); + move |settings, _cx| { + (field.write)(settings, new_text.map(Into::into)); }, ) .log_err(); // todo(settings_ui) don't log err @@ -4123,8 +4122,8 @@ fn render_toggle_button + From + Copy>( telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type()); let state = *state == ui::ToggleState::Selected; - update_settings_file(file.clone(), field.json_path, window, cx, move |settings, app| { - (field.write)(settings, Some(state.into()), app); + update_settings_file(file.clone(), field.json_path, window, cx, move |settings, _cx| { + (field.write)(settings, Some(state.into())); }) .log_err(); // todo(settings_ui) don't log err } @@ -4158,8 +4157,8 @@ fn render_editable_number_field( field.json_path, window, cx, - move |settings, app| { - (field.write)(settings, Some(value), app); + move |settings, _cx| { + (field.write)(settings, Some(value)); }, ) .log_err(); // todo(settings_ui) don't log err @@ -4198,8 +4197,8 @@ where field.json_path, window, cx, - move |settings, app| { - (field.write)(settings, Some(value), app); + move |settings, _cx| { + (field.write)(settings, Some(value)); }, ) .log_err(); // todo(settings_ui) don't log err @@ -4253,8 +4252,8 @@ fn render_font_picker( field.json_path, window, cx, - move |settings, app| { - (field.write)(settings, Some(font_name.to_string().into()), app); + move |settings, _cx| { + (field.write)(settings, Some(font_name.to_string().into())); }, ) .log_err(); // todo(settings_ui) don't log err @@ -4303,11 +4302,10 @@ fn render_theme_picker( field.json_path, window, cx, - move |settings, app| { + move |settings, _cx| { (field.write)( settings, Some(settings::ThemeName(theme_name.into())), - app, ); }, ) @@ -4357,11 +4355,10 @@ fn render_icon_theme_picker( field.json_path, window, cx, - move |settings, app| { + move |settings, _cx| { (field.write)( settings, Some(settings::IconThemeName(theme_name.into())), - app, ); }, ) diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index 9224edc3be..2de895114d 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -6141,45 +6141,6 @@ async fn test_archive_thread_keeps_metadata_but_hides_from_sidebar(cx: &mut Test }); } -#[gpui::test] -async fn test_archive_thread_drops_retained_conversation_view(cx: &mut TestAppContext) { - let project = init_test_project_with_agent_panel("/project-a", cx).await; - let (multi_workspace, cx) = - cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); - let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx); - cx.run_until_parked(); - - let connection = acp_thread::StubAgentConnection::new(); - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Done".into()), - )]); - open_thread_with_connection(&panel, connection, cx); - send_message(&panel, cx); - let session_id = active_session_id(&panel, cx); - let thread_id = active_thread_id(&panel, cx); - cx.run_until_parked(); - - sidebar.read_with(cx, |sidebar, _| { - assert!( - is_active_session(sidebar, &session_id), - "expected the newly created thread to be active before archiving", - ); - }); - - sidebar.update_in(cx, |sidebar, window, cx| { - sidebar.archive_thread(&session_id, window, cx); - }); - cx.run_until_parked(); - - panel.read_with(cx, |panel, _| { - assert!( - !panel.is_retained_thread(&thread_id), - "archiving a thread must drop its ConversationView from retained_threads, \ - but the archived thread id {thread_id:?} is still retained", - ); - }); -} - #[gpui::test] async fn test_archive_thread_active_entry_management(cx: &mut TestAppContext) { // Tests two archive scenarios: diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 1f6082e0bf..8a598c1d77 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -43,7 +43,6 @@ url.workspace = true util.workspace = true urlencoding.workspace = true parking_lot.workspace = true -percent-encoding.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs index 65b1f5d82f..649c3bdee3 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/terminal_hyperlinks.rs @@ -138,10 +138,6 @@ pub(super) fn find_from_grid_point( if let Ok(url) = Url::parse(&maybe_url_or_path) { if let Ok(path) = url.to_file_path_ext(path_style) { return (path.to_string_lossy().into_owned(), false, word_match); - } else if let Some(path) = try_osc8_url_to_path(url) - && path_style.is_posix() - { - return (path, false, word_match); } } // Fallback: strip file:// prefix if URL parsing fails @@ -158,22 +154,6 @@ pub(super) fn find_from_grid_point( }) } -// OSC 8 mandates that file:// URIs must be encoded as file://{host}{path} -// We need to skip the {host} part if it's set -fn try_osc8_url_to_path(url: url::Url) -> Option { - use percent_encoding::percent_decode; - if url.scheme() != "file" { - return None; - } - - let bytes = url - .path_segments()? - .skip(1) - .flat_map(|segment| percent_decode(segment.as_bytes())) - .collect::>(); - bytes.try_into().ok() -} - fn sanitize_url_punctuation( url: String, url_match: Match, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index aba0040f48..271f95bea2 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -489,7 +489,6 @@ impl TerminalView { pub fn deploy_context_menu( &mut self, position: Point, - has_selection: bool, window: &mut Window, cx: &mut Context, ) { @@ -498,6 +497,13 @@ impl TerminalView { .upgrade() .and_then(|workspace| workspace.read(cx).panel::(cx)) .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled()); + let has_selection = self + .terminal + .read(cx) + .last_content + .selection_text + .as_ref() + .is_some_and(|text| !text.is_empty()); let context_menu = ContextMenu::build(window, cx, |menu, _, _| { menu.context(self.focus_handle.clone()) .action("New Terminal", Box::new(NewTerminal::default())) @@ -1243,21 +1249,12 @@ impl Render for TerminalView { MouseButton::Right, cx.listener(|this, event: &MouseDownEvent, window, cx| { if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) { - let had_selection = this.terminal.read(cx).last_content.selection.is_some(); - if !had_selection { + if this.terminal.read(cx).last_content.selection.is_none() { this.terminal.update(cx, |terminal, _| { terminal.select_word_at_event_position(event); }); - } - let has_selection = !had_selection - || this - .terminal - .read(cx) - .last_content - .selection_text - .as_ref() - .is_some_and(|text| !text.is_empty()); - this.deploy_context_menu(event.position, has_selection, window, cx); + }; + this.deploy_context_menu(event.position, window, cx); cx.notify(); } }), diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 14e1df3884..fce0e54c72 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -176,7 +176,6 @@ impl ThemeColors { vim_visual_line_background: system.transparent, vim_visual_block_background: system.transparent, vim_yank_background: neutral().light_alpha().step_3(), - vim_helix_jump_label_foreground: red().light().step_9(), vim_helix_normal_background: system.transparent, vim_helix_select_background: system.transparent, vim_normal_foreground: system.transparent, @@ -323,7 +322,6 @@ impl ThemeColors { vim_visual_line_background: system.transparent, vim_visual_block_background: system.transparent, vim_yank_background: neutral().dark_alpha().step_4(), - vim_helix_jump_label_foreground: red().dark().step_9(), vim_helix_normal_background: system.transparent, vim_helix_select_background: system.transparent, vim_normal_foreground: system.transparent, diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index 22a2c73704..a739df3213 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -260,7 +260,6 @@ pub(crate) fn zed_default_dark() -> Theme { vim_visual_line_background: SystemColors::default().transparent, vim_visual_block_background: SystemColors::default().transparent, vim_yank_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.2), - vim_helix_jump_label_foreground: red, vim_helix_normal_background: SystemColors::default().transparent, vim_helix_select_background: SystemColors::default().transparent, vim_normal_foreground: SystemColors::default().transparent, diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 63ccdacca7..75ba8ea391 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -177,8 +177,6 @@ pub struct ThemeColors { pub vim_visual_block_background: Hsla, /// Background color for Vim yank highlight. pub vim_yank_background: Hsla, - /// Foreground color for Helix jump labels. - pub vim_helix_jump_label_foreground: Hsla, /// Background color for Vim Helix Normal mode indicator. pub vim_helix_normal_background: Hsla, /// Background color for Vim Helix Select mode indicator. diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs index 725881f8cc..be8e5e01d0 100644 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -26,18 +26,7 @@ impl Focusable for IconThemeSelector { } } -impl ModalView for IconThemeSelector { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - cx: &mut Context, - ) -> workspace::DismissDecision { - self.picker.update(cx, |picker, cx| { - picker.delegate.revert_theme(cx); - }); - workspace::DismissDecision::Dismiss(true) - } -} +impl ModalView for IconThemeSelector {} impl IconThemeSelector { pub fn new( @@ -143,13 +132,6 @@ impl IconThemeSelectorDelegate { .unwrap_or(self.selected_index); } - fn revert_theme(&mut self, cx: &mut App) { - if !self.selection_completed { - Self::set_icon_theme(self.original_theme.clone(), cx); - self.selection_completed = true; - } - } - fn set_icon_theme(name: IconThemeName, cx: &mut App) { SettingsStore::update_global(cx, |store, _| { let mut theme_settings = store.get::(None).clone(); @@ -203,7 +185,10 @@ impl PickerDelegate for IconThemeSelectorDelegate { } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.revert_theme(cx); + if !self.selection_completed { + Self::set_icon_theme(self.original_theme.clone(), cx); + self.selection_completed = true; + } self.selector .update(cx, |_, cx| cx.emit(DismissEvent)) diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index c60218003d..8a0e835fa8 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -79,18 +79,7 @@ fn toggle_icon_theme_selector( }); } -impl ModalView for ThemeSelector { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - cx: &mut Context, - ) -> workspace::DismissDecision { - self.picker.update(cx, |picker, cx| { - picker.delegate.revert_theme(cx); - }); - workspace::DismissDecision::Dismiss(true) - } -} +impl ModalView for ThemeSelector {} struct ThemeSelector { picker: Entity>, @@ -226,15 +215,6 @@ impl ThemeSelectorDelegate { } } - fn revert_theme(&mut self, cx: &mut App) { - if !self.selection_completed { - SettingsStore::update_global(cx, |store, _| { - store.override_global(self.original_theme_settings.clone()); - }); - self.selection_completed = true; - } - } - fn set_theme(&mut self, new_theme: Arc, cx: &mut App) { // Update the global (in-memory) theme settings. SettingsStore::update_global(cx, |store, _| { @@ -391,7 +371,12 @@ impl PickerDelegate for ThemeSelectorDelegate { } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.revert_theme(cx); + if !self.selection_completed { + SettingsStore::update_global(cx, |store, _| { + store.override_global(self.original_theme_settings.clone()); + }); + self.selection_completed = true; + } self.selector .update(cx, |_, cx| cx.emit(DismissEvent)) diff --git a/crates/theme_settings/src/schema.rs b/crates/theme_settings/src/schema.rs index 76c2e2a8b2..93eb4d30aa 100644 --- a/crates/theme_settings/src/schema.rs +++ b/crates/theme_settings/src/schema.rs @@ -775,11 +775,6 @@ pub fn theme_colors_refinement( .as_ref() .and_then(|color| try_parse_color(color).ok()) .or(editor_document_highlight_read_background), - vim_helix_jump_label_foreground: this - .vim_helix_jump_label_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(status_colors.error), vim_helix_normal_background: this .vim_helix_normal_background .as_ref() @@ -853,39 +848,3 @@ fn try_parse_color(color: &str) -> anyhow::Result { Ok(hsla) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn helix_jump_label_color_uses_theme_color_or_status_error() { - let status_error = try_parse_color("#e63333").expect("valid color"); - let override_color = try_parse_color("#00ff00").expect("valid color"); - let status_colors = StatusColorsRefinement { - error: Some(status_error), - ..Default::default() - }; - - let fallback_refinement = - theme_colors_refinement(&ThemeColorsContent::default(), &status_colors); - - assert_eq!( - fallback_refinement.vim_helix_jump_label_foreground, - Some(status_error) - ); - - let override_refinement = theme_colors_refinement( - &ThemeColorsContent { - vim_helix_jump_label_foreground: Some("#00ff00".to_string()), - ..Default::default() - }, - &status_colors, - ); - - assert_eq!( - override_refinement.vim_helix_jump_label_foreground, - Some(override_color) - ); - } -} diff --git a/crates/theme_settings/src/settings.rs b/crates/theme_settings/src/settings.rs index 08b8532a35..7b8261d27b 100644 --- a/crates/theme_settings/src/settings.rs +++ b/crates/theme_settings/src/settings.rs @@ -96,17 +96,11 @@ pub(crate) struct UiFontSize(Pixels); impl Global for UiFontSize {} -/// In-memory override for the UI font size in the agent panel. +/// In-memory override for the font size in the agent panel. #[derive(Default)] -pub struct AgentUiFontSize(Pixels); +pub struct AgentFontSize(Pixels); -impl Global for AgentUiFontSize {} - -/// In-memory override for the buffer font size in the agent panel. -#[derive(Default)] -pub struct AgentBufferFontSize(Pixels); - -impl Global for AgentBufferFontSize {} +impl Global for AgentFontSize {} /// Represents the selection of a theme, which can be either static or dynamic. #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -384,7 +378,7 @@ impl ThemeSettings { /// Returns the agent panel font size. Falls back to the UI font size if unset. pub fn agent_ui_font_size(&self, cx: &App) -> Pixels { - cx.try_global::() + cx.try_global::() .map(|size| size.0) .or(self.agent_ui_font_size) .map(clamp_font_size) @@ -393,7 +387,7 @@ impl ThemeSettings { /// Returns the agent panel buffer font size. pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels { - cx.try_global::() + cx.try_global::() .map(|size| size.0) .or(self.agent_buffer_font_size) .map(clamp_font_size) @@ -549,16 +543,16 @@ pub fn reset_ui_font_size(cx: &mut App) { pub fn adjust_agent_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let agent_ui_font_size = ThemeSettings::get_global(cx).agent_ui_font_size(cx); let adjusted_size = cx - .try_global::() + .try_global::() .map_or(agent_ui_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(AgentUiFontSize(clamp_font_size(f(adjusted_size)))); + cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } /// Resets the agent response font size in the agent panel to the default value. pub fn reset_agent_ui_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); + if cx.has_global::() { + cx.remove_global::(); cx.refresh_windows(); } } @@ -567,16 +561,16 @@ pub fn reset_agent_ui_font_size(cx: &mut App) { pub fn adjust_agent_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { let agent_buffer_font_size = ThemeSettings::get_global(cx).agent_buffer_font_size(cx); let adjusted_size = cx - .try_global::() + .try_global::() .map_or(agent_buffer_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(AgentBufferFontSize(clamp_font_size(f(adjusted_size)))); + cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); cx.refresh_windows(); } /// Resets the user message font size in the agent panel to the default value. pub fn reset_agent_buffer_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); + if cx.has_global::() { + cx.remove_global::(); cx.refresh_windows(); } } diff --git a/crates/theme_settings/src/theme_settings.rs b/crates/theme_settings/src/theme_settings.rs index 9be00af475..39ffe83274 100644 --- a/crates/theme_settings/src/theme_settings.rs +++ b/crates/theme_settings/src/theme_settings.rs @@ -28,12 +28,12 @@ pub use crate::schema::{ }; use crate::settings::adjust_buffer_font_size; pub use crate::settings::{ - AgentBufferFontSize, AgentUiFontSize, BufferLineHeight, FontFamilyName, IconThemeName, - IconThemeSelection, ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, - adjust_agent_buffer_font_size, adjust_agent_ui_font_size, adjust_ui_font_size, - adjusted_font_size, appearance_to_mode, clamp_font_size, default_theme, - observe_buffer_font_size_adjustment, reset_agent_buffer_font_size, reset_agent_ui_font_size, - reset_buffer_font_size, reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font, + AgentFontSize, BufferLineHeight, FontFamilyName, IconThemeName, IconThemeSelection, + ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings, adjust_agent_buffer_font_size, + adjust_agent_ui_font_size, adjust_ui_font_size, adjusted_font_size, appearance_to_mode, + clamp_font_size, default_theme, observe_buffer_font_size_adjustment, + reset_agent_buffer_font_size, reset_agent_ui_font_size, reset_buffer_font_size, + reset_ui_font_size, set_icon_theme, set_mode, set_theme, setup_ui_font, }; pub use theme::UiDensity; diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 8edfca6734..edc21f91f6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -32,12 +32,9 @@ use gpui::{ pulsating_between, }; use onboarding_banner::OnboardingBanner; -use project::{ - Project, git_store::GitStoreEvent, project_settings::ProjectSettings, - trusted_worktrees::TrustedWorktrees, -}; +use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees}; use remote::RemoteConnectionOptions; -use settings::Settings as _; +use settings::Settings; use std::sync::Arc; use std::time::Duration; @@ -178,7 +175,6 @@ impl Render for TitleBar { let title_bar_settings = *TitleBarSettings::get_global(cx); let button_layout = title_bar_settings.button_layout; - let is_git_enabled = ProjectSettings::get_global(cx).git.enabled.status; let show_menus = show_menus(cx); @@ -235,9 +231,9 @@ impl Render for TitleBar { .child(self.render_project_name(project_name, window, cx)) }) .when_some( - repository.filter(|_| is_git_enabled), + repository.filter(|_| title_bar_settings.show_branch_name), |title_bar, repository| { - title_bar.children(self.render_worktree_and_branch( + title_bar.children(self.render_project_branch( repository, linked_worktree_name, cx, @@ -833,7 +829,7 @@ impl TitleBar { .anchor(gpui::Anchor::TopLeft) } - fn render_worktree_and_branch( + fn render_project_branch( &self, repository: Entity, linked_worktree_name: Option, @@ -878,6 +874,7 @@ impl TitleBar { (branch_name, icon_info, is_detached_head) }; + let branch_name = branch_name?; let settings = TitleBarSettings::get_global(cx); let effective_repository = Some(repository); @@ -938,77 +935,66 @@ impl TitleBar { .anchor(gpui::Anchor::TopLeft) }; - let branch_picker = branch_name.and_then(|branch_name| { - settings.show_branch_name.then(|| { - let branch_tooltip_label = branch_name.clone(); - let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon { - icon_info - } else { - (IconName::GitBranch, Color::Muted) - }; + let branch_tooltip_label = branch_name.clone(); + let (branch_icon, branch_icon_color) = if settings.show_branch_status_icon { + icon_info + } else { + (IconName::GitBranch, Color::Muted) + }; - let trigger = if is_detached_head { - Button::new("project_branch_trigger", "Create Branch") - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .start_icon( - Icon::new(IconName::GitBranchPlus) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - } else { - Button::new("project_branch_trigger", branch_name) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .color(Color::Muted) - .start_icon( - Icon::new(branch_icon) - .size(IconSize::XSmall) - .color(branch_icon_color), - ) - }; + let trigger = if is_detached_head { + Button::new("project_branch_trigger", "Create Branch") + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .label_size(LabelSize::Small) + .start_icon( + Icon::new(IconName::GitBranchPlus) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + } else { + Button::new("project_branch_trigger", branch_name) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .label_size(LabelSize::Small) + .color(Color::Muted) + .start_icon( + Icon::new(branch_icon) + .size(IconSize::XSmall) + .color(branch_icon_color), + ) + }; - PopoverMenu::new("branch-menu") - .menu(move |window, cx| { - Some(git_ui::git_picker::popover( - workspace.downgrade(), - effective_repository.clone(), - git_ui::git_picker::GitPickerTab::Branches, - gpui::rems(34.), - window, - cx, - )) - }) - .trigger_with_tooltip(trigger, move |_window, cx| { - let meta = if is_detached_head { - format!("Detached HEAD: {}", branch_tooltip_label) - } else { - format!("Currently Checked Out: {}", branch_tooltip_label) - }; - Tooltip::with_meta( - "Branch & Stash", - Some(&zed_actions::git::Branch), - meta, - cx, - ) - }) - .anchor(gpui::Anchor::TopLeft) + let git_picker_button = PopoverMenu::new("branch-menu") + .menu(move |window, cx| { + Some(git_ui::git_picker::popover( + workspace.downgrade(), + effective_repository.clone(), + git_ui::git_picker::GitPickerTab::Branches, + gpui::rems(34.), + window, + cx, + )) }) - }); + .trigger_with_tooltip(trigger, move |_window, cx| { + let meta = if is_detached_head { + format!("Detached HEAD: {}", branch_tooltip_label) + } else { + format!("Currently Checked Out: {}", branch_tooltip_label) + }; + Tooltip::with_meta("Branch & Stash", Some(&zed_actions::git::Branch), meta, cx) + }) + .anchor(gpui::Anchor::TopLeft); Some( h_flex() .gap_px() .child(worktree_button) - .when_some(branch_picker, |this, branch_picker| { - this.child( - Label::new("/") - .size(LabelSize::Small) - .color(Color::Muted) - .alpha(0.25), - ) - .child(branch_picker) - }) + .child( + Label::new("/") + .size(LabelSize::Small) + .color(Color::Muted) + .alpha(0.25), + ) + .child(git_picker_button) .into_any_element(), ) } diff --git a/crates/util/src/rel_path.rs b/crates/util/src/rel_path.rs index 7b17094e08..8916236925 100644 --- a/crates/util/src/rel_path.rs +++ b/crates/util/src/rel_path.rs @@ -254,14 +254,6 @@ impl RelPath { #[derive(Debug)] pub struct StripPrefixError; -impl std::fmt::Display for StripPrefixError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("prefix not found") - } -} - -impl std::error::Error for StripPrefixError {} - impl ToOwned for RelPath { type Owned = RelPathBuf; diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index eebad4d438..fe25852682 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -5,29 +5,27 @@ mod paste; mod select; mod surround; -use editor::display_map::{DisplayRow, DisplaySnapshot}; +use editor::display_map::DisplaySnapshot; use editor::{ DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - NavigationOverlayLabel, NavigationTargetOverlay, SelectionEffects, ToOffset, ToPoint, movement, + SelectionEffects, ToOffset, ToPoint, movement, }; use gpui::actions; -use gpui::{App, Context, Font, Hsla, Pixels, Window, WindowTextSystem}; -use language::{CharClassifier, CharKind, Point, Selection}; -use multi_buffer::MultiBufferSnapshot; +use gpui::{Context, Window}; +use language::{CharClassifier, CharKind, Point}; use search::{BufferSearchBar, SearchOptions}; use settings::Settings; use text::{Bias, SelectionGoal}; -use theme::ActiveTheme as _; -use ui::px; -use workspace::searchable::{self, Direction, FilteredSearchRange}; +use workspace::searchable::FilteredSearchRange; +use workspace::searchable::{self, Direction}; use crate::motion::{self, MotionKind}; -use crate::state::{HelixJumpBehaviour, HelixJumpLabel, Mode, Operator, SearchState}; +use crate::state::{Operator, SearchState}; use crate::{ PushHelixSurroundAdd, PushHelixSurroundDelete, PushHelixSurroundReplace, Vim, motion::{Motion, right}, + state::Mode, }; -use std::ops::Range; actions!( vim, @@ -57,8 +55,6 @@ actions!( HelixSubstitute, /// Delete the selection and enter edit mode, without yanking the selection. HelixSubstituteNoYank, - /// Activate Helix-style word jump labels. - HelixJumpToWord, /// Select the next match for the current search query. HelixSelectNext, /// Select the previous match for the current search query. @@ -86,7 +82,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); Vim::action(editor, cx, Vim::helix_substitute); Vim::action(editor, cx, Vim::helix_substitute_no_yank); - Vim::action(editor, cx, Vim::helix_jump_to_word); Vim::action(editor, cx, Vim::helix_select_next); Vim::action(editor, cx, Vim::helix_select_previous); Vim::action(editor, cx, |vim, _: &PushHelixSurroundAdd, window, cx| { @@ -977,876 +972,20 @@ impl Vim { }); } } - - pub fn helix_jump_to_word( - &mut self, - _: &HelixJumpToWord, - window: &mut Window, - cx: &mut Context, - ) { - let behaviour = if self.mode.is_visual() { - HelixJumpBehaviour::Extend - } else { - HelixJumpBehaviour::Move - }; - self.start_helix_jump(behaviour, window, cx); - } - - fn start_helix_jump( - &mut self, - behaviour: HelixJumpBehaviour, - window: &mut Window, - cx: &mut Context, - ) { - let is_visual = self.mode.is_visual(); - let Some(data) = self.collect_helix_jump_data(is_visual, window, cx) else { - return; - }; - - if data.labels.is_empty() { - self.clear_helix_jump_ui(window, cx); - return; - } - - if !self.apply_helix_jump_ui(data.overlays, window, cx) { - return; - } - - self.push_operator( - Operator::HelixJump { - behaviour, - first_char: None, - labels: data.labels, - }, - window, - cx, - ); - } - - fn collect_helix_jump_data( - &mut self, - is_visual: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let display_snapshot = &snapshot.display_snapshot; - let buffer_snapshot = display_snapshot.buffer_snapshot(); - let visible_range = Self::visible_jump_range(editor, &snapshot, display_snapshot, cx); - let start_offset = buffer_snapshot.point_to_offset(visible_range.start); - let end_offset = buffer_snapshot.point_to_offset(visible_range.end); - - let selections = editor.selections.all::(&display_snapshot); - let skip_data = Self::selection_skip_offsets(buffer_snapshot, &selections, is_visual); - - // Get the primary cursor position for alternating forward/backward labeling - let cursor_offset = selections - .first() - .map(|s| buffer_snapshot.point_to_offset(s.head())) - .unwrap_or(start_offset); - - let style = editor.style(cx); - let font = style.text.font(); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let label_color = cx.theme().colors().vim_helix_jump_label_foreground; - - Self::build_helix_jump_ui_data( - buffer_snapshot, - start_offset, - end_offset, - cursor_offset, - label_color, - &skip_data, - window.text_system(), - font, - font_size, - ) - }) - } - - fn visible_jump_range( - editor: &Editor, - snapshot: &editor::EditorSnapshot, - display_snapshot: &DisplaySnapshot, - cx: &App, - ) -> Range { - let visible_range = editor.multi_buffer_visible_range(display_snapshot, cx); - if editor.visible_line_count().is_some() || visible_range.start != visible_range.end { - return visible_range; - } - - let scroll_position = snapshot.scroll_position(); - let top_row = scroll_position.y.floor().max(0.0) as u32; - let visible_rows = display_snapshot - .max_point() - .row() - .0 - .saturating_sub(top_row) - .saturating_add(1); - let start_display_point = DisplayPoint::new(DisplayRow(top_row), 0); - let end_display_point = - DisplayPoint::new(DisplayRow(top_row.saturating_add(visible_rows)), 0); - - display_snapshot.display_point_to_point(start_display_point, Bias::Left) - ..display_snapshot.display_point_to_point(end_display_point, Bias::Right) - } - - fn build_helix_jump_ui_data( - buffer: &MultiBufferSnapshot, - start_offset: MultiBufferOffset, - end_offset: MultiBufferOffset, - cursor_offset: MultiBufferOffset, - label_color: Hsla, - skip_data: &HelixJumpSkipData, - text_system: &WindowTextSystem, - font: Font, - font_size: Pixels, - ) -> HelixJumpUiData { - if start_offset >= end_offset { - return HelixJumpUiData::default(); - } - - // First pass: collect all word candidates without assigning labels - let candidates = Self::collect_jump_candidates(buffer, start_offset, end_offset, skip_data); - - if candidates.is_empty() { - return HelixJumpUiData::default(); - } - - let ordered_candidates = Self::order_jump_candidates(candidates, cursor_offset); - - // Now assign labels and build UI data - let mut labels = Vec::with_capacity(ordered_candidates.len()); - let mut overlays = Vec::with_capacity(ordered_candidates.len()); - - let width_of = |text: &str| -> Pixels { - if text.is_empty() { - return px(0.0); - } - - let run = gpui::TextRun { - len: text.len(), - font: font.clone(), - color: Hsla::default(), - background_color: None, - underline: None, - strikethrough: None, - }; - - text_system.layout_line(text, font_size, &[run], None).width - }; - - let is_monospace = Self::is_monospace_jump_font(text_system, &font, font_size); - - for (label_index, candidate) in ordered_candidates.into_iter().enumerate() { - let start_anchor = buffer.anchor_after(candidate.word_start); - let end_anchor = buffer.anchor_after(candidate.word_end); - let label = Self::jump_label_for_index(label_index); - let label_text = label.iter().collect::(); - // Monospace fonts: the label always matches the width of the first two characters, - // so no per-word measurement is needed. - // Proportional fonts: a label like "mw" can be wider than a short word like "if", - // so we hide enough of the word (and possibly trailing whitespace) to make room, - // or shift the label left into preceding whitespace. - let fit = if is_monospace { - JumpLabelFit::monospace(candidate.first_two_end) - } else { - let label_width = width_of(&label_text); - Self::fit_proportional_jump_label( - buffer, - &candidate, - end_offset, - label_width, - &width_of, - ) - }; - - let hide_end_anchor = buffer.anchor_after(fit.hide_end_offset); - - labels.push(HelixJumpLabel { - label, - range: start_anchor..end_anchor, - }); - - overlays.push(NavigationTargetOverlay { - target_range: start_anchor..end_anchor, - label: NavigationOverlayLabel { - text: label_text.into(), - text_color: label_color, - x_offset: -fit.left_shift, - scale_factor: fit.scale_factor, - }, - covered_text_range: Some(start_anchor..hide_end_anchor), - }); - } - - HelixJumpUiData { labels, overlays } - } - - fn collect_jump_candidates( - buffer: &MultiBufferSnapshot, - start_offset: MultiBufferOffset, - end_offset: MultiBufferOffset, - skip_data: &HelixJumpSkipData, - ) -> Vec { - let mut candidates = Vec::new(); - - let mut offset = start_offset; - let mut in_word = false; - let mut word_start = start_offset; - let mut first_two_end = start_offset; - let mut char_count = 0; - - for chunk in buffer.text_for_range(start_offset..end_offset) { - for (idx, ch) in chunk.char_indices() { - let absolute = offset + idx; - let is_word = is_jump_word_char(ch); - if is_word { - if !in_word { - in_word = true; - word_start = absolute; - char_count = 0; - } - if char_count == 1 { - first_two_end = absolute + ch.len_utf8(); - } - char_count += 1; - } - - if !is_word && in_word { - if char_count >= 2 - && !Self::should_skip_jump_candidate(word_start, absolute, skip_data) - { - candidates.push(JumpCandidate { - word_start, - word_end: absolute, - first_two_end, - }); - } - in_word = false; - } - } - offset += chunk.len(); - } - - // Handle word at end of buffer - if in_word - && char_count >= 2 - && !Self::should_skip_jump_candidate(word_start, end_offset, skip_data) - { - candidates.push(JumpCandidate { - word_start, - word_end: end_offset, - first_two_end, - }); - } - - candidates - } - - fn selection_skip_offsets( - buffer: &MultiBufferSnapshot, - selections: &[Selection], - is_visual: bool, - ) -> HelixJumpSkipData { - let mut skip_points = Vec::with_capacity(selections.len()); - let mut skip_ranges = Vec::new(); - - for selection in selections { - let head_offset = buffer.point_to_offset(selection.head()); - skip_points.push(head_offset); - - // In visual mode, don't skip ranges so we can shrink the selection - if !is_visual && selection.start != selection.end { - let mut start = buffer.point_to_offset(selection.start); - let mut end = buffer.point_to_offset(selection.end); - if start > end { - std::mem::swap(&mut start, &mut end); - } - skip_ranges.push(start..end); - } - } - - skip_points.sort_unstable(); - - skip_ranges.sort_unstable_by_key(|range| range.start); - let mut merged_ranges: Vec> = - Vec::with_capacity(skip_ranges.len()); - for range in skip_ranges { - if let Some(previous_range) = merged_ranges.last_mut() - && range.start <= previous_range.end - { - previous_range.end = previous_range.end.max(range.end); - } else { - merged_ranges.push(range); - } - } - - HelixJumpSkipData { - points: skip_points, - ranges: merged_ranges, - } - } - - fn should_skip_jump_candidate( - word_start: MultiBufferOffset, - word_end: MultiBufferOffset, - skip_data: &HelixJumpSkipData, - ) -> bool { - // word_end is exclusive, so points at the following delimiter should not skip the word. - let point_index = skip_data - .points - .partition_point(|offset| *offset < word_start); - if skip_data - .points - .get(point_index) - .is_some_and(|offset| *offset < word_end) - { - return true; - } - - let range_index = skip_data - .ranges - .partition_point(|range| range.end <= word_start); - skip_data - .ranges - .get(range_index) - .is_some_and(|range| range.start < word_end) - } - - /// Interleave candidates so forward targets get even label indices (aa, ac, ae...) - /// and backward targets get odd indices (ab, ad, af...), matching Helix's algorithm. - /// This keeps the earliest label assignments close to the cursor in both directions. - fn order_jump_candidates( - candidates: Vec, - cursor_offset: MultiBufferOffset, - ) -> Vec { - let mut forward = Vec::with_capacity(candidates.len()); - let mut backward = Vec::new(); - - for candidate in candidates { - if candidate.word_start < cursor_offset { - backward.push(candidate); - } else { - forward.push(candidate); - } - } - - backward.reverse(); - - let mut ordered_candidates = - Vec::with_capacity((forward.len() + backward.len()).min(HELIX_JUMP_LABEL_LIMIT)); - let mut forward_candidates = forward.into_iter(); - let mut backward_candidates = backward.into_iter(); - - loop { - let mut pushed_candidate = false; - - if ordered_candidates.len() < HELIX_JUMP_LABEL_LIMIT - && let Some(candidate) = forward_candidates.next() - { - ordered_candidates.push(candidate); - pushed_candidate = true; - } - - if ordered_candidates.len() < HELIX_JUMP_LABEL_LIMIT - && let Some(candidate) = backward_candidates.next() - { - ordered_candidates.push(candidate); - pushed_candidate = true; - } - - if !pushed_candidate { - break; - } - } - - ordered_candidates - } - - fn jump_label_for_index(index: usize) -> [char; 2] { - [ - HELIX_JUMP_ALPHABET[index / HELIX_JUMP_ALPHABET.len()], - HELIX_JUMP_ALPHABET[index % HELIX_JUMP_ALPHABET.len()], - ] - } - - fn is_monospace_jump_font( - text_system: &WindowTextSystem, - font: &Font, - font_size: Pixels, - ) -> bool { - let font_id = text_system.resolve_font(font); - let width_of_char = |ch| { - text_system - .advance(font_id, font_size, ch) - .map(|size| size.width) - .unwrap_or_else(|_| text_system.layout_width(font_id, font_size, ch)) - }; - - let a = width_of_char('i'); - let b = width_of_char('w'); - let c = width_of_char('0'); - let d = width_of_char('1'); - let diff_1 = if a > b { a - b } else { b - a }; - let diff_2 = if c > d { c - d } else { d - c }; - diff_1 <= HELIX_JUMP_MONOSPACE_TOLERANCE && diff_2 <= HELIX_JUMP_MONOSPACE_TOLERANCE - } - - /// Fit a jump label over a word in a proportional font. - /// - /// Prefer fitting within the word itself, using available whitespace to the left - /// before consuming trailing whitespace after the word. If the label still cannot - /// fit cleanly, allow a small amount of scaling. - fn fit_proportional_jump_label Pixels>( - buffer: &MultiBufferSnapshot, - candidate: &JumpCandidate, - end_offset: MultiBufferOffset, - label_width: Pixels, - width_of: &F, - ) -> JumpLabelFit { - let fit_budget = Self::jump_label_fit_budget(buffer, candidate, end_offset, width_of); - - let mut hidden_prefix = HiddenPrefixFitState::new(candidate.first_two_end); - let min_label_scale = if fit_budget.preserve_full_scale { - 1.0 - } else { - HELIX_JUMP_MIN_LABEL_SCALE - }; - - hidden_prefix.extend_to_fit( - buffer, - candidate.word_start, - candidate.word_end, - candidate.word_end, - label_width, - fit_budget.max_left_shift, - min_label_scale, - width_of, - ); - - if label_width > px(0.0) - && hidden_prefix.needs_more_width(label_width, fit_budget.max_left_shift) - && fit_budget.allowed_trailing_hide_end > candidate.word_end - { - hidden_prefix.extend_to_fit( - buffer, - candidate.word_end, - fit_budget.allowed_trailing_hide_end, - candidate.word_end, - label_width, - fit_budget.max_left_shift, - min_label_scale, - width_of, - ); - } - - // Jump candidates always contain at least two word characters, and the initial - // scan above always measures through that second character before we read the width. - let hidden_width = hidden_prefix.hidden_width; - - let left_shift = if label_width > hidden_width { - (label_width - hidden_width).min(fit_budget.max_left_shift) - } else { - px(0.0) - }; - - let scale_factor = if label_width > px(0.0) { - let scale = ((hidden_width + left_shift) / label_width).min(1.0); - if scale < 1.0 { scale * 0.99 } else { 1.0 } - } else { - 1.0 - }; - - JumpLabelFit { - hide_end_offset: hidden_prefix.hide_end_offset, - left_shift, - scale_factor: if fit_budget.preserve_full_scale { - 1.0 - } else { - scale_factor - }, - } - } - - fn jump_label_fit_budget Pixels>( - buffer: &MultiBufferSnapshot, - candidate: &JumpCandidate, - end_offset: MultiBufferOffset, - width_of: &F, - ) -> JumpLabelFitBudget { - let mut left_ws_rev = String::new(); - let mut left_ws_count = 0usize; - let mut left_stopped_at_line_break = false; - let mut left_stopped_at_non_ws = false; - let mut left_hit_limit = false; - - for ch in buffer.reversed_chars_at(candidate.word_start) { - if ch == '\n' || ch == '\r' { - left_stopped_at_line_break = true; - break; - } - - if !ch.is_whitespace() { - left_stopped_at_non_ws = true; - break; - } - - left_ws_count += 1; - if left_ws_count > HELIX_JUMP_MAX_LEFT_WS_CHARS { - left_hit_limit = true; - break; - } - - left_ws_rev.push(ch); - } - - let left_ws: String = left_ws_rev.chars().rev().collect(); - let left_ws_width = width_of(&left_ws); - let left_is_indentation = - left_stopped_at_line_break || (!left_stopped_at_non_ws && !left_hit_limit); - // Between tokens leave a small gap so the label doesn't touch the previous word; - // for line-leading indentation the full width is safe. - let min_left_gap = if left_is_indentation { - px(0.0) - } else { - px(2.0) - }; - let max_left_shift = (left_ws_width - min_left_gap).max(px(0.0)); - - let mut allowed_trailing_hide_end = candidate.word_end; - let mut ws_count = 0usize; - let mut last_ws_start = candidate.word_end; - let mut ws_end_offset = candidate.word_end; - let mut next_non_ws = None; - let mut hit_line_break_after_word = false; - - let mut ws_scan_offset = candidate.word_end; - 'scan: for chunk in buffer.text_for_range(candidate.word_end..end_offset) { - for (idx, ch) in chunk.char_indices() { - let absolute = ws_scan_offset + idx; - if ch == '\n' || ch == '\r' { - hit_line_break_after_word = true; - break 'scan; - } - if !ch.is_whitespace() { - next_non_ws = Some(ch); - break 'scan; - } - - ws_count += 1; - last_ws_start = absolute; - ws_end_offset = absolute + ch.len_utf8(); - } - ws_scan_offset += chunk.len(); - } - - let preserve_full_scale = hit_line_break_after_word && next_non_ws.is_none() - || matches!( - buffer.chars_at(candidate.word_end).next(), - None | Some('\n') | Some('\r') - ); - - if ws_count > 0 { - let next_is_word = match next_non_ws { - Some(ch) => is_jump_word_char(ch), - None => false, - }; - - if next_is_word { - // Keep at least one whitespace character visible so adjacent labels - // remain visually separated. - if ws_count > 1 { - allowed_trailing_hide_end = last_ws_start; - } - } else { - // Next token is punctuation or end-of-range — safe to hide all whitespace. - allowed_trailing_hide_end = ws_end_offset; - } - } - - JumpLabelFitBudget { - max_left_shift, - allowed_trailing_hide_end, - preserve_full_scale, - } - } -} - -const HELIX_JUMP_ALPHABET: &[char; 26] = &[ - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', - 't', 'u', 'v', 'w', 'x', 'y', 'z', -]; -const HELIX_JUMP_LABEL_LIMIT: usize = HELIX_JUMP_ALPHABET.len() * HELIX_JUMP_ALPHABET.len(); -const HELIX_JUMP_MONOSPACE_TOLERANCE: Pixels = px(0.5); -const HELIX_JUMP_MIN_LABEL_SCALE: f32 = 1.0; -const HELIX_JUMP_MAX_HIDDEN_CHARS: usize = 16; -const HELIX_JUMP_MAX_LEFT_WS_CHARS: usize = 32; - -fn is_jump_word_char(ch: char) -> bool { - ch == '_' || ch.is_alphanumeric() -} - -/// A word candidate for jump labels, before label assignment. -#[derive(Clone)] -struct JumpCandidate { - word_start: MultiBufferOffset, - word_end: MultiBufferOffset, - first_two_end: MultiBufferOffset, -} - -struct HelixJumpSkipData { - points: Vec, - ranges: Vec>, -} - -struct JumpLabelFit { - hide_end_offset: MultiBufferOffset, - left_shift: Pixels, - scale_factor: f32, -} - -struct JumpLabelFitBudget { - max_left_shift: Pixels, - allowed_trailing_hide_end: MultiBufferOffset, - preserve_full_scale: bool, -} - -struct HiddenPrefixFitState { - text: String, - hide_end_offset: MultiBufferOffset, - hidden_width: Pixels, - total_char_count: usize, - word_char_count: usize, -} - -impl JumpLabelFit { - fn monospace(hide_end_offset: MultiBufferOffset) -> Self { - Self { - hide_end_offset, - left_shift: px(0.0), - scale_factor: 1.0, - } - } -} - -impl HiddenPrefixFitState { - fn new(hide_end_offset: MultiBufferOffset) -> Self { - Self { - text: String::new(), - hide_end_offset, - hidden_width: px(0.0), - total_char_count: 0, - word_char_count: 0, - } - } - - fn needs_more_width(&self, label_width: Pixels, max_left_shift: Pixels) -> bool { - (self.hidden_width + max_left_shift) / label_width < HELIX_JUMP_MIN_LABEL_SCALE - } - - fn extend_to_fit Pixels>( - &mut self, - buffer: &MultiBufferSnapshot, - range_start: MultiBufferOffset, - range_end: MultiBufferOffset, - word_end: MultiBufferOffset, - label_width: Pixels, - max_left_shift: Pixels, - min_label_scale: f32, - width_of: &F, - ) { - let mut offset = range_start; - for chunk in buffer.text_for_range(range_start..range_end) { - for (idx, ch) in chunk.char_indices() { - let absolute = offset + idx; - - self.total_char_count += 1; - if self.total_char_count > HELIX_JUMP_MAX_HIDDEN_CHARS { - return; - } - - self.text.push(ch); - let end_offset = absolute + ch.len_utf8(); - - if absolute < word_end && is_jump_word_char(ch) { - self.word_char_count += 1; - } - - if self.word_char_count < 2 { - continue; - } - - self.hide_end_offset = end_offset; - self.hidden_width = width_of(self.text.as_str()); - - let effective_width = self.hidden_width + max_left_shift; - let scale_needed = if label_width > px(0.0) { - (effective_width / label_width).min(1.0) - } else { - 1.0 - }; - - if scale_needed >= min_label_scale { - return; - } - } - offset += chunk.len(); - } - } -} - -#[derive(Default)] -struct HelixJumpUiData { - labels: Vec, - overlays: Vec, } #[cfg(test)] mod test { - use std::{fmt::Write, time::Duration}; - - use editor::{HighlightKey, MultiBufferOffset}; use gpui::{KeyBinding, UpdateGlobal, VisualTestContext}; use indoc::indoc; - use language::Point; use project::FakeFs; use search::{ProjectSearchView, project_search}; use serde_json::json; - use settings::{SettingsStore, ThemeColorsContent, ThemeStyleContent}; - use theme::ActiveTheme as _; + use settings::SettingsStore; use util::path; use workspace::{DeploySearch, MultiWorkspace}; - use super::HELIX_JUMP_LABEL_LIMIT; - use crate::{ - HELIX_JUMP_OVERLAY_KEY, Vim, VimAddon, - state::{Mode, Operator}, - test::VimTestContext, - }; - - fn active_helix_jump_labels(cx: &mut VimTestContext) -> Vec<(String, String)> { - cx.update_editor(|editor, window, cx| { - let labels = match editor - .addon::() - .unwrap() - .entity - .read(cx) - .operator_stack - .last() - .cloned() - { - Some(Operator::HelixJump { labels, .. }) => labels, - other => panic!("expected active HelixJump operator, got {other:?}"), - }; - - let snapshot = editor.snapshot(window, cx); - let buffer_snapshot = snapshot.display_snapshot.buffer_snapshot(); - - labels - .into_iter() - .map(|label| { - let jump_label = label.label.iter().collect::(); - let word = buffer_snapshot - .text_for_range(label.range) - .collect::(); - (jump_label, word) - }) - .collect() - }) - } - - fn helix_jump_label_for_word(cx: &mut VimTestContext, target_word: &str) -> String { - active_helix_jump_labels(cx) - .into_iter() - .find_map(|(label, word)| (word == target_word).then_some(label)) - .unwrap_or_else(|| { - let mut message = String::new(); - let labels = active_helix_jump_labels(cx); - let _ = write!( - &mut message, - "expected jump label for word {target_word:?}, available labels: {labels:?}" - ); - panic!("{message}"); - }) - } - - fn jump_to_word(cx: &mut VimTestContext, target_word: &str) { - cx.simulate_keystrokes("g w"); - - let label = helix_jump_label_for_word(cx, target_word); - - let mut chars = label.chars(); - let first = chars.next().expect("jump labels are two characters long"); - let second = chars.next().expect("jump labels are two characters long"); - cx.simulate_keystrokes(&format!("{first} {second}")); - } - - fn active_helix_jump_overlay_counts(cx: &mut VimTestContext) -> (usize, usize) { - let covered_text_range_count = cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - snapshot - .text_highlight_ranges(HighlightKey::NavigationOverlay(HELIX_JUMP_OVERLAY_KEY)) - .map(|ranges| ranges.as_ref().clone().1.len()) - .unwrap_or_default() - }); - let label_count = match cx.active_operator() { - Some(Operator::HelixJump { labels, .. }) => labels.len(), - _ => 0, - }; - - (covered_text_range_count, label_count) - } - - fn assert_helix_jump_cleared(cx: &mut VimTestContext, expected_overlay_counts: (usize, usize)) { - assert_eq!(cx.active_operator(), None); - assert_eq!( - active_helix_jump_overlay_counts(cx), - expected_overlay_counts, - "expected Helix jump UI to be fully cleared" - ); - } - - fn helix_jump_labels_for_full_buffer(cx: &mut VimTestContext) -> Vec<(String, String)> { - cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let display_snapshot = &snapshot.display_snapshot; - let buffer_snapshot = display_snapshot.buffer_snapshot(); - let selections = editor.selections.all::(display_snapshot); - let skip_data = Vim::selection_skip_offsets(buffer_snapshot, &selections, false); - let cursor_offset = selections - .first() - .map(|selection| buffer_snapshot.point_to_offset(selection.head())) - .unwrap_or(MultiBufferOffset(0)); - let style = editor.style(cx); - let font = style.text.font(); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let label_color = cx.theme().colors().vim_helix_jump_label_foreground; - let data = Vim::build_helix_jump_ui_data( - buffer_snapshot, - MultiBufferOffset(0), - buffer_snapshot.len(), - cursor_offset, - label_color, - &skip_data, - window.text_system(), - font, - font_size, - ); - - data.labels - .into_iter() - .map(|label| { - let jump_label = label.label.iter().collect::(); - let word = buffer_snapshot - .text_for_range(label.range) - .collect::(); - (jump_label, word) - }) - .collect() - }) - } + use crate::{VimAddon, state::Mode, test::VimTestContext}; #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { @@ -3247,374 +2386,6 @@ mod test { ); } - #[gpui::test] - async fn test_helix_jump_starts_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇhello world\njump labels", Mode::HelixNormal); - - cx.simulate_keystrokes("g w"); - - assert!( - matches!(cx.active_operator(), Some(Operator::HelixJump { .. })), - "expected HelixJump operator to be active" - ) - } - - #[gpui::test] - async fn test_helix_jump_cancels_on_escape(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇhello world\njump labels", Mode::HelixNormal); - let overlay_counts = active_helix_jump_overlay_counts(&mut cx); - - cx.simulate_keystrokes("g w"); - cx.simulate_keystrokes("escape"); - - cx.assert_state("ˇhello world\njump labels", Mode::HelixNormal); - assert_helix_jump_cleared(&mut cx, overlay_counts); - } - - #[gpui::test] - async fn test_helix_jump_cancels_on_invalid_first_char(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇalpha beta gamma", Mode::HelixNormal); - let overlay_counts = active_helix_jump_overlay_counts(&mut cx); - - cx.simulate_keystrokes("g w"); - cx.simulate_keystrokes("z"); - - cx.assert_state("ˇalpha beta gamma", Mode::HelixNormal); - assert_helix_jump_cleared(&mut cx, overlay_counts); - } - - #[gpui::test] - async fn test_helix_jump_cancels_on_invalid_second_char(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇalpha beta gamma", Mode::HelixNormal); - let overlay_counts = active_helix_jump_overlay_counts(&mut cx); - - cx.simulate_keystrokes("g w"); - cx.simulate_keystrokes("a z"); - - cx.assert_state("ˇalpha beta gamma", Mode::HelixNormal); - assert_helix_jump_cleared(&mut cx, overlay_counts); - } - - #[gpui::test] - async fn test_helix_jump_keeps_full_overlay_after_first_key(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - let text = format!( - "ˇ{}", - (0..28) - .map(|index| format!("w{index:02}")) - .collect::>() - .join(" ") - ); - cx.set_state(&text, Mode::HelixNormal); - - cx.simulate_keystrokes("g w"); - let labels = active_helix_jump_labels(&mut cx); - let initial_overlay_counts = active_helix_jump_overlay_counts(&mut cx); - let first_group = labels - .first() - .and_then(|(label, _)| label.chars().next()) - .expect("expected at least one helix jump label"); - let next_group = labels - .iter() - .filter_map(|(label, _)| label.chars().next()) - .find(|ch| *ch != first_group) - .expect("expected labels spanning more than one first-character group"); - - cx.simulate_keystrokes(&next_group.to_string()); - - assert_eq!( - active_helix_jump_overlay_counts(&mut cx), - initial_overlay_counts - ); - assert!( - matches!( - cx.active_operator(), - Some(Operator::HelixJump { - first_char: Some(ch), - .. - }) if ch == next_group - ), - "expected HelixJump operator to keep the first typed label character" - ); - } - - #[gpui::test] - async fn test_helix_jump_includes_word_before_cursor_boundary(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("oneˇ two three", Mode::HelixNormal); - - jump_to_word(&mut cx, "one"); - - cx.assert_state("«oneˇ» two three", Mode::HelixNormal); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_skips_single_char_words(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇa bb c dd e", Mode::HelixNormal); - - let words = helix_jump_labels_for_full_buffer(&mut cx) - .into_iter() - .map(|(_, word)| word) - .collect::>(); - - assert_eq!(words, vec!["bb".to_string(), "dd".to_string()]); - } - - #[gpui::test] - async fn test_helix_jump_handles_underscored_words(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("baz quxˇ foo_bar _private", Mode::HelixNormal); - - let words = helix_jump_labels_for_full_buffer(&mut cx) - .into_iter() - .map(|(_, word)| word) - .collect::>(); - - assert!(words.iter().any(|word| word == "foo_bar")); - assert!(words.iter().any(|word| word == "_private")); - assert!(!words.iter().any(|word| word == "foo")); - assert!(!words.iter().any(|word| word == "bar")); - } - - #[gpui::test] - async fn test_helix_jump_at_end_of_buffer(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("alpha beta gammaˇ", Mode::HelixNormal); - - jump_to_word(&mut cx, "gamma"); - - cx.assert_state("alpha beta «gammaˇ»", Mode::HelixNormal); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_moves_to_target_word(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇone two three", Mode::HelixNormal); - - jump_to_word(&mut cx, "three"); - - cx.assert_state("one two «threeˇ»", Mode::HelixNormal); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_extends_selection_forward(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("one «twoˇ» three four", Mode::HelixSelect); - - jump_to_word(&mut cx, "four"); - - cx.assert_state("one «two three fourˇ»", Mode::HelixSelect); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_extends_selection_backward_from_forward_selection( - cx: &mut gpui::TestAppContext, - ) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("one «twoˇ» three four", Mode::HelixSelect); - - jump_to_word(&mut cx, "one"); - - cx.assert_state("«ˇone two» three four", Mode::HelixSelect); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_extends_reversed_selection_backward(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("one two «ˇthree» four", Mode::HelixSelect); - - jump_to_word(&mut cx, "one"); - - cx.assert_state("«ˇone two three» four", Mode::HelixSelect); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_prioritizes_nearby_targets_before_truncating( - cx: &mut gpui::TestAppContext, - ) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - let cursor_index = 850usize; - let target_word = format!("w{:03}", cursor_index + 1); - let early_word = "w010".to_string(); - let text = (0..900usize) - .map(|index| { - let word = format!("w{index:03}"); - if index == cursor_index { - format!("ˇ{word}") - } else { - word - } - }) - .collect::>() - .join(" "); - cx.set_state(&text, Mode::HelixNormal); - - let labels = helix_jump_labels_for_full_buffer(&mut cx); - - assert_eq!(labels.len(), HELIX_JUMP_LABEL_LIMIT); - assert!( - labels.iter().any(|(_, word)| word == &target_word), - "expected nearby target {target_word:?} to survive truncation" - ); - assert!( - !labels.iter().any(|(_, word)| word == &early_word), - "expected distant early target {early_word:?} to be truncated first" - ); - } - - #[gpui::test] - async fn test_helix_jump_label_ordering_alternates_directions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("aaa bbb ccc ˇddd eee fff ggg", Mode::HelixNormal); - - let first_labels = helix_jump_labels_for_full_buffer(&mut cx) - .into_iter() - .take(6) - .collect::>(); - - assert_eq!( - first_labels, - vec![ - ("aa".to_string(), "eee".to_string()), - ("ab".to_string(), "ccc".to_string()), - ("ac".to_string(), "fff".to_string()), - ("ad".to_string(), "bbb".to_string()), - ("ae".to_string(), "ggg".to_string()), - ("af".to_string(), "aaa".to_string()), - ] - ); - } - - #[gpui::test] - async fn test_helix_jump_uses_theme_label_color(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.theme.experimental_theme_overrides = Some(ThemeStyleContent { - colors: ThemeColorsContent { - vim_helix_jump_label_foreground: Some("#00ff00".to_string()), - ..Default::default() - }, - ..Default::default() - }); - }); - }); - }); - cx.executor().advance_clock(Duration::from_millis(200)); - cx.run_until_parked(); - - let configured_label_color = - cx.update(|_, cx| cx.theme().colors().vim_helix_jump_label_foreground); - assert_ne!( - configured_label_color, - cx.update(|_, cx| cx.theme().status().error) - ); - cx.set_state("ˇalpha beta gamma", Mode::HelixNormal); - - let label_colors = cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let display_snapshot = &snapshot.display_snapshot; - let buffer_snapshot = display_snapshot.buffer_snapshot(); - let selections = editor.selections.all::(display_snapshot); - let skip_data = Vim::selection_skip_offsets(buffer_snapshot, &selections, false); - let cursor_offset = selections - .first() - .map(|selection| buffer_snapshot.point_to_offset(selection.head())) - .unwrap_or(MultiBufferOffset(0)); - let style = editor.style(cx); - let font = style.text.font(); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let data = Vim::build_helix_jump_ui_data( - buffer_snapshot, - MultiBufferOffset(0), - buffer_snapshot.len(), - cursor_offset, - configured_label_color, - &skip_data, - window.text_system(), - font, - font_size, - ); - - data.overlays - .into_iter() - .map(|overlay| overlay.label.text_color) - .collect::>() - }); - - assert!(!label_colors.is_empty()); - assert!( - label_colors - .into_iter() - .all(|color| color == configured_label_color) - ); - } - - #[gpui::test] - async fn test_helix_jump_input_is_case_insensitive(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇone two three", Mode::HelixNormal); - - cx.simulate_keystrokes("g w"); - let label = helix_jump_label_for_word(&mut cx, "three"); - let mut chars = label.chars(); - let first = chars - .next() - .expect("jump labels are two characters long") - .to_ascii_uppercase(); - let second = chars - .next() - .expect("jump labels are two characters long") - .to_ascii_uppercase(); - - cx.simulate_keystrokes(&format!("{first} {second}")); - - cx.assert_state("one two «threeˇ»", Mode::HelixNormal); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_helix_jump_with_unicode_words(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state("ˇcafé résumé naïve", Mode::HelixNormal); - - jump_to_word(&mut cx, "naïve"); - - cx.assert_state("café résumé «naïveˇ»", Mode::HelixNormal); - assert_eq!(cx.active_operator(), None); - } - #[gpui::test] async fn test_project_search_opens_in_normal_mode(cx: &mut gpui::TestAppContext) { VimTestContext::init(cx); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 3bd4c0a5b8..0442387c39 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -155,23 +155,6 @@ pub enum Operator { replaced_char: Option, }, HelixSurroundDelete, - HelixJump { - behaviour: HelixJumpBehaviour, - first_char: Option, - labels: Vec, - }, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct HelixJumpLabel { - pub label: [char; 2], - pub range: Range, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum HelixJumpBehaviour { - Move, - Extend, } #[derive(Default, Clone, Debug)] @@ -1102,7 +1085,6 @@ impl Operator { Operator::HelixMatch => "helix_m", Operator::HelixNext { .. } => "helix_next", Operator::HelixPrevious { .. } => "helix_previous", - Operator::HelixJump { .. } => "gw", Operator::HelixSurroundAdd => "helix_ms", Operator::HelixSurroundReplace { .. } => "helix_mr", Operator::HelixSurroundDelete => "helix_md", @@ -1130,7 +1112,6 @@ impl Operator { Operator::HelixMatch => "m".to_string(), Operator::HelixNext { .. } => "]".to_string(), Operator::HelixPrevious { .. } => "[".to_string(), - Operator::HelixJump { .. } => "gw".to_string(), Operator::HelixSurroundAdd => "ms".to_string(), Operator::HelixSurroundReplace { replaced_char: None, @@ -1161,8 +1142,7 @@ impl Operator { | Operator::ChangeSurrounds { target: Some(_), .. } - | Operator::DeleteSurrounds - | Operator::HelixJump { .. } => true, + | Operator::DeleteSurrounds => true, Operator::Change | Operator::Delete | Operator::Yank @@ -1233,8 +1213,7 @@ impl Operator { | Operator::Register | Operator::RecordRegister | Operator::ReplayRegister - | Operator::HelixMatch - | Operator::HelixJump { .. } => false, + | Operator::HelixMatch => false, } } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a76846fd33..d9617d34ec 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -23,7 +23,7 @@ use crate::normal::paste::Paste as VimPaste; use collections::HashMap; use editor::{ Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - NavigationOverlayKey, NavigationTargetOverlay, SelectionEffects, + SelectionEffects, actions::Paste, display_map::ToDisplayPoint, movement::{self, FindRange}, @@ -46,9 +46,7 @@ use settings::RegisterSetting; pub use settings::{ ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file, }; -use state::{ - HelixJumpBehaviour, HelixJumpLabel, Mode, Operator, RecordedSelection, SearchState, VimGlobals, -}; +use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; use std::{mem, ops::Range, sync::Arc}; use surrounds::SurroundsType; use theme_settings::ThemeSettings; @@ -62,11 +60,6 @@ use crate::{ state::ReplayableAction, }; -enum HelixJumpNavigationOverlay {} - -pub(crate) const HELIX_JUMP_OVERLAY_KEY: NavigationOverlayKey = - NavigationOverlayKey::unique::(); - /// Number is used to manage vim's count. Pushing a digit /// multiplies the current value by 10 and adds the digit. #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] @@ -1740,9 +1733,6 @@ impl Vim { } fn clear_operator(&mut self, window: &mut Window, cx: &mut Context) { - if matches!(self.active_operator(), Some(Operator::HelixJump { .. })) { - self.clear_helix_jump_ui(window, cx); - } Vim::take_count(cx); Vim::take_forced_motion(cx); self.selected_register.take(); @@ -1750,110 +1740,6 @@ impl Vim { self.sync_vim_settings(window, cx); } - fn clear_helix_jump_ui(&mut self, _window: &mut Window, cx: &mut Context) { - self.update_editor(cx, move |_, editor, cx| { - editor.clear_navigation_overlays(HELIX_JUMP_OVERLAY_KEY, cx); - }); - } - - fn apply_helix_jump_ui( - &mut self, - overlays: Vec, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.clear_helix_jump_ui(window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.set_navigation_overlays(HELIX_JUMP_OVERLAY_KEY, overlays, cx); - }) - .is_some() - } - - fn handle_helix_jump_input( - &mut self, - operator: Operator, - input_char: char, - window: &mut Window, - cx: &mut Context, - ) { - let Operator::HelixJump { - behaviour, - first_char, - labels, - } = operator - else { - return; - }; - - let input = input_char.to_ascii_lowercase(); - self.pop_operator(window, cx); - - if let Some(first) = first_char { - let first = first.to_ascii_lowercase(); - if let Some(candidate) = labels.into_iter().find(|label| { - label.label[0].eq_ignore_ascii_case(&first) - && label.label[1].eq_ignore_ascii_case(&input) - }) { - self.finish_helix_jump(candidate, behaviour, window, cx); - } else { - self.clear_helix_jump_ui(window, cx); - } - } else { - if !labels - .iter() - .any(|label| label.label[0].eq_ignore_ascii_case(&input)) - { - self.clear_helix_jump_ui(window, cx); - return; - } - - self.push_operator( - Operator::HelixJump { - behaviour, - first_char: Some(input), - labels, - }, - window, - cx, - ); - } - } - - fn finish_helix_jump( - &mut self, - candidate: HelixJumpLabel, - behaviour: HelixJumpBehaviour, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| match behaviour { - HelixJumpBehaviour::Move => { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges([candidate.range.clone()]) - }); - } - HelixJumpBehaviour::Extend => { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(&mut |map, selection| { - let word_start = candidate.range.start.to_display_point(map); - let word_end = candidate.range.end.to_display_point(map); - let tail = selection.tail(); - - if word_start >= tail { - // Jumping forward: extend head to end of target word - selection.set_head(word_end, SelectionGoal::None); - } else { - // Jumping backward: extend backward while keeping current extent - // Use current end as tail to preserve the selection - selection.set_head_tail(word_start, selection.end, SelectionGoal::None); - } - }); - }); - } - }); - self.clear_helix_jump_ui(window, cx); - } - fn active_operator(&self) -> Option { self.operator_stack.last().cloned() } @@ -2034,11 +1920,6 @@ impl Vim { self.push_operator(Operator::SneakBackward { first_char }, window, cx); } } - Some(operator @ Operator::HelixJump { .. }) => { - if let Some(input_char) = text.chars().next() { - self.handle_helix_jump_input(operator, input_char, window, cx); - } - } Some(Operator::Replace) => match self.mode { Mode::Normal => self.normal_replace(text, window, cx), Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index fbaa1b50a0..e5a9d5ce0f 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -9,6 +9,7 @@ use gpui::{ use std::any::TypeId; use theme::CLIENT_SIDE_DECORATION_ROUNDING; use ui::{Divider, Indicator, Tooltip, prelude::*}; +use util::ResultExt; pub trait StatusItemView: Render { /// Event callback that is triggered when the active pane item changes. @@ -245,7 +246,7 @@ impl StatusBar { self.left_items .iter() .chain(self.right_items.iter()) - .find_map(|item| item.to_any().downcast().ok()) + .find_map(|item| item.to_any().downcast().log_err()) } pub fn position_of_item(&self) -> Option diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 4a85e91c5a..5e658659e2 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -29,7 +29,7 @@ use gpui::{ Task, }; use ignore::IgnoreStack; -use language::{ByteContent, DiskState, FILE_ANALYSIS_BYTES, analyze_byte_content}; +use language::DiskState; use parking_lot::Mutex; use paths::{local_settings_folder_name, local_vscode_folder_name}; @@ -6337,6 +6337,8 @@ impl fs::Watcher for NullWatcher { } } +const FILE_ANALYSIS_BYTES: usize = 1024; + async fn decode_file_text( fs: &dyn Fs, abs_path: &Path, @@ -6451,6 +6453,163 @@ fn decode_byte_full( } } +#[derive(Debug, PartialEq)] +enum ByteContent { + Utf16Le, + Utf16Be, + Binary, + Unknown, +} + +// Heuristic check using null byte distribution plus a generic text-likeness +// heuristic. This prefers UTF-16 when many bytes are NUL and otherwise +// distinguishes between text-like and binary-like content. +fn analyze_byte_content(bytes: &[u8]) -> ByteContent { + if bytes.len() < 2 { + return ByteContent::Unknown; + } + + if is_known_binary_header(bytes) { + return ByteContent::Binary; + } + + let limit = bytes.len().min(FILE_ANALYSIS_BYTES); + let mut even_null_count = 0usize; + let mut odd_null_count = 0usize; + let mut non_text_like_count = 0usize; + + for (i, &byte) in bytes[..limit].iter().enumerate() { + if byte == 0 { + if i % 2 == 0 { + even_null_count += 1; + } else { + odd_null_count += 1; + } + non_text_like_count += 1; + continue; + } + + let is_text_like = match byte { + b'\t' | b'\n' | b'\r' | 0x0C => true, + 0x20..=0x7E => true, + // Treat bytes that are likely part of UTF-8 or single-byte encodings as text-like. + 0x80..=0xBF | 0xC2..=0xF4 => true, + _ => false, + }; + + if !is_text_like { + non_text_like_count += 1; + } + } + + let total_null_count = even_null_count + odd_null_count; + + // If there are no NUL bytes at all, this is overwhelmingly likely to be text. + if total_null_count == 0 { + return ByteContent::Unknown; + } + + let has_significant_nulls = total_null_count >= limit / 16; + let nulls_skew_to_even = even_null_count > odd_null_count * 4; + let nulls_skew_to_odd = odd_null_count > even_null_count * 4; + + if has_significant_nulls { + let sample = &bytes[..limit]; + + // UTF-16BE ASCII: [0x00, char] — nulls at even positions (high byte first) + // UTF-16LE ASCII: [char, 0x00] — nulls at odd positions (low byte first) + + if nulls_skew_to_even && is_plausible_utf16_text(sample, false) { + return ByteContent::Utf16Be; + } + + if nulls_skew_to_odd && is_plausible_utf16_text(sample, true) { + return ByteContent::Utf16Le; + } + + return ByteContent::Binary; + } + + if non_text_like_count * 100 < limit * 8 { + ByteContent::Unknown + } else { + ByteContent::Binary + } +} + +fn is_known_binary_header(bytes: &[u8]) -> bool { + bytes.starts_with(b"%PDF-") // PDF + || bytes.starts_with(b"PK\x03\x04") // ZIP local header + || bytes.starts_with(b"PK\x05\x06") // ZIP end of central directory + || bytes.starts_with(b"PK\x07\x08") // ZIP spanning/splitting + || bytes.starts_with(b"\x89PNG\r\n\x1a\n") // PNG + || bytes.starts_with(b"\xFF\xD8\xFF") // JPEG + || bytes.starts_with(b"GIF87a") // GIF87a + || bytes.starts_with(b"GIF89a") // GIF89a + || bytes.starts_with(b"IWAD") // Doom IWAD archive + || bytes.starts_with(b"PWAD") // Doom PWAD archive + || bytes.starts_with(b"RIFF") // WAV, AVI, WebP + || bytes.starts_with(b"OggS") // OGG (Vorbis, Opus, FLAC) + || bytes.starts_with(b"fLaC") // FLAC + || bytes.starts_with(b"ID3") // MP3 with ID3v2 tag + || bytes.starts_with(b"\xFF\xFB") // MP3 frame sync (MPEG1 Layer3) + || bytes.starts_with(b"\xFF\xFA") // MP3 frame sync (MPEG1 Layer3) + || bytes.starts_with(b"\xFF\xF3") // MP3 frame sync (MPEG2 Layer3) + || bytes.starts_with(b"\xFF\xF2") // MP3 frame sync (MPEG2 Layer3) +} + +// Null byte skew alone is not enough to identify UTF-16 -- binary formats with +// small 16-bit values (like PCM audio) produce the same pattern. Decode the +// bytes as UTF-16 and reject if too many code units land in control character +// ranges or form unpaired surrogates, which real text almost never contains. +fn is_plausible_utf16_text(bytes: &[u8], little_endian: bool) -> bool { + let mut suspicious_count = 0usize; + let mut total = 0usize; + + let mut i = 0; + while let Some(code_unit) = read_u16(bytes, i, little_endian) { + total += 1; + + match code_unit { + 0x0009 | 0x000A | 0x000C | 0x000D => {} + // C0/C1 control characters and non-characters + 0x0000..=0x001F | 0x007F..=0x009F | 0xFFFE | 0xFFFF => suspicious_count += 1, + 0xD800..=0xDBFF => { + let next_offset = i + 2; + let has_low_surrogate = read_u16(bytes, next_offset, little_endian) + .is_some_and(|next| (0xDC00..=0xDFFF).contains(&next)); + if has_low_surrogate { + total += 1; + i += 2; + } else { + suspicious_count += 1; + } + } + // Lone low surrogate without a preceding high surrogate + 0xDC00..=0xDFFF => suspicious_count += 1, + _ => {} + } + + i += 2; + } + + if total == 0 { + return false; + } + + // Real UTF-16 text has near-zero control characters; binary data with + // small 16-bit values typically exceeds 5%. 2% provides a safe margin. + suspicious_count * 100 < total * 2 +} + +fn read_u16(bytes: &[u8], offset: usize, little_endian: bool) -> Option { + let pair = [*bytes.get(offset)?, *bytes.get(offset + 1)?]; + if little_endian { + return Some(u16::from_le_bytes(pair)); + } + Some(u16::from_be_bytes(pair)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 68542f285a..59b0a5b1ce 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -38,7 +38,6 @@ - [Finding & Navigating](./finding-navigating.md) - [Command Palette](./command-palette.md) - [Outline Panel](./outline-panel.md) - - [Project Panel](./project-panel.md) - [Tab Switcher](./tab-switcher.md) - [Running & Testing](./running-testing.md) - [Terminal](./terminal.md) diff --git a/docs/src/ai/parallel-agents.md b/docs/src/ai/parallel-agents.md index f858d747de..f7d007f0f3 100644 --- a/docs/src/ai/parallel-agents.md +++ b/docs/src/ai/parallel-agents.md @@ -39,8 +39,6 @@ You can search your threads in history; search will fuzzy match on thread titles If you have external agents installed, Zed will detect whether you have existing threads and invite you to import them into Zed. Once you open Thread History, you'll find an import icon button in the Thread History toolbar that lets you import threads at any time. Clicking on it opens a modal where you can select the agents whose threads you want to import. -> **Note:** Thread import is subject to agent support. Some agents (such as Cursor and Gemini CLI) are not currently supported. - ## Running Multiple Threads {#running-multiple-threads} Each thread runs independently, so you can send a prompt, open a second thread, and give it a different task while the first continues working. To scope a new thread to a specific project, hover over that project's header in the Threads Sidebar and click the `+` button, or use {#action agents_sidebar::NewThreadInGroup} from the keyboard. See [Creating New Threads](./agent-panel.md#new-thread) for the other entry points. @@ -55,11 +53,10 @@ To add another project to the sidebar, click the **Add Project** button (open-fo ### Multi-Root Folder Projects {#multi-root-folder-projects} -A single project can contain multiple folders (a multi-root folder project). Agents can then read and write across all of those folders in a single thread. There are multiple ways to set one up: +A single project can contain multiple folders (a multi-root folder project). Agents can then read and write across all of those folders in a single thread. There are two ways to set one up: - **From the sidebar:** Click the **Add Project** button, choose **Add Local Folders**, and select multiple folders in the file picker. They open together as one multi-root project. - **From the title bar:** Click the project picker (the leftmost project name). For any local entry in the recent projects list, hover it and click the folder-with-plus icon (**Add Folder to this Project**) to merge that project's folders into the current project. -- **From the Project panel:** Right-click a root folder or any empty space in the Project panel and choose **Add Folders to Project** to add more folders to the current project. ## Worktree Isolation {#worktree-isolation} diff --git a/docs/src/finding-navigating.md b/docs/src/finding-navigating.md index c04ae57748..af30b0ee71 100644 --- a/docs/src/finding-navigating.md +++ b/docs/src/finding-navigating.md @@ -13,12 +13,6 @@ The Command Palette ({#kb command_palette::Toggle}) is your gateway to almost ev [Learn more about the Command Palette →](./command-palette.md) -## Project Panel - -The Project Panel ({#kb project_panel::ToggleFocus}) shows a tree view of your workspace's files and directories. Browse, create, rename, move, and delete files without leaving the editor. It also surfaces git status and diagnostics at a glance. - -[Learn more about the Project Panel →](./project-panel.md) - ## File Finder Open any file in your project with {#kb file_finder::Toggle}. Type part of the filename or path to narrow results. @@ -63,4 +57,3 @@ Quickly switch between open tabs with {#kb tab_switcher::Toggle}. Tabs are sorte | Symbol in project | {#kb project_symbols::Toggle} | | Outline Panel | {#kb outline_panel::ToggleFocus} | | Tab Switcher | {#kb tab_switcher::Toggle} | -| Project Panel | {#kb project_panel::ToggleFocus} | diff --git a/docs/src/project-panel.md b/docs/src/project-panel.md deleted file mode 100644 index 5b2ba4db4f..0000000000 --- a/docs/src/project-panel.md +++ /dev/null @@ -1,184 +0,0 @@ ---- -title: Project Panel - Zed -description: Navigate workspace files and directories with Zed's project panel. Create, rename, trash and delete file and directories. ---- - -# Project Panel - -The project panel shows a tree view of your workspace's files and directories. -Toggle it with {#action project_panel::ToggleFocus} ({#kb -project_panel::ToggleFocus}), or click the **Project Panel** button in the -status bar. - -![Project Panel](https://images.zed.dev/docs/project-panel/panel.png) - -## Navigating - -Use the arrow keys to move through entries. {#kb -project_panel::ExpandSelectedEntry} expands a directory and {#kb -project_panel::CollapseSelectedEntry} collapses it. {#kb -project_panel::CollapseAllEntries} collapses every directory at once. Press {#kb -project_panel::Open} or click to preview a selected file, without giving it a -permanent tab. Editing the file or double-clicking it promotes it to a permanent tab. - -### Auto-reveal - -By default, switching files in the editor will automatically highlight it in the -project panel and scroll it into view. This can be disabled with the -`project_panel.auto_reveal_entries` setting. - -### Sticky Scroll - -When `project_panel.sticky_scroll` is enabled (the default), ancestor directories pin themselves to the top -of the panel as you scroll, so you always know which directory you're on. - -![Project Panel: Sticky Scroll Enabled](https://images.zed.dev/docs/project-panel/sticky-scroll-true.png) - -![Project Panel: Sticky Scroll Disabled](https://images.zed.dev/docs/project-panel/sticky-scroll-false.png) - -### Directory Folding - -When `project_panel.auto_fold_dirs` is enabled (the default), chains of directories that each contain a -single child directory are collapsed into one row (for example, -`src/utils/helpers` instead of three separate levels). Right-click a folded -directory and choose **Unfold Directory** to expand the chain, or **Fold -Directory** to collapse it again. - -![Project Panel: Auto Fold Directories Enabled](https://images.zed.dev/docs/project-panel/auto-fold-dirs-true.png) - -![Project Panel: Auto Fold Directories Disabled](https://images.zed.dev/docs/project-panel/auto-fold-dirs-false.png) - -## Selecting Multiple Entries - -Hold `shift` while pressing the up/down arrow keys to mark additional entries. -Most file operations, like cut, copy, trash, delete and drag, apply to the full -set of marked entries. - -When exactly two files are marked, {#action project_panel::CompareMarkedFiles} -({#kb project_panel::CompareMarkedFiles}) opens a diff view comparing them. - -![Project Panel: Compare Marked Files](https://images.zed.dev/docs/project-panel/compare-marked-files.png) - -## File Operations - -Right-click an entry to see the full list of available operations, or use the -keybindings below. - -### Creating Files and Directories - -- {#action project_panel::NewFile} ({#kb project_panel::NewFile}) creates a new - file inside the selected directory. -- {#action project_panel::NewDirectory} ({#kb project_panel::NewDirectory}) - creates a new directory. - -An inline editor appears so you can type the name. Press `enter` to -confirm or `escape` to cancel. - -### Renaming - -Press {#kb project_panel::Rename} to rename the selected entry. The filename -stem is pre-selected so you can type a new name without accidentally changing -the extension. Press `enter` to confirm or `escape` to -cancel. - -### Cut, Copy, and Paste - -- {#action project_panel::Cut} ({#kb project_panel::Cut}) marks entries for - moving. -- {#action project_panel::Copy} ({#kb project_panel::Copy}) marks entries for - copying. -- {#action project_panel::Paste} ({#kb project_panel::Paste}) places them in the - selected directory. - -When pasting would create a name conflict, Zed appends a "copy" suffix (e.g., -`file copy.txt`, `file copy 2.txt`). If a single file is pasted with a generated -suffix, the rename editor opens automatically so you can adjust the name. - -### Duplicate - -{#action project_panel::Duplicate} ({#kb project_panel::Duplicate}) copies and -pastes the selected entries in one step. - -### Trash and Delete - -- {#action project_panel::Trash} ({#kb project_panel::Trash}) moves entries to - the system trash. -- {#action project_panel::Delete} ({#kb project_panel::Delete}) permanently - deletes entries. - -Both actions show a confirmation prompt listing the affected files. If any of -the files have unsaved changes, the prompt warns you. - -### Drag and Drop - -Drag entries within the panel to move them. Hold `alt` while dropping to copy -instead of move. You can also drag files from your operating system's file -manager into the project panel to copy them into the project. Drag and drop can -be disabled with the `project_panel.drag_and_drop` setting. - -## Git Integration - -When `project_panel.git_status` is enabled (the default), file and directory names are tinted -to reflect their git status—modified, added, deleted, untracked, or conflicting. - -Setting `project_panel.git_status_indicator` to `true` (disabled by default) adds a letter badge next -to each name: **M** (modified), **A** (added), **D** (deleted), **U** -(untracked) or **!** (conflict). - -![Project Panel: Git Integration](https://images.zed.dev/docs/project-panel/git-status.png) - -Use {#action project_panel::SelectNextGitEntry} and {#action -project_panel::SelectPrevGitEntry} to jump between tracked files with -uncommitted changes. The right-click menu also offers **Restore File** to -discard changes and **View File History** to browse a file's commit log. - -## Diagnostics - -The `project_panel.show_diagnostics` setting controls whether error and warning -indicators appear on file and folder icons. Set it to `"all"` to see both errors -and warnings, `"errors"` for errors only, or `"off"` to hide them. Diagnostics -propagate upward—if a file deep in a directory has an error, its ancestor -folders show an indicator too. - -Enable `project_panel.diagnostic_badges` (disabled by default) to display numeric error and warning -counts next to each entry. Use {#action project_panel::SelectNextDiagnostic} and -{#action project_panel::SelectPrevDiagnostic} to navigate between files that -have diagnostics. - -See also [Diagnostics & Quick Fixes](./diagnostics.md) for editor and tab diagnostic settings. - -## Filtering and Sorting - -### Hiding Files - -- `project_panel.hide_gitignore` hides files matched by `.gitignore`. Toggle - this with {#action project_panel::ToggleHideGitIgnore}. -- `project_panel.hide_hidden` hides dotfiles and other hidden entries. Toggle - with {#action project_panel::ToggleHideHidden}. - -### Sorting - -The `project_panel.sort_mode` setting controls grouping: - -- `"directories_first"` (default) — directories appear before files at each - level. -- `"files_first"` — files appear before directories. -- `"mixed"` — directories and files are sorted together. - -The `project_panel.sort_order` setting controls name comparison: - -- `"default"` — case-insensitive natural sort (`file2` before `file10`). -- `"upper"` — uppercase names grouped first, then lowercase. -- `"lower"` — lowercase names grouped first, then uppercase. -- `"unicode"` — raw Unicode codepoint order with no case folding. - -## Other Actions - -- {#action project_panel::RevealInFileManager} ({#kb - project_panel::RevealInFileManager}) reveals the selected entry in Finder / - File Explorer. -- {#action project_panel::NewSearchInDirectory} ({#kb - project_panel::NewSearchInDirectory}) opens a project search scoped to the - selected directory. -- {#action project_panel::RemoveFromProject} removes a workspace root folder - from the project. diff --git a/tooling/compliance/src/checks.rs b/tooling/compliance/src/checks.rs index 6558f6e362..570c87b655 100644 --- a/tooling/compliance/src/checks.rs +++ b/tooling/compliance/src/checks.rs @@ -1,26 +1,26 @@ use std::{fmt, ops::Not as _, rc::Rc}; -use futures::StreamExt; use itertools::Itertools as _; use crate::{ - git::{AutomatedChangeKind, CommitDetails, CommitList, ZED_ZIPPY_LOGIN}, + git::{CommitDetails, CommitList, ZED_ZIPPY_LOGIN}, github::{ - Approvable, CommitAuthor, CommitFileChange, CommitMetadata, GithubApiClient, GithubLogin, - PullRequestComment, PullRequestData, PullRequestReview, Repository, ReviewState, + Approvable, CommitAuthor, GithubApiClient, GithubLogin, PullRequestComment, + PullRequestData, PullRequestReview, Repository, ReviewState, }, - report::{Report, ReportEntry}, + report::Report, }; const ZED_ZIPPY_COMMENT_APPROVAL_PATTERN: &str = "@zed-zippy approve"; const ZED_ZIPPY_GROUP_APPROVAL: &str = "@zed-industries/approved"; +const EXPECTED_VERSION_BUMP_LOC: u64 = 2; #[derive(Debug)] pub enum ReviewSuccess { ApprovingComment(Vec), CoAuthored(Vec), PullRequestReviewed(Vec), - ZedZippyCommit(AutomatedChangeKind, GithubLogin), + ZedZippyCommit(GithubLogin), } impl ReviewSuccess { @@ -36,7 +36,7 @@ impl ReviewSuccess { .iter() .map(|comment| format!("@{}", comment.user.login)) .collect_vec(), - Self::ZedZippyCommit(_, login) => vec![login.to_string()], + Self::ZedZippyCommit(login) => vec![login.to_string()], }; let reviewers = reviewers.into_iter().unique().collect_vec(); @@ -59,8 +59,8 @@ impl fmt::Display for ReviewSuccess { Self::ApprovingComment(_) => { formatter.write_str("Approved by an organization approval comment") } - Self::ZedZippyCommit(kind, _) => { - write!(formatter, "Fully untampered automated {kind}") + Self::ZedZippyCommit(_) => { + formatter.write_str("Fully untampered automated version bump commit") } } } @@ -71,7 +71,7 @@ pub enum ReviewFailure { // todo: We could still query the GitHub API here to search for one NoPullRequestFound, Unreviewed, - UnexpectedZippyAction(AutomatedChangeFailure), + UnexpectedZippyAction(VersionBumpFailure), Other(anyhow::Error), } @@ -90,25 +90,17 @@ impl fmt::Display for ReviewFailure { } #[derive(Debug)] -pub enum AutomatedChangeFailure { +pub enum VersionBumpFailure { NoMentionInTitle, MissingCommitData, AuthorMismatch, UnexpectedCoAuthors, NotSigned, InvalidSignature, - UnexpectedLineChanges { - kind: AutomatedChangeKind, - additions: u64, - deletions: u64, - }, - UnexpectedFiles { - kind: AutomatedChangeKind, - found: Vec, - }, + UnexpectedLineChanges { additions: u64, deletions: u64 }, } -impl fmt::Display for AutomatedChangeFailure { +impl fmt::Display for VersionBumpFailure { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NoMentionInTitle => formatter.write_str("No @-mention found in commit title"), @@ -120,62 +112,19 @@ impl fmt::Display for AutomatedChangeFailure { Self::NotSigned => formatter.write_str("Commit is not signed"), Self::InvalidSignature => formatter.write_str("Commit signature is invalid"), Self::UnexpectedLineChanges { - kind, additions, deletions, } => { write!( formatter, - "Unexpected line changes for {kind} \ - ({additions} additions, {deletions} deletions, \ - expected {} each)", - kind.expected_loc() - ) - } - Self::UnexpectedFiles { kind, found } => { - let expected = kind.expected_files().join(", "); - let actual = found.join(", "); - write!( - formatter, - "Unexpected files changed for {kind} \ - (expected [{expected}], found [{actual}])" + "Unexpected line changes ({additions} additions, {deletions} deletions, \ + expected {EXPECTED_VERSION_BUMP_LOC} each)" ) } } } } -impl AutomatedChangeKind { - fn validate_changes( - self, - metadata: &CommitMetadata, - files: &[CommitFileChange], - ) -> Result<(), AutomatedChangeFailure> { - let expected_loc = self.expected_loc(); - if metadata.additions() != expected_loc || metadata.deletions() != expected_loc { - return Err(AutomatedChangeFailure::UnexpectedLineChanges { - kind: self, - additions: metadata.additions(), - deletions: metadata.deletions(), - }); - } - - let files_differ = files.len() != self.expected_files().len() - || files - .iter() - .any(|f| self.expected_files().contains(&f.filename.as_str()).not()); - - if files_differ { - return Err(AutomatedChangeFailure::UnexpectedFiles { - kind: self, - found: files.into_iter().map(|f| f.filename.clone()).collect(), - }); - } - - Ok(()) - } -} - pub(crate) type ReviewResult = Result; impl> From for ReviewFailure { @@ -213,7 +162,7 @@ impl Reporter { ) -> Result { let Some(pr_number) = commit.pr_number() else { if commit.author().is_zed_zippy() { - return self.check_zippy_automated_change(commit).await; + return self.check_zippy_version_bump(commit).await; } else { return Err(ReviewFailure::NoPullRequestFound); } @@ -245,15 +194,15 @@ impl Reporter { Err(ReviewFailure::Unreviewed) } - async fn check_zippy_automated_change( + async fn check_zippy_version_bump( &self, commit: &CommitDetails, ) -> Result { - let (change_kind, responsible_actor) = + let responsible_actor = commit - .detect_automated_change() + .version_bump_mention() .ok_or(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::NoMentionInTitle, + VersionBumpFailure::NoMentionInTitle, ))?; let commit_data = self @@ -261,54 +210,54 @@ impl Reporter { .get_commit_metadata(&Repository::ZED, &[commit.sha()]) .await?; - let metadata = - commit_data - .get(commit.sha()) - .ok_or(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::MissingCommitData, - ))?; + let authors = commit_data + .get(commit.sha()) + .ok_or(ReviewFailure::UnexpectedZippyAction( + VersionBumpFailure::MissingCommitData, + ))?; - if !metadata + if !authors .primary_author() .user() .is_some_and(|login| login.as_str() == ZED_ZIPPY_LOGIN) { return Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::AuthorMismatch, + VersionBumpFailure::AuthorMismatch, )); } - if metadata.co_authors().is_some() { + if authors.co_authors().is_some() { return Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::UnexpectedCoAuthors, + VersionBumpFailure::UnexpectedCoAuthors, )); } - let signature = metadata + let signature = authors .signature() .ok_or(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::NotSigned, + VersionBumpFailure::NotSigned, ))?; if !signature.is_valid() { return Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::InvalidSignature, + VersionBumpFailure::InvalidSignature, )); } - let files = self - .github_client - .get_commit_files(&Repository::ZED, commit.sha()) - .await?; + if authors.additions() != EXPECTED_VERSION_BUMP_LOC + || authors.deletions() != EXPECTED_VERSION_BUMP_LOC + { + return Err(ReviewFailure::UnexpectedZippyAction( + VersionBumpFailure::UnexpectedLineChanges { + additions: authors.additions(), + deletions: authors.deletions(), + }, + )); + } - change_kind - .validate_changes(metadata, &files) - .map_err(ReviewFailure::UnexpectedZippyAction)?; - - Ok(ReviewSuccess::ZedZippyCommit( - change_kind, - GithubLogin::new(responsible_actor.to_owned()), - )) + Ok(ReviewSuccess::ZedZippyCommit(GithubLogin::new( + responsible_actor.to_owned(), + ))) } async fn check_commit_co_authors( @@ -410,37 +359,30 @@ impl Reporter { body.contains(ZED_ZIPPY_COMMENT_APPROVAL_PATTERN) || body.contains(ZED_ZIPPY_GROUP_APPROVAL) } - pub async fn generate_report(mut self, max_concurrent_checks: usize) -> Report { + pub async fn generate_report(mut self) -> anyhow::Result { + let mut report = Report::new(); + let commits_to_check = std::mem::take(&mut self.commits); let total_commits = commits_to_check.len(); - let reports = futures::stream::iter(commits_to_check.into_iter().enumerate().map( - async |(i, commit)| { - println!( - "Checking commit {:?} ({current}/{total})", - commit.sha().short(), - current = i + 1, - total = total_commits - ); + for (i, commit) in commits_to_check.into_iter().enumerate() { + println!( + "Checking commit {:?} ({current}/{total})", + commit.sha().short(), + current = i + 1, + total = total_commits + ); - let review_result = self.check_commit(&commit).await; + let review_result = self.check_commit(&commit).await; - if let Err(err) = &review_result { - println!("Commit {:?} failed review: {:?}", commit.sha().short(), err); - } + if let Err(err) = &review_result { + println!("Commit {:?} failed review: {:?}", commit.sha().short(), err); + } - (commit, review_result) - }, - )) - .buffered(max_concurrent_checks) - .collect::>() - .await; + report.add(commit, review_result); + } - Report::from_entries( - reports - .into_iter() - .map(|(commit, result)| ReportEntry::new(commit, result)), - ) + Ok(report) } } @@ -449,23 +391,19 @@ mod tests { use std::rc::Rc; use std::str::FromStr; - use crate::git::{ - AutomatedChangeKind, CommitDetails, CommitList, CommitSha, ZED_ZIPPY_EMAIL, ZED_ZIPPY_LOGIN, - }; + use crate::git::{CommitDetails, CommitList, CommitSha, ZED_ZIPPY_EMAIL, ZED_ZIPPY_LOGIN}; use crate::github::{ - AuthorAssociation, CommitFileChange, CommitMetadataBySha, GithubApiClient, GithubLogin, - GithubUser, PullRequestComment, PullRequestData, PullRequestReview, Repository, - ReviewState, + AuthorAssociation, CommitMetadataBySha, GithubApiClient, GithubLogin, GithubUser, + PullRequestComment, PullRequestData, PullRequestReview, Repository, ReviewState, }; - use super::{AutomatedChangeFailure, Reporter, ReviewFailure, ReviewSuccess}; + use super::{Reporter, ReviewFailure, ReviewSuccess, VersionBumpFailure}; struct MockGithubApi { pull_request: PullRequestData, reviews: Vec, comments: Vec, commit_metadata_json: serde_json::Value, - commit_files: Vec, org_members: Vec, } @@ -503,14 +441,6 @@ mod tests { serde_json::from_value(self.commit_metadata_json.clone()).map_err(Into::into) } - async fn get_commit_files( - &self, - _repo: &Repository<'_>, - _sha: &CommitSha, - ) -> anyhow::Result> { - Ok(self.commit_files.clone()) - } - async fn check_repo_write_permission( &self, _repo: &Repository<'_>, @@ -616,7 +546,6 @@ mod tests { reviews: Vec, comments: Vec, commit_metadata_json: serde_json::Value, - commit_files: Vec, org_members: Vec, commit: CommitDetails, } @@ -635,7 +564,6 @@ mod tests { reviews: vec![], comments: vec![], commit_metadata_json: serde_json::json!({}), - commit_files: vec![], org_members: vec![], commit: make_commit( "abc12345abc12345", @@ -672,16 +600,6 @@ mod tests { self } - fn with_commit_files(mut self, filenames: Vec<&str>) -> Self { - self.commit_files = filenames - .into_iter() - .map(|f| CommitFileChange { - filename: f.to_owned(), - }) - .collect(); - self - } - fn zippy_version_bump() -> Self { Self { pull_request: PullRequestData { @@ -704,14 +622,6 @@ mod tests { "deletions": 2 } }), - commit_files: vec![ - CommitFileChange { - filename: "Cargo.lock".to_owned(), - }, - CommitFileChange { - filename: "crates/zed/Cargo.toml".to_owned(), - }, - ], org_members: vec![], commit: make_commit( "abc12345abc12345", @@ -723,49 +633,12 @@ mod tests { } } - fn zippy_release_channel_update() -> Self { - Self { - pull_request: PullRequestData { - number: 0, - user: None, - merged_by: None, - labels: None, - }, - reviews: vec![], - comments: vec![], - commit_metadata_json: serde_json::json!({ - "abc12345abc12345": { - "author": zippy_author(), - "authors": { "nodes": [] }, - "signature": { - "isValid": true, - "signer": { "login": ZED_ZIPPY_LOGIN } - }, - "additions": 1, - "deletions": 1 - } - }), - commit_files: vec![CommitFileChange { - filename: "crates/zed/RELEASE_CHANNEL".to_owned(), - }], - org_members: vec![], - commit: make_commit( - "abc12345abc12345", - "Zed Zippy", - ZED_ZIPPY_EMAIL, - "v0.233.x stable for @cole-miller", - "", - ), - } - } - async fn run_scenario(self) -> Result { let mock = MockGithubApi { pull_request: self.pull_request, reviews: self.reviews, comments: self.comments, commit_metadata_json: self.commit_metadata_json, - commit_files: self.commit_files, org_members: self.org_members, }; let client = Rc::new(mock); @@ -1028,14 +901,8 @@ mod tests { #[tokio::test] async fn zippy_version_bump_with_valid_signature_succeeds() { let result = TestScenario::zippy_version_bump().run_scenario().await; - assert!(matches!( - result, - Ok(ReviewSuccess::ZedZippyCommit( - AutomatedChangeKind::VersionBump, - _ - )) - )); - if let Ok(ReviewSuccess::ZedZippyCommit(_, login)) = &result { + assert!(matches!(result, Ok(ReviewSuccess::ZedZippyCommit(_)))); + if let Ok(ReviewSuccess::ZedZippyCommit(login)) = &result { assert_eq!(login.as_str(), "cole-miller"); } } @@ -1055,7 +922,7 @@ mod tests { assert!(matches!( result, Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::NoMentionInTitle + VersionBumpFailure::NoMentionInTitle )) )); } @@ -1076,7 +943,7 @@ mod tests { assert!(matches!( result, Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::NotSigned + VersionBumpFailure::NotSigned )) )); } @@ -1101,7 +968,7 @@ mod tests { assert!(matches!( result, Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::InvalidSignature + VersionBumpFailure::InvalidSignature )) )); } @@ -1126,7 +993,7 @@ mod tests { assert!(matches!( result, Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::UnexpectedLineChanges { .. } + VersionBumpFailure::UnexpectedLineChanges { .. } )) )); } @@ -1151,7 +1018,7 @@ mod tests { assert!(matches!( result, Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::AuthorMismatch + VersionBumpFailure::AuthorMismatch )) )); } @@ -1176,42 +1043,11 @@ mod tests { assert!(matches!( result, Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::UnexpectedCoAuthors + VersionBumpFailure::UnexpectedCoAuthors )) )); } - #[tokio::test] - async fn zippy_version_bump_with_wrong_files_fails() { - let result = TestScenario::zippy_version_bump() - .with_commit_files(vec!["crates/zed/RELEASE_CHANNEL"]) - .run_scenario() - .await; - assert!(matches!( - result, - Err(ReviewFailure::UnexpectedZippyAction( - AutomatedChangeFailure::UnexpectedFiles { .. } - )) - )); - } - - #[tokio::test] - async fn zippy_release_channel_update_succeeds() { - let result = TestScenario::zippy_release_channel_update() - .run_scenario() - .await; - assert!(matches!( - result, - Ok(ReviewSuccess::ZedZippyCommit( - AutomatedChangeKind::ReleaseChannelUpdate, - _ - )) - )); - if let Ok(ReviewSuccess::ZedZippyCommit(_, login)) = &result { - assert_eq!(login.as_str(), "cole-miller"); - } - } - #[tokio::test] async fn non_zippy_commit_without_pr_is_no_pr_found() { let result = TestScenario::single_commit() diff --git a/tooling/compliance/src/git.rs b/tooling/compliance/src/git.rs index aeea0b2f26..08adcb4b13 100644 --- a/tooling/compliance/src/git.rs +++ b/tooling/compliance/src/git.rs @@ -18,37 +18,6 @@ use serde::Deserialize; pub(crate) const ZED_ZIPPY_LOGIN: &str = "zed-zippy[bot]"; pub(crate) const ZED_ZIPPY_EMAIL: &str = "234243425+zed-zippy[bot]@users.noreply.github.com"; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AutomatedChangeKind { - VersionBump, - ReleaseChannelUpdate, -} - -impl AutomatedChangeKind { - pub(crate) fn expected_files(&self) -> &'static [&'static str] { - match self { - Self::VersionBump => &["Cargo.lock", "crates/zed/Cargo.toml"], - Self::ReleaseChannelUpdate => &["crates/zed/RELEASE_CHANNEL"], - } - } - - pub(crate) fn expected_loc(&self) -> u64 { - match self { - Self::VersionBump => 2, - Self::ReleaseChannelUpdate => 1, - } - } -} - -impl fmt::Display for AutomatedChangeKind { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::VersionBump => formatter.write_str("version bump"), - Self::ReleaseChannelUpdate => formatter.write_str("release channel update"), - } - } -} - pub trait Subcommand { type ParsedOutput: FromStr; @@ -273,29 +242,19 @@ impl CommitDetails { &self.title } - pub fn sha(&self) -> &CommitSha { + pub(crate) fn sha(&self) -> &CommitSha { &self.sha } - pub(crate) fn detect_automated_change(&self) -> Option<(AutomatedChangeKind, &str)> { + pub(crate) fn version_bump_mention(&self) -> Option<&str> { static VERSION_BUMP_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r"^Bump to [0-9]+\.[0-9]+\.[0-9]+ for @([a-zA-Z0-9][a-zA-Z0-9-]*)$").unwrap() }); - static RELEASE_CHANNEL_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(r"^v[0-9]+\.[0-9]+\.x (?:stable|preview) for @([a-zA-Z0-9][a-zA-Z0-9-]*)$") - .unwrap() - }); VERSION_BUMP_REGEX .captures(&self.title) - .and_then(|capture| capture.get(1)) - .map(|r#match| (AutomatedChangeKind::VersionBump, r#match.as_str())) - .or_else(|| { - RELEASE_CHANNEL_REGEX - .captures(&self.title) - .and_then(|capture| capture.get(1)) - .map(|r#match| (AutomatedChangeKind::ReleaseChannelUpdate, r#match.as_str())) - }) + .and_then(|cap| cap.get(1)) + .map(|m| m.as_str()) } } @@ -671,69 +630,53 @@ mod tests { } #[test] - fn automated_change_detects_version_bump() { + fn version_bump_mention_extracts_username() { let line = format!( "abc123{d}Zed Zippy{d}bot@test.com{d}Bump to 0.230.2 for @cole-miller", d = CommitDetails::FIELD_DELIMITER ); let commit = CommitDetails::parse(&line, "").unwrap(); - let (kind, actor) = commit.detect_automated_change().unwrap(); - assert_eq!(kind, AutomatedChangeKind::VersionBump); - assert_eq!(actor, "cole-miller"); + assert_eq!(commit.version_bump_mention(), Some("cole-miller")); } #[test] - fn automated_change_detects_stable_release_channel() { - let line = format!( - "abc123{d}Zed Zippy{d}bot@test.com{d}v0.233.x stable for @cole-miller", - d = CommitDetails::FIELD_DELIMITER - ); - let commit = CommitDetails::parse(&line, "").unwrap(); - let (kind, actor) = commit.detect_automated_change().unwrap(); - assert_eq!(kind, AutomatedChangeKind::ReleaseChannelUpdate); - assert_eq!(actor, "cole-miller"); - } - - #[test] - fn automated_change_detects_preview_release_channel() { - let line = format!( - "abc123{d}Zed Zippy{d}bot@test.com{d}v0.234.x preview for @cole-miller", - d = CommitDetails::FIELD_DELIMITER - ); - let commit = CommitDetails::parse(&line, "").unwrap(); - let (kind, actor) = commit.detect_automated_change().unwrap(); - assert_eq!(kind, AutomatedChangeKind::ReleaseChannelUpdate); - assert_eq!(actor, "cole-miller"); - } - - #[test] - fn automated_change_returns_none_for_regular_commit() { + fn version_bump_mention_returns_none_without_mention() { let line = format!( "abc123{d}Alice{d}alice@test.com{d}Fix a bug", d = CommitDetails::FIELD_DELIMITER ); let commit = CommitDetails::parse(&line, "").unwrap(); - assert!(commit.detect_automated_change().is_none()); + assert!(commit.version_bump_mention().is_none()); } #[test] - fn automated_change_rejects_wrong_prefix() { + fn version_bump_mention_rejects_wrong_prefix() { let line = format!( "abc123{d}Zed Zippy{d}bot@test.com{d}Fix thing for @cole-miller", d = CommitDetails::FIELD_DELIMITER ); let commit = CommitDetails::parse(&line, "").unwrap(); - assert!(commit.detect_automated_change().is_none()); + assert!(commit.version_bump_mention().is_none()); } #[test] - fn automated_change_rejects_trailing_text() { + fn version_bump_mention_rejects_bare_mention() { + let line = format!( + "abc123{d}Zed Zippy{d}bot@test.com{d}@cole-miller bumped something", + d = CommitDetails::FIELD_DELIMITER + ); + let commit = CommitDetails::parse(&line, "").unwrap(); + assert!(commit.version_bump_mention().is_none()); + } + + #[test] + fn version_bump_mention_rejects_trailing_text() { let line = format!( "abc123{d}Zed Zippy{d}bot@test.com{d}Bump to 0.230.2 for @cole-miller extra", d = CommitDetails::FIELD_DELIMITER ); let commit = CommitDetails::parse(&line, "").unwrap(); - assert!(commit.detect_automated_change().is_none()); + assert!(commit.version_bump_mention().is_none()); } #[test] diff --git a/tooling/compliance/src/github.rs b/tooling/compliance/src/github.rs index 4d1da9ba5c..9a2333078f 100644 --- a/tooling/compliance/src/github.rs +++ b/tooling/compliance/src/github.rs @@ -177,11 +177,6 @@ impl CommitSignature { } } -#[derive(Debug, Clone, Deserialize)] -pub struct CommitFileChange { - pub filename: String, -} - #[derive(Debug, Deserialize)] pub struct CommitMetadata { #[serde(rename = "author")] @@ -306,11 +301,6 @@ pub trait GithubApiClient { repo: &Repository<'_>, commit_shas: &[&CommitSha], ) -> Result; - async fn get_commit_files( - &self, - repo: &Repository<'_>, - sha: &CommitSha, - ) -> Result>; async fn check_repo_write_permission( &self, repo: &Repository<'_>, @@ -440,8 +430,8 @@ mod octo_client { }; use super::{ - AuthorAssociation, CommitFileChange, CommitMetadataBySha, GithubApiClient, GithubLogin, - GithubUser, PullRequestComment, PullRequestData, PullRequestReview, ReviewState, + AuthorAssociation, CommitMetadataBySha, GithubApiClient, GithubLogin, GithubUser, + PullRequestComment, PullRequestData, PullRequestReview, ReviewState, }; fn convert_author_association(association: OctocrabAuthorAssociation) -> AuthorAssociation { @@ -622,27 +612,6 @@ mod octo_client { .map(|response| response.repository) } - async fn get_commit_files( - &self, - repo: &Repository<'_>, - sha: &CommitSha, - ) -> Result> { - let response = self - .client - .commits(repo.owner.as_ref(), repo.name.as_ref()) - .get(sha.as_str()) - .await?; - - Ok(response - .files - .into_iter() - .flatten() - .map(|file| CommitFileChange { - filename: file.filename, - }) - .collect()) - } - async fn check_repo_write_permission( &self, repo: &Repository<'_>, diff --git a/tooling/compliance/src/report.rs b/tooling/compliance/src/report.rs index 4de67a0010..711db20a03 100644 --- a/tooling/compliance/src/report.rs +++ b/tooling/compliance/src/report.rs @@ -21,12 +21,6 @@ pub struct ReportEntry { reason: R, } -impl ReportEntry { - pub fn new(commit: CommitDetails, reason: R) -> Self { - Self { commit, reason } - } -} - impl ReportEntry { fn commit_cell(&self) -> String { let title = escape_markdown_link_text(self.commit.title()); @@ -53,12 +47,6 @@ impl ReportEntry { } } -impl ReportEntry { - pub fn is_unknown_error(&self) -> bool { - matches!(self.reason, Err(ReviewFailure::Other(_))) - } -} - impl ReportEntry { fn issue_kind(&self) -> IssueKind { match self.reason { @@ -121,7 +109,7 @@ impl ReportSummary { .count(), errors: entries .iter() - .filter(|entry| entry.is_unknown_error()) + .filter(|entry| matches!(entry.reason, Err(ReviewFailure::Other(_)))) .count(), } } @@ -161,12 +149,6 @@ impl Report { Self::default() } - pub fn from_entries(entries: impl IntoIterator>) -> Self { - Self { - entries: entries.into_iter().collect(), - } - } - pub fn add(&mut self, commit: CommitDetails, result: ReviewResult) { self.entries.push(ReportEntry { commit, @@ -338,7 +320,7 @@ mod tests { use crate::{ checks::{ReviewFailure, ReviewSuccess}, - git::{AutomatedChangeKind, CommitDetails, CommitList}, + git::{CommitDetails, CommitList}, github::{AuthorAssociation, GithubLogin, GithubUser, PullRequestReview, ReviewState}, }; @@ -400,10 +382,9 @@ mod tests { ); report.add( make_commit("ddd", "Dave", "dave@test.com", "Bump Version", ""), - Ok(ReviewSuccess::ZedZippyCommit( - AutomatedChangeKind::VersionBump, - GithubLogin::new("dave".to_string()), - )), + Ok(ReviewSuccess::ZedZippyCommit(GithubLogin::new( + "dave".to_string(), + ))), ); let summary = report.summary(); diff --git a/tooling/xtask/src/tasks/compliance.rs b/tooling/xtask/src/tasks/compliance.rs index f4ae8593e3..46c40fb82c 100644 --- a/tooling/xtask/src/tasks/compliance.rs +++ b/tooling/xtask/src/tasks/compliance.rs @@ -10,18 +10,12 @@ use compliance::{ report::ReportReviewSummary, }; -const MAX_CONCURRENT_REQUESTS: usize = 5; - #[derive(Parser)] pub(crate) struct ComplianceArgs { #[clap(subcommand)] mode: ComplianceMode, } -const IGNORE_LIST: &[&str] = &[ - "75fa566511e3ae7d03cfd76008512080291bd81d", // GitHub nuked this PR out of orbit -]; - #[derive(Subcommand)] pub(crate) enum ComplianceMode { // Check compliance for all commits between two version tags @@ -122,8 +116,8 @@ async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> { println!("Checking commit range {range}, {} total", commits.len()); let report = Reporter::new(commits, client.clone()) - .generate_report(MAX_CONCURRENT_REQUESTS) - .await; + .generate_report() + .await?; println!( "Generated report for version {}", @@ -137,10 +131,6 @@ async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> { summary.prs_with_errors() ); - let all_errors_known = report.errors().all(|error| { - error.is_unknown_error() && IGNORE_LIST.contains(&error.commit.sha().as_str()) - }); - for report in report.errors() { if let Some(pr_number) = report.commit.pr_number() && let Ok(pull_request) = client.get_pull_request(&Repository::ZED, pr_number).await @@ -171,14 +161,6 @@ async fn check_compliance_impl(args: ComplianceArgs) -> Result<()> { "Compliance check failed, found {} commits not reviewed", summary.not_reviewed )), - ReportReviewSummary::MissingReviewsWithErrors if all_errors_known => { - println!( - "Compliance check failed with {} unreviewed commits, but all errors are known.", - summary.not_reviewed - ); - - Ok(()) - } ReportReviewSummary::MissingReviewsWithErrors => Err(anyhow::anyhow!( "Compliance check failed with {} unreviewed commits and {} other issues", summary.not_reviewed, diff --git a/tooling/xtask/src/tasks/workflows/bump_zed_version.rs b/tooling/xtask/src/tasks/workflows/bump_zed_version.rs index 5501412f1b..af1ba95d1b 100644 --- a/tooling/xtask/src/tasks/workflows/bump_zed_version.rs +++ b/tooling/xtask/src/tasks/workflows/bump_zed_version.rs @@ -175,10 +175,7 @@ fn create_preview_branch( let main_sha = StepOutput::new(&main_sha_step, "main_sha"); let commit_step: Step = steps::BotCommitStep::new( - format!( - "{} preview for @${{{{ github.actor }}}}", - outputs.preview_branch - ), + format!("{} preview", outputs.preview_branch), &outputs.preview_branch, &token, ) @@ -232,10 +229,7 @@ fn promote_to_stable( let write_channel = named::bash("echo -n stable > crates/zed/RELEASE_CHANNEL"); let commit_step: Step = steps::BotCommitStep::new( - format!( - "{} stable for @${{{{ github.actor }}}}", - outputs.stable_branch - ), + format!("{} stable", outputs.stable_branch), &outputs.stable_branch, &token, ) diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs index 644c033f68..5dd8bdd779 100644 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ b/tooling/xtask/src/tasks/workflows/run_tests.rs @@ -792,7 +792,7 @@ pub(crate) fn check_scripts() -> NamedJob { named::job( release_job(&[]) - .runs_on(runners::LINUX_LARGE) + .runs_on(runners::LINUX_SMALL) .add_step(steps::checkout_repo()) .add_step(run_shellcheck()) .add_step(download_actionlint().id("get_actionlint"))