diff --git a/.github/workflows/bump_zed_version.yml b/.github/workflows/bump_zed_version.yml index f8fae3de7af..02159b8ad24 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 4f39b4ed52c..17e121b9958 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 2051fa567b5..d7b9883f01d 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 c11331308fe..908069954d7 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", @@ -10795,9 +10790,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" name = "miniprofiler_ui" version = "0.1.0" dependencies = [ + "command_palette_hooks", "gpui", "rpc", "serde_json", + "settings", "smol", "theme_settings", "util", @@ -17771,7 +17768,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 50914b29335..999c9b72a1e 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 d57794e61a9..d5bc26b9417 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 8fc86ac72b0..1a707d211ad 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. @@ -1691,14 +1686,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": { @@ -2526,6 +2513,11 @@ // Mostly useful for developers who are managing multiple instances of Zed. "nightly": { // "theme": "Andromeda" + "instrumentation": { + "performance_profiler": { + "enabled": true, + }, + }, }, // Settings overrides to use when using Zed Stable. // Mostly useful for developers who are managing multiple instances of Zed. @@ -2536,6 +2528,11 @@ // Mostly useful for developers who are managing multiple instances of Zed. "dev": { // "theme": "Andromeda" + "instrumentation": { + "performance_profiler": { + "enabled": true, + }, + }, }, // Settings overrides to use when using Linux. "linux": {}, @@ -2655,4 +2652,16 @@ // // Example: {"log": {"client": "warn"}} "log": {}, + + // Configuration for developer-oriented instrumentation tools that can be + // toggled at runtime. + "instrumentation": { + // Performance profiler, accessed via the `zed: open performance profiler` + // action. Collects timing data for foreground and background executor + // tasks. Enabling this may lead to increased memory usage, hence it's + // disabled by default for regular builds. + "performance_profiler": { + "enabled": false, + }, + }, } diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index bf1d933acd7..cf4693beba7 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 403b71736c9..ac7b2d23cb7 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 c5bc4c58285..9eb0f84b6fb 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 9ccebd80e85..a56c793bb85 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 04951fcbe7c..50297390970 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 e5c6f161022..b363d6a42ae 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 992d5ac937e..6577496965a 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 6b552610cf4..0c7b3eb6baa 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 213ce4e88c9..0f213cb9f1e 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 0b7e023e2c0..21ac2af0997 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 269edbea7e2..ac94daba711 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 7bb4821cc78..16bb5012e58 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 c550d95979f..4e84d389467 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 57daa55394e..16783aa9e96 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 77ba83597ed..ebd0d2c06dc 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 a3f60fd7b50..12db814d3ae 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 ff4450f2697..323d6346ed4 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 b466cf1e1fc..5eee727a7df 100644 --- a/crates/collab/tests/integration/integration_tests.rs +++ b/crates/collab/tests/integration/integration_tests.rs @@ -5299,7 +5299,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 6655d86d25c..3eaae6a1d48 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 c156e694666..16ce0d65990 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 8869e1c3f3a..0a8cd1b066a 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 072051a8de9..f0fa37c4d6f 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 bbd12dec4e3..261d3383d0a 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 87d2426878e..c1bf2525d9e 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 9102fc428f6..b01d1592abb 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 025cd4251ad..6dfbf334024 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 1cefd2179b7..0646bf7a068 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 125f09c9661..1752aefc5e6 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 347383d0171..43479f1713c 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 f7f09956c15..67ebab62295 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 d75481f6f74..9a9cc983fa7 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 eefe2717f22..e7b8dcd4ebd 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 6b7e4816a18..6694b19e373 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: i64 = 0x01021997; // Plan 9 / WSL2 interop (drvfs) - const NFS_SUPER_MAGIC: i64 = 0x6969; - const CIFS_MAGIC: i64 = 0xFF534D42u32 as i64; // CIFS/SMB - const SMB_SUPER_MAGIC: i64 = 0x517B; - const SMB2_MAGIC: i64 = 0xFE534D42u32 as i64; - const FUSE_SUPER_MAGIC: i64 = 0x65735546; // FUSE (includes sshfs) - - let fs_type = stat.f_type; - 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 909424558b7..02a6b087811 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 { } } -pub fn global(f: impl FnOnce(&GlobalWatcher) -> T) -> anyhow::Result { - let global_watcher = global_watcher(); - global_watcher.ensure_native_watcher()?; - Ok(f(global_watcher)) +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 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 1e2d7f79dd3..5a9350f8aec 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 b119d243ad6..69829231619 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 921bcd19608..ad6d960a307 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 e313700dd86..6fe3d9484b4 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 8257b3cf4fb..6f5463d65a3 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 caf4b22dc45..9ccbced249d 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 4a300a52574..9f5b65d7560 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 739c3148805..fe2add8177e 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 ba411cb0642..c568b007f76 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 3642d46a332..3453364a20e 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 df6403460d9..bdda213dfd0 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/elements/list.rs b/crates/gpui/src/elements/list.rs index 5a88d81c18d..c7409687faf 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -125,6 +125,14 @@ impl FollowState { *self = FollowState::Tail { is_following: true }; } } + + fn stop_following(&mut self) { + if let FollowState::Tail { is_following: true } = self { + *self = FollowState::Tail { + is_following: false, + }; + } + } } /// Whether the list is scrolling from top to bottom or bottom to top. @@ -464,9 +472,7 @@ impl ListState { let state = &mut *self.0.borrow_mut(); if distance < px(0.) { - if let FollowState::Tail { is_following } = &mut state.follow_state { - *is_following = false; - } + state.follow_state.stop_following(); } let mut cursor = state.items.cursor::(()); @@ -544,9 +550,7 @@ impl ListState { } if scroll_top.item_ix < item_count { - if let FollowState::Tail { is_following } = &mut state.follow_state { - *is_following = false; - } + state.follow_state.stop_following(); } state.logical_scroll_top = Some(scroll_top); @@ -727,10 +731,8 @@ impl StateInner { }); } - if let FollowState::Tail { is_following } = &mut self.follow_state { - if delta.y > px(0.) { - *is_following = false; - } + if delta.y > px(0.) { + self.follow_state.stop_following(); } if let Some(handler) = self.scroll_handler.as_mut() { @@ -1135,7 +1137,7 @@ impl StateInner { content_height - self.scrollbar_drag_start_height.unwrap_or(content_height); let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max); - self.follow_state = FollowState::Normal; + self.follow_state.stop_following(); if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max { self.logical_scroll_top = None; @@ -2014,14 +2016,11 @@ mod test { ); } - /// Calling `set_follow_mode(FollowState::Normal)` or dragging the scrollbar should - /// fully disengage follow_tail — clearing any suspended state so - /// follow_tail won’t auto-re-engage. #[gpui::test] - fn test_follow_tail_suspended_state_cleared_by_explicit_actions(cx: &mut TestAppContext) { + fn test_follow_tail_reengages_after_scrollbar_disengagement(cx: &mut TestAppContext) { let cx = cx.add_empty_window(); - // 10 items × 50px = 500px total, 200px viewport. + // 10 items × 50px = 500px total, 200px viewport, scroll_max = 300px. let state = ListState::new(10, crate::ListAlignment::Top, px(0.)).measure_all(); struct TestView(ListState); @@ -2038,59 +2037,24 @@ mod test { let view = cx.update(|_, cx| cx.new(|_| TestView(state.clone()))); state.set_follow_mode(FollowMode::Tail); - // --- Part 1: set_follow_mode(FollowState::Normal) clears suspended state --- - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { view.clone().into_any_element() }); + assert!(state.is_following_tail()); - // Scroll up — suspends follow_tail. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(50.))), - ..Default::default() - }); + // Drag the scrollbar up to the middle — follow_tail should suspend. + state.set_offset_from_scrollbar(point(px(0.), px(150.))); assert!(!state.is_following_tail()); - // Scroll back to the bottom — should re-engage follow_tail. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))), - ..Default::default() - }); - + // Drag the scrollbar back to the bottom — follow_tail should re-engage + // on the next paint. + state.set_offset_from_scrollbar(point(px(0.), px(300.))); cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() + view.into_any_element() }); assert!( state.is_following_tail(), - "follow_tail should re-engage after scrolling back to the bottom" - ); - - // --- Part 2: scrollbar drag clears suspended state --- - - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - - // Drag the scrollbar to the middle — should clear suspended state. - state.set_offset_from_scrollbar(point(px(0.), px(150.))); - - // Scroll to the bottom. - cx.simulate_event(ScrollWheelEvent { - position: point(px(50.), px(100.)), - delta: ScrollDelta::Pixels(point(px(0.), px(-10000.))), - ..Default::default() - }); - - // Paint — should NOT re-engage because the scrollbar drag - // cleared the suspended state. - cx.draw(point(px(0.), px(0.)), size(px(100.), px(200.)), |_, _| { - view.clone().into_any_element() - }); - assert!( - !state.is_following_tail(), - "follow_tail should not re-engage after scrollbar drag cleared the suspended state" + "follow_tail should re-engage after scrolling back to the bottom via the scrollbar" ); } } diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index ab253472ad8..f66f5844787 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/profiler.rs b/crates/gpui/src/profiler.rs index 1405b4d0496..009afb9099e 100644 --- a/crates/gpui/src/profiler.rs +++ b/crates/gpui/src/profiler.rs @@ -3,7 +3,10 @@ use std::{ cell::LazyCell, collections::{HashMap, VecDeque}, hash::{DefaultHasher, Hash, Hasher}, - sync::Arc, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, thread::ThreadId, }; @@ -348,6 +351,9 @@ impl Drop for ThreadTimings { #[doc(hidden)] pub fn add_task_timing(timing: TaskTiming) { + if !PROFILER_ENABLED.load(Ordering::Acquire) { + return; + } THREAD_TIMINGS.with(|timings| { timings.lock().add_task_timing(timing); }); @@ -357,3 +363,28 @@ pub fn add_task_timing(timing: TaskTiming) { pub fn get_current_thread_task_timings() -> ThreadTaskTimings { THREAD_TIMINGS.with(|timings| timings.lock().get_thread_task_timings()) } + +static PROFILER_ENABLED: AtomicBool = AtomicBool::new(false); + +/// Enables or disables task timing collection at runtime. +/// +/// When transitioning from enabled to disabled, `add_task_timing` becomes a +/// no-op and the existing per-thread buffers are cleared so stale data isn't +/// reported after a later re-enable. Calls with the current value are a no-op. +pub fn set_enabled(enabled: bool) -> bool { + if PROFILER_ENABLED.swap(enabled, Ordering::AcqRel) == enabled { + return false; + } + + if !enabled { + for global in GLOBAL_THREAD_TIMINGS.lock().iter() { + if let Some(timings) = global.timings.upgrade() { + let mut timings = timings.lock(); + timings.timings.clear(); + timings.timings.shrink_to_fit(); + timings.total_pushed = 0; + } + } + } + true +} diff --git a/crates/gpui/src/text_system/line.rs b/crates/gpui/src/text_system/line.rs index 437ef48cb17..9c6ffdbe3f6 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 3b8ca0f4502..10f4aab0db1 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 07d1596a2f7..70c0392d43c 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 eac1e2559a2..ae56c2ccd0e 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 ebd5afb7c74..0b55631e5ca 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 e15568affcc..3770e4ccf13 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 bb543478e5e..00000000000 --- 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 ea8a0c69a1a..505fbb4d7d2 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 701b363d9eb..adc874b666c 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 db50f5161e3..ef9fbae1131 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 da5659bf508..adae72068c5 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 27ece268d66..2b7379b0b40 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/miniprofiler_ui/Cargo.toml b/crates/miniprofiler_ui/Cargo.toml index a8041b8b37c..09144c0fc68 100644 --- a/crates/miniprofiler_ui/Cargo.toml +++ b/crates/miniprofiler_ui/Cargo.toml @@ -12,8 +12,10 @@ workspace = true path = "src/miniprofiler_ui.rs" [dependencies] +command_palette_hooks.workspace = true gpui.workspace = true rpc.workspace = true +settings.workspace = true theme_settings.workspace = true zed_actions.workspace = true workspace.workspace = true diff --git a/crates/miniprofiler_ui/src/miniprofiler_ui.rs b/crates/miniprofiler_ui/src/miniprofiler_ui.rs index 351d0a68e26..82c7a1e6881 100644 --- a/crates/miniprofiler_ui/src/miniprofiler_ui.rs +++ b/crates/miniprofiler_ui/src/miniprofiler_ui.rs @@ -5,14 +5,17 @@ use std::{ time::{Duration, Instant}, }; +use command_palette_hooks::CommandPaletteFilter; use gpui::{ App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement, ParentElement as _, ProfilingCollector, Render, SerializedLocation, SerializedTaskTiming, SerializedThreadTaskTimings, SharedString, StatefulInteractiveElement, Styled, Task, ThreadTimingsDelta, TitlebarOptions, UniformListScrollHandle, WeakEntity, WindowBounds, - WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list, + WindowOptions, div, prelude::FluentBuilder, profiler, px, relative, size, uniform_list, }; use rpc::{AnyProtoClient, proto}; +use settings::{RegisterSetting, Settings, SettingsContent, SettingsStore}; +use std::any::TypeId; use util::ResultExt; use workspace::{ Workspace, @@ -61,7 +64,49 @@ impl ProfileSource { } } +#[derive(Clone, Copy, Debug, Default, RegisterSetting)] +struct PerformanceProfilerSettings { + enabled: bool, +} + +impl Settings for PerformanceProfilerSettings { + fn from_settings(content: &SettingsContent) -> Self { + let instrumentation = content.instrumentation.as_ref().unwrap(); + let profiler = instrumentation.performance_profiler.as_ref().unwrap(); + Self { + enabled: profiler.enabled.unwrap(), + } + } +} + pub fn init(startup_time: Instant, cx: &mut App) { + let initial_enabled = PerformanceProfilerSettings::get_global(cx).enabled; + profiler::set_enabled(initial_enabled); + update_command_palette_filter(initial_enabled, cx); + + cx.observe_global::(|cx| { + let enabled = PerformanceProfilerSettings::get_global(cx).enabled; + // `set_enabled` reports whether the value actually changed, so skip the + // filter update and window cleanup on the common no-op path — the + // settings observer fires for every settings change. + if !profiler::set_enabled(enabled) { + return; + } + update_command_palette_filter(enabled, cx); + if !enabled { + for window in cx + .windows() + .into_iter() + .filter_map(|window| window.downcast::()) + { + window + .update(cx, |_, window, _| window.remove_window()) + .ok(); + } + } + }) + .detach(); + cx.observe_new(move |workspace: &mut workspace::Workspace, _, cx| { let workspace_handle = cx.entity().downgrade(); workspace.register_action(move |_workspace, _: &OpenPerformanceProfiler, window, cx| { @@ -71,6 +116,17 @@ pub fn init(startup_time: Instant, cx: &mut App) { .detach(); } +fn update_command_palette_filter(enabled: bool, cx: &mut App) { + CommandPaletteFilter::update_global(cx, |filter, _| { + let action = [TypeId::of::()]; + if enabled { + filter.show_action_types(&action); + } else { + filter.hide_action_types(&action); + } + }); +} + fn open_performance_profiler( startup_time: Instant, workspace_handle: WeakEntity, diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index cff9da17ac9..eba5b309619 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 c54a76b08ea..42bb3e7d2e5 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 a1914670162..921ad7e26a4 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 b04bf128974..5f30ef2aa18 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 34beb9a8e17..5b91a3a8901 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 502f1a7bec6..64dd60558d8 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -6287,39 +6287,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); @@ -12322,7 +12289,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 b8df94c0230..5cca4767a2f 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 8170d303eb8..9dcef1b60e1 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 c79937ab7ad..a4ea449ffc8 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/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index ffff9b7427c..e6f79bf70d4 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -222,6 +222,7 @@ impl VsCodeSettings { which_key: None, modeline_lines: None, feature_flags: None, + instrumentation: None, } } diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index b9accd6b83d..56dbb141316 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 704b13683bb..85cdc73a39c 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -223,6 +223,33 @@ pub struct SettingsContent { /// Local overrides for feature flags, keyed by flag name. pub feature_flags: Option, + + /// Settings for developer-oriented instrumentation tools (profilers, + /// tracers, etc.) that can be toggled at runtime. + pub instrumentation: Option, +} + +/// Configuration for developer-oriented instrumentation tools that collect +/// diagnostic data about a running Zed instance. +#[with_fallible_options] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] +pub struct InstrumentationSettingsContent { + /// Configuration for the performance profiler, accessed via the + /// `zed: open performance profiler` action. + pub performance_profiler: Option, +} + +/// Configuration for the performance profiler which collects timing data +/// for foreground and background executor tasks. +#[with_fallible_options] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] +pub struct PerformanceProfilerSettingsContent { + /// Whether to collect timing data for foreground and background executor + /// tasks. Enabling this may lead to increased memory usage, hence it's + /// disabled by default for regular builds. + /// + /// Default: false + pub enabled: Option, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, MergeFrom)] @@ -647,12 +674,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 3197ae0602a..623e463cfb6 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 b39b2fc61e6..bc6ae06cb43 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 17d89b2f762..b84b7017d49 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::{ @@ -101,6 +100,31 @@ fn developer_page() -> SettingsPage { files: USER, render: crate::pages::render_feature_flags_page, }), + SettingsPageItem::SectionHeader("Instrumentation"), + SettingsPageItem::SettingItem(SettingItem { + title: "Performance Profiler", + description: "Collect timing data for foreground and background executor tasks so they can be inspected via `zed: open performance profiler`. May lead to increased memory usage.", + field: Box::new(SettingField { + json_path: Some("instrumentation.performance_profiler.enabled"), + pick: |settings_content| { + settings_content + .instrumentation + .as_ref() + .and_then(|i| i.performance_profiler.as_ref()) + .and_then(|p| p.enabled.as_ref()) + }, + write: |settings_content, value| { + settings_content + .instrumentation + .get_or_insert_default() + .performance_profiler + .get_or_insert_default() + .enabled = value; + }, + }), + metadata: None, + files: USER, + }), ]), } } @@ -120,7 +144,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 +159,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 +174,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 +187,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 +200,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 +216,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 +236,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 +262,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 +289,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 +305,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 +326,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 +340,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 +363,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 +384,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 +403,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 +447,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 +461,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 +502,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -512,7 +530,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -538,7 +556,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -564,7 +582,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -597,7 +615,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 +672,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -682,7 +700,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -708,7 +726,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -734,7 +752,7 @@ fn appearance_page() -> SettingsPage { _ => None } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -764,7 +782,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 +795,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 +808,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 +832,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 +887,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 +914,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 +932,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 +952,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 +965,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 +978,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 +993,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 +1009,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 +1035,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 +1054,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 +1075,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 +1094,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 +1107,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 +1120,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 +1133,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 +1152,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 +1167,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 +1180,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 +1193,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 +1211,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 +1237,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 +1263,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 +1327,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 +1401,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 +1457,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 +1494,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 +1512,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 +1533,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 +1546,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 +1559,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 +1579,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 +1595,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 +1610,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 +1631,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 +1646,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 +1661,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 +1674,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 +1687,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 +1702,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 +1715,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 +1734,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 +1757,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 +1775,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 +1788,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 +1807,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 +1821,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 +1834,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 +1850,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 +1875,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 +1898,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 +1927,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 +1944,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 +1963,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 +1986,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 +2009,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 +2032,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 +2051,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 +2068,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 +2089,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 +2108,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 +2132,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 +2156,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 +2180,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 +2204,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 +2228,7 @@ fn editor_page() -> SettingsPage { .diagnostics .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2236,7 +2254,7 @@ fn editor_page() -> SettingsPage { .horizontal .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2264,7 +2282,7 @@ fn editor_page() -> SettingsPage { .vertical .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .scrollbar @@ -2291,7 +2309,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 +2329,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 +2348,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 +2372,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 +2396,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 +2420,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 +2450,7 @@ fn editor_page() -> SettingsPage { .breadcrumbs .as_ref() }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { settings_content .editor .toolbar @@ -2456,7 +2474,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 +2498,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 +2522,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 +2546,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 +2569,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 +2588,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 +2606,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 +2624,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 +2640,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 +2659,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 +2677,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 +2701,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 +2727,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 +2753,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 +2779,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 +2800,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 +2847,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 +2870,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 +2889,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 +2919,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 +2945,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 +2971,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 +2997,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 +3029,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 +3056,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 +3082,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 +3152,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 +3176,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 +3193,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 +3213,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 +3232,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 +3245,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 +3264,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 +3286,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 +3312,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 +3330,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 +3352,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 +3374,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 +3403,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 +3425,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 +3442,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 +3457,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 +3485,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 +3507,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 +3529,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 +3551,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 +3567,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 +3580,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 +3595,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 +3612,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 +3631,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 +3659,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 +3681,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 +3703,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 +3725,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 +3743,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 +3761,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 +3783,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 +3801,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 +3830,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 +3910,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 +3939,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 +3952,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 +3965,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 +3980,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 +3997,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 +4017,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 +4039,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 +4061,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 +4085,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 +4103,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 +4121,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 +4145,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 +4167,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 +4189,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 +4211,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 +4233,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 +4255,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 +4277,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 +4299,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 +4320,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 +4344,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 +4366,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 +4389,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 +4415,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 +4428,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 +4454,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 +4478,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 +4495,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 +4519,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 +4537,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 +4573,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 +4592,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 +4614,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 +4636,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 +4654,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 +4676,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 +4694,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 +4716,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 +4738,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 +4760,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 +4782,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 +4804,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 +4830,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 +4856,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 +4880,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 +4902,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 +4924,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 +4946,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 +4971,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 +4994,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 +5012,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 +5034,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 +5052,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 +5069,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 +5094,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 +5120,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 +5146,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 +5167,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 +5188,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 +5201,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 +5220,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 +5244,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 +5260,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 +5279,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 +5297,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 +5319,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 +5337,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 +5359,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 +5381,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 +5403,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 +5428,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 +5442,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 +5451,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 +5464,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 +5479,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 +5497,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 +5519,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 +5537,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 +5559,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 +5577,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 +5592,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 +5610,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 +5628,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 +5650,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 +5660,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 +5676,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 +5700,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 +5725,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 +5743,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 +5765,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 +5787,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 +5800,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 +5813,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 +5828,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 +5843,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 +5867,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 +5895,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 +5939,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 +5961,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 +5977,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 +5996,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 +6018,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 +6059,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 +6134,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 +6165,7 @@ fn terminal_page() -> SettingsPage { _ => None, } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -6202,7 +6198,7 @@ fn terminal_page() -> SettingsPage { _ => None, } }, - write: |settings_content, value, _app: &App| { + write: |settings_content, value| { let Some(value) = value else { return; }; @@ -6236,7 +6232,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 +6271,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 +6339,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 +6365,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 +6381,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 +6412,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 +6431,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 +6454,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 +6474,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 +6497,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 +6524,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 +6544,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 +6560,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 +6579,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 +6601,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 +6625,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 +6643,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 +6665,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 +6681,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 +6702,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 +6720,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 +6748,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 +6770,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 +6800,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 +6834,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 +6884,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 +6923,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 +6949,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 +6975,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 +6991,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 +7018,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 +7044,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 +7070,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 +7096,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 +7122,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 +7154,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 +7186,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 +7210,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 +7223,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 +7256,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 +7271,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 +7305,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 +7328,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 +7357,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 +7370,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 +7406,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 +7426,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 +7444,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 +7466,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 +7488,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 +7506,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 +7528,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 +7550,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 +7572,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 +7594,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 +7616,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 +7634,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 +7652,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 +7678,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 +7703,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 +7739,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 +7755,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 +7823,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 +7842,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 +7861,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 +7880,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 +7905,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 +7924,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 +7943,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 +7963,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 +7988,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 +8016,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 +8038,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 +8060,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 +8085,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 +8106,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 +8136,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 +8160,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 +8179,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 +8198,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 +8221,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 +8246,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 +8266,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 +8297,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 +8316,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 +8335,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 +8355,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 +8380,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 +8400,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 +8426,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 +8457,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 +8476,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 +8495,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 +8514,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 +8534,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 +8549,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 +8572,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 +8591,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 +8613,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 +8632,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 +8654,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 +8676,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 +8695,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 +8717,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 +8746,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 +8780,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 +8800,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 +8825,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 +8850,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 +8870,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 +8891,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 +8908,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 +8927,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 +8942,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 +8965,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 +8985,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 +9001,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 +9019,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 +9033,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 +9090,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 +9110,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 +9135,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 +9152,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 +9183,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 +9204,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 +9223,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 +9248,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 +9267,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 +9289,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 +9315,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 +9346,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 +9365,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 +9385,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 +9411,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 +9437,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 +9449,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 +9459,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 +9479,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 +9514,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 +9523,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 +9539,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 5730dd6fa29..7ae8f0c7816 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 d6696cc7033..cfc0906d5b8 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 9224edc3bed..2de895114d1 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 1f6082e0bfc..8a598c1d773 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 65b1f5d82f0..649c3bdee35 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 aba0040f482..271f95bea2d 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 14e1df38841..fce0e54c720 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 22a2c737048..a739df3213d 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 63ccdacca7a..75ba8ea3918 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 725881f8cc1..be8e5e01d0a 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 c60218003df..8a0e835fa86 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 76c2e2a8b24..93eb4d30aa7 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 08b8532a355..7b8261d27b6 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 9be00af4755..39ffe832746 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 8edfca67349..edc21f91f6f 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 7b17094e080..8916236925e 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 eebad4d4382..fe25852682c 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/helix/surround.rs b/crates/vim/src/helix/surround.rs index 69f59c0506d..a1aa7b21afe 100644 --- a/crates/vim/src/helix/surround.rs +++ b/crates/vim/src/helix/surround.rs @@ -37,6 +37,24 @@ fn find_nearest_surrounding_pair( best_pair } +fn surrounding_markers_containing_cursor( + display_map: &DisplaySnapshot, + cursor: DisplayPoint, + open_marker: char, + close_marker: char, +) -> Option> { + let range = surrounding_markers(display_map, cursor, true, true, open_marker, close_marker)?; + let cursor_offset = cursor.to_offset(display_map, Bias::Left); + let start_offset = range.start.to_offset(display_map, Bias::Left); + let end_offset = range.end.to_offset(display_map, Bias::Right); + + if cursor_offset >= start_offset && cursor_offset <= end_offset { + Some(range) + } else { + None + } +} + fn selection_cursor(map: &DisplaySnapshot, selection: &Selection) -> DisplayPoint { if selection.reversed || selection.is_empty() { selection.head() @@ -134,9 +152,12 @@ impl Vim { continue; }; - if let Some(range) = - surrounding_markers(display_map, cursor, true, true, open_marker, close_marker) - { + if let Some(range) = surrounding_markers_containing_cursor( + display_map, + cursor, + open_marker, + close_marker, + ) { let open_start = range.start.to_offset(display_map, Bias::Left); let open_end = open_start + open_marker.len_utf8(); let close_end = range.end.to_offset(display_map, Bias::Left); @@ -188,9 +209,12 @@ impl Vim { continue; }; - if let Some(range) = - surrounding_markers(display_map, cursor, true, true, open_marker, close_marker) - { + if let Some(range) = surrounding_markers_containing_cursor( + display_map, + cursor, + open_marker, + close_marker, + ) { let open_start = range.start.to_offset(display_map, Bias::Left); let open_end = open_start + open_marker.len_utf8(); let close_end = range.end.to_offset(display_map, Bias::Left); @@ -259,6 +283,22 @@ mod test { cx.simulate_keystrokes("m d ("); cx.assert_state("hello woˇrld test", Mode::HelixNormal); + cx.set_state( + indoc! {" + \"heˇllo\" + fn world() { + }"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("m d ("); + cx.assert_state( + indoc! {" + \"heˇllo\" + fn world() { + }"}, + Mode::HelixNormal, + ); + cx.set_state("((woˇrld))", Mode::HelixNormal); cx.simulate_keystrokes("m d ("); cx.assert_state("(woˇrld)", Mode::HelixNormal); @@ -281,6 +321,22 @@ mod test { cx.simulate_keystrokes("m r \" {"); cx.assert_state("hello {woˇrld} test", Mode::HelixNormal); + cx.set_state( + indoc! {" + \"heˇllo\" + fn world() { + }"}, + Mode::HelixNormal, + ); + cx.simulate_keystrokes("m r ( ["); + cx.assert_state( + indoc! {" + \"heˇllo\" + fn world() { + }"}, + Mode::HelixNormal, + ); + cx.set_state("((woˇrld))", Mode::HelixNormal); cx.simulate_keystrokes("m r ( ["); cx.assert_state("([woˇrld])", Mode::HelixNormal); diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 3bd4c0a5b80..0442387c395 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 a76846fd33d..d9617d34ecf 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 fbaa1b50a0a..e5a9d5ce0f4 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 4a85e91c5a7..5e658659e2b 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 68542f285ac..59b0a5b1cef 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 f858d747de0..f7d007f0f3b 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 c04ae577484..af30b0ee715 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 5b2ba4db4fd..00000000000 --- 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/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 1de73e0486f..e96dfe0655d 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -3089,6 +3089,32 @@ If you wish to exclude certain hosts from using the proxy, set the `NO_PROXY` en } ``` +## Instrumentation + +- Description: Configuration for developer-oriented instrumentation tools (profilers, tracers, etc.) that can be toggled at runtime. +- Setting: `instrumentation` +- Default: + +```json +{ + "instrumentation": { + "performance_profiler": { + "enabled": false + } + } +} +``` + +### Performance Profiler + +- Description: Collects timing data for foreground and background executor tasks so they can be inspected via the `zed: open performance profiler` action. Enabling this may lead to increased memory usage, hence it's disabled by default for regular builds. +- Setting: `instrumentation.performance_profiler.enabled` +- Default: `false` + +**Options** + +`boolean` values + ## Profiles - Description: Configuration profiles that can be temporarily applied on top of existing settings or Zed's defaults. diff --git a/tooling/compliance/src/checks.rs b/tooling/compliance/src/checks.rs index 6558f6e3622..570c87b6554 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 aeea0b2f267..08adcb4b137 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 4d1da9ba5cf..9a2333078f9 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 4de67a0010a..711db20a030 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 f4ae8593e32..46c40fb82c5 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 5501412f1be..af1ba95d1b3 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 644c033f681..5dd8bdd7796 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"))