diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index 1bf6c80e40..e132eca1e5 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..06d418a750 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17521,6 +17521,7 @@ dependencies = [ "icons", "itertools 0.14.0", "menu", + "schemars", "serde", "settings", "smallvec", diff --git a/assets/icons/terminal_ghost.svg b/assets/icons/terminal_ghost.svg deleted file mode 100644 index 7d0d0e068e..0000000000 --- a/assets/icons/terminal_ghost.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/acp_grid.svg b/assets/images/acp_grid.svg deleted file mode 100644 index 8ebff8e1bc..0000000000 --- a/assets/images/acp_grid.svg +++ /dev/null @@ -1,1257 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/acp_logo.svg b/assets/images/acp_logo.svg deleted file mode 100644 index efaa46707b..0000000000 --- a/assets/images/acp_logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/assets/images/acp_logo_serif.svg b/assets/images/acp_logo_serif.svg deleted file mode 100644 index 6bc359cf82..0000000000 --- a/assets/images/acp_logo_serif.svg +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3cca560c00..e84f4834af 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -40,7 +40,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", + "ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +120,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-alt-shift-e": "editor::ToggleEditPrediction", + "ctrl-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json deleted file mode 100644 index c7a6c3149c..0000000000 --- a/assets/keymaps/default-windows.json +++ /dev/null @@ -1,1260 +0,0 @@ -[ - // Standard Windows bindings - { - "use_key_equivalents": true, - "bindings": { - "home": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "pageup": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-pagedown": "menu::SelectLast", - "pagedown": "menu::SelectLast", - "ctrl-n": "menu::SelectNext", - "tab": "menu::SelectNext", - "down": "menu::SelectNext", - "ctrl-p": "menu::SelectPrevious", - "shift-tab": "menu::SelectPrevious", - "up": "menu::SelectPrevious", - "enter": "menu::Confirm", - "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel", - "shift-alt-enter": "menu::Restart", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], - "ctrl-shift-w": "workspace::CloseWindow", - "shift-escape": "workspace::ToggleZoom", - "open": "workspace::Open", - "ctrl-o": "workspace::Open", - "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }], - "ctrl-,": "zed::OpenSettings", - "ctrl-q": "zed::Quit", - "f4": "debugger::Start", - "shift-f5": "debugger::Stop", - "ctrl-shift-f5": "debugger::RerunSession", - "f6": "debugger::Pause", - "f7": "debugger::StepOver", - "ctrl-f11": "debugger::StepInto", - "shift-f11": "debugger::StepOut", - "f11": "zed::ToggleFullScreen", - "ctrl-shift-i": "edit_prediction::ToggleMenu", - "shift-alt-l": "lsp_tool::ToggleMenu" - } - }, - { - "context": "Picker || menu", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-k ctrl-q": "editor::Rewrap", - "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", - "cut": "editor::Cut", - "shift-delete": "editor::Cut", - "ctrl-x": "editor::Cut", - "copy": "editor::Copy", - "ctrl-insert": "editor::Copy", - "ctrl-c": "editor::Copy", - "paste": "editor::Paste", - "shift-insert": "editor::Paste", - "ctrl-v": "editor::Paste", - "undo": "editor::Undo", - "ctrl-z": "editor::Undo", - "redo": "editor::Redo", - "ctrl-y": "editor::Redo", - "ctrl-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "ctrl-up": "editor::LineUp", - "ctrl-down": "editor::LineDown", - "pageup": "editor::MovePageUp", - "alt-pageup": "editor::PageUp", - "shift-pageup": "editor::SelectPageUp", - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "down": "editor::MoveDown", - "pagedown": "editor::MovePageDown", - "alt-pagedown": "editor::PageDown", - "shift-pagedown": "editor::SelectPageDown", - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "left": "editor::MoveLeft", - "right": "editor::MoveRight", - "ctrl-left": "editor::MoveToPreviousWordStart", - "ctrl-right": "editor::MoveToNextWordEnd", - "ctrl-home": "editor::MoveToBeginning", - "ctrl-end": "editor::MoveToEnd", - "shift-up": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-left": "editor::SelectToPreviousWordStart", - "ctrl-shift-right": "editor::SelectToNextWordEnd", - "ctrl-shift-home": "editor::SelectToBeginning", - "ctrl-shift-end": "editor::SelectToEnd", - "ctrl-a": "editor::SelectAll", - "ctrl-l": "editor::SelectLine", - "shift-alt-f": "editor::Format", - "shift-alt-o": "editor::OrganizeImports", - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-alt-space": "editor::ShowCharacterPalette", - "ctrl-;": "editor::ToggleLineNumbers", - "ctrl-'": "editor::ToggleSelectedDiffHunks", - "ctrl-\"": "editor::ExpandAllDiffHunks", - "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "git::Blame", - "alt-g m": "git::OpenModifiedFiles", - "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu", - "ctrl-shift-e": "editor::ToggleEditPrediction", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", - "ctrl-k ctrl-z": "editor::ToggleSoftWrap", - "ctrl-k z": "editor::ToggleSoftWrap", - "find": "buffer_search::Deploy", - "ctrl-f": "buffer_search::Deploy", - "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "assistant::QuoteSelection", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-alt-e": "editor::SelectEnclosingSymbol", - "ctrl-shift-backspace": "editor::GoToPreviousChange", - "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } - }, - { - "context": "Editor && mode == auto_height", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "editor::Newline", - "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } - }, - { - "context": "Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::Copy", - "ctrl-c": "markdown::Copy" - } - }, - { - "context": "Editor && jupyter && !ContextEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } - }, - { - "context": "Editor && !agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-k ctrl-r": "git::Restore", - "alt-y": "git::StageAndNext", - "shift-alt-y": "git::UnstageAndNext" - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "ctrl-shift-r": "agent::OpenAgentDiff" - } - }, - { - "context": "AgentDiff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ContextEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "assistant::Assist", - "ctrl-s": "workspace::Save", - "save": "workspace::Save", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-enter": "assistant::Split", - "ctrl-r": "assistant::CycleMessageRole", - "enter": "assistant::ConfirmCommand", - "alt-enter": "editor::Newline", - "ctrl-k c": "assistant::CopyCode", - "ctrl-g": "search::SelectNextMatch", - "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } - }, - { - "context": "AgentPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewThread", - "shift-alt-n": "agent::NewTextThread", - "ctrl-shift-h": "agent::OpenHistory", - "shift-alt-c": "agent::OpenSettings", - "shift-alt-p": "agent::OpenRulesLibrary", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-alt-/": "agent::ToggleModelSelector", - "ctrl-shift-a": "agent::ToggleContextPicker", - "ctrl-shift-j": "agent::ToggleNavigationMenu", - "ctrl-shift-i": "agent::ToggleOptionsMenu", - // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "assistant::QuoteSelection", - "shift-alt-e": "agent::RemoveAllContext", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-enter": "agent::ContinueThread", - "super-ctrl-b": "agent::ToggleBurnMode", - "alt-enter": "agent::ContinueWithBurnMode" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::CopyAsMarkdown", - "ctrl-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && prompt_editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "AgentPanel && external_agent_thread", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-enter": "agent::ChatWithFollow", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::Chat", - "enter": "editor::Newline", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "EditMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "AgentFeedbackMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "ContextStrip", - "use_key_equivalents": true, - "bindings": { - "up": "agent::FocusUp", - "right": "agent::FocusRight", - "left": "agent::FocusLeft", - "down": "agent::FocusDown", - "backspace": "agent::RemoveFocusedContext", - "enter": "agent::AcceptSuggestedContext" - } - }, - { - "context": "AcpThread > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ThreadHistory", - "use_key_equivalents": true, - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "PromptLibrary", - "use_key_equivalents": true, - "bindings": { - "new": "rules_library::NewRule", - "ctrl-n": "rules_library::NewRule", - "ctrl-shift-s": "rules_library::ToggleDefaultRule" - } - }, - { - "context": "BufferSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "buffer_search::Dismiss", - "tab": "buffer_search::FocusEditor", - "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPreviousMatch", - "alt-enter": "search::SelectAllMatches", - "find": "search::FocusSearch", - "ctrl-f": "search::FocusSearch", - "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } - }, - { - "context": "BufferSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } - }, - { - "context": "BufferSearchBar && !in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "shift-find": "search::FocusSearch", - "ctrl-shift-f": "search::FocusSearch", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "ProjectSearchBar > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "alt-1": ["pane::ActivateItem", 0], - "alt-2": ["pane::ActivateItem", 1], - "alt-3": ["pane::ActivateItem", 2], - "alt-4": ["pane::ActivateItem", 3], - "alt-5": ["pane::ActivateItem", 4], - "alt-6": ["pane::ActivateItem", 5], - "alt-7": ["pane::ActivateItem", 6], - "alt-8": ["pane::ActivateItem", 7], - "alt-9": ["pane::ActivateItem", 8], - "alt-0": "pane::ActivateLastItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-shift-pageup": "pane::SwapItemLeft", - "ctrl-shift-pagedown": "pane::SwapItemRight", - "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes", - "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], - "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], - "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], - "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", - "back": "pane::GoBack", - "alt--": "pane::GoBack", - "alt-=": "pane::GoForward", - "forward": "pane::GoForward", - "f3": "search::SelectNextMatch", - "shift-f3": "search::SelectPreviousMatch", - "shift-find": "project_search::ToggleFocus", - "ctrl-shift-f": "project_search::ToggleFocus", - "shift-alt-h": "search::ToggleReplace", - "alt-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-c": "search::ToggleCaseSensitive", - "alt-w": "search::ToggleWholeWord", - "alt-find": "project_search::ToggleFilters", - "alt-f": "project_search::ToggleFilters", - "alt-r": "search::ToggleRegex", - // "ctrl-shift-alt-x": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "editor::Outdent", - "ctrl-]": "editor::Indent", - "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above - "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below - "ctrl-shift-k": "editor::DeleteLine", - "alt-up": "editor::MoveLineUp", - "alt-down": "editor::MoveLineDown", - "shift-alt-up": "editor::DuplicateLineUp", - "shift-alt-down": "editor::DuplicateLineDown", - "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection - "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection - "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection - "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word - "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch - "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch - "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip - "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch - "ctrl-k ctrl-i": "editor::Hover", - "ctrl-k ctrl-b": "editor::BlameHover", - "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], - "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "f2": "editor::Rename", - "f12": "editor::GoToDefinition", - "alt-f12": "editor::GoToDefinitionSplit", - "ctrl-shift-f10": "editor::GoToDefinitionSplit", - "ctrl-f12": "editor::GoToImplementation", - "shift-f12": "editor::GoToTypeDefinition", - "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit", - "shift-alt-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains - "ctrl-shift-\\": "editor::MoveToEnclosingBracket", - "ctrl-shift-[": "editor::Fold", - "ctrl-shift-]": "editor::UnfoldLines", - "ctrl-k ctrl-l": "editor::ToggleFold", - "ctrl-k ctrl-[": "editor::FoldRecursive", - "ctrl-k ctrl-]": "editor::UnfoldRecursive", - "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1], - "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2], - "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3], - "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4], - "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5], - "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6], - "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7], - "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8], - "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9], - "ctrl-k ctrl-0": "editor::FoldAll", - "ctrl-k ctrl-j": "editor::UnfoldAll", - "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowWordCompletions", - "ctrl-.": "editor::ToggleCodeActions", - "ctrl-k r": "editor::RevealInFileManager", - "ctrl-k p": "editor::CopyPath", - "ctrl-\\": "pane::SplitRight", - "ctrl-shift-alt-c": "editor::DisplayCursorNames", - "alt-.": "editor::GoToHunk", - "alt-,": "editor::GoToPreviousHunk" - } - }, - { - "context": "Editor && extension == md", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "markdown::OpenPreviewToTheSide", - "ctrl-shift-v": "markdown::OpenPreview" - } - }, - { - "context": "Editor && extension == svg", - "use_key_equivalents": true, - "bindings": { - "ctrl-k v": "svg::OpenPreviewToTheSide", - "ctrl-shift-v": "svg::OpenPreview" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], - // Change the default action on `menu::Confirm` by setting the parameter - // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], - "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], - "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - // Change to open path modal for existing remote connection by setting the parameter - // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", - "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "shift-alt-b": "branches::OpenRecent", - "shift-alt-enter": "toast::RunAction", - "ctrl-shift-`": "workspace::NewTerminal", - "save": "workspace::Save", - "ctrl-s": "workspace::Save", - "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "shift-save": "workspace::SaveAs", - "ctrl-shift-s": "workspace::SaveAs", - "new": "workspace::NewFile", - "ctrl-n": "workspace::NewFile", - "shift-new": "workspace::NewWindow", - "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", - "f10": ["app_menu::OpenApplicationMenu", "Zed"], - "alt-1": ["workspace::ActivatePane", 0], - "alt-2": ["workspace::ActivatePane", 1], - "alt-3": ["workspace::ActivatePane", 2], - "alt-4": ["workspace::ActivatePane", 3], - "alt-5": ["workspace::ActivatePane", 4], - "alt-6": ["workspace::ActivatePane", 5], - "alt-7": ["workspace::ActivatePane", 6], - "alt-8": ["workspace::ActivatePane", 7], - "alt-9": ["workspace::ActivatePane", 8], - "ctrl-alt-b": "workspace::ToggleRightDock", - "ctrl-b": "workspace::ToggleLeftDock", - "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-shift-y": "workspace::CloseAllDocks", - "alt-r": "workspace::ResetActiveDockSize", - // For 0px parameter, uses UI font size value. - "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], - "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], - "shift-alt-0": "workspace::ResetOpenDocksSize", - "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], - "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], - "shift-find": "pane::DeploySearch", - "ctrl-shift-f": "pane::DeploySearch", - "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], - "ctrl-shift-t": "pane::ReopenClosedItem", - "ctrl-k ctrl-s": "zed::OpenKeymapEditor", - "ctrl-k ctrl-t": "theme_selector::Toggle", - "ctrl-alt-super-p": "settings_profile_selector::Toggle", - "ctrl-t": "project_symbols::Toggle", - "ctrl-p": "file_finder::Toggle", - "ctrl-tab": "tab_switcher::Toggle", - "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-e": "file_finder::Toggle", - "f1": "command_palette::Toggle", - "ctrl-shift-p": "command_palette::Toggle", - "ctrl-shift-m": "diagnostics::Deploy", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-b": "outline_panel::ToggleFocus", - "ctrl-shift-g": "git_panel::ToggleFocus", - "ctrl-shift-d": "debug_panel::ToggleFocus", - "ctrl-shift-/": "agent::ToggleFocus", - "alt-save": "workspace::SaveAll", - "ctrl-k s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "escape": "workspace::Unfollow", - "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", - "ctrl-k ctrl-right": "workspace::ActivatePaneRight", - "ctrl-k ctrl-up": "workspace::ActivatePaneUp", - "ctrl-k ctrl-down": "workspace::ActivatePaneDown", - "ctrl-k shift-left": "workspace::SwapPaneLeft", - "ctrl-k shift-right": "workspace::SwapPaneRight", - "ctrl-k shift-up": "workspace::SwapPaneUp", - "ctrl-k shift-down": "workspace::SwapPaneDown", - "ctrl-shift-x": "zed::Extensions", - "ctrl-shift-r": "task::Rerun", - "alt-t": "task::Rerun", - "shift-alt-t": "task::Spawn", - "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }], - // also possible to spawn tasks by name: - // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] - // or by tag: - // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], - "f5": "debugger::Rerun", - "ctrl-f4": "workspace::CloseActiveDock", - "ctrl-w": "workspace::CloseActiveDock" - } - }, - { - "context": "Workspace && debugger_running", - "use_key_equivalents": true, - "bindings": { - "f5": "zed::NoAction" - } - }, - { - "context": "Workspace && debugger_stopped", - "use_key_equivalents": true, - "bindings": { - "f5": "debugger::Continue" - } - }, - { - "context": "ApplicationMenu", - "use_key_equivalents": true, - "bindings": { - "f10": "menu::Cancel", - "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-u": "editor::UndoSelection", - "ctrl-shift-u": "editor::RedoSelection", - "ctrl-shift-j": "editor::JoinLines", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "shift-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "shift-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd" - } - }, - // Bindings from Atom - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "ctrl-k up": "pane::SplitUp", - "ctrl-k down": "pane::SplitDown", - "ctrl-k left": "pane::SplitLeft", - "ctrl-k right": "pane::SplitRight" - } - }, - // Bindings that should be unified with bindings for more general actions - { - "context": "Editor && renaming", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmRename" - } - }, - { - "context": "Editor && showing_completions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCompletion", - "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } - }, - // Bindings for accepting edit predictions - // - // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is - // because alt-tab may not be available, as it is often used for window switching. - { - "context": "Editor && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && edit_prediction_conflict", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && showing_code_actions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCodeAction" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "editor::ContextMenuPrevious", - "up": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext", - "down": "editor::ContextMenuNext", - "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } - }, - // Custom bindings - { - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-alt-f": "workspace::FollowNextCollaborator", - // Only available in debug builds: opens an element inspector for development. - "shift-alt-i": "dev::ToggleInspector" - } - }, - { - "context": "!Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } - }, - { - "context": "!ContextEditor > Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "alt-enter": "editor::OpenExcerpts", - "shift-enter": "editor::ExpandExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit", - "ctrl-shift-e": "pane::RevealInProjectPanel", - "ctrl-f8": "editor::GoToHunk", - "ctrl-shift-f8": "editor::GoToPreviousHunk", - "ctrl-enter": "assistant::InlineAssist", - "ctrl-shift-;": "editor::ToggleInlayHints" - } - }, - { - "context": "PromptEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "agent::CyclePreviousInlineAssist", - "ctrl-]": "agent::CycleNextInlineAssist", - "shift-alt-e": "agent::RemoveAllContext" - } - }, - { - "context": "Prompt", - "use_key_equivalents": true, - "bindings": { - "left": "menu::SelectPrevious", - "right": "menu::SelectNext", - "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } - }, - { - "context": "ProjectSearchBar && !in_replace", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "alt-copy": "outline_panel::CopyPath", - "shift-alt-c": "outline_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-shift-alt-c": "workspace::CopyRelativePath", - "ctrl-alt-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::OpenSelectedEntry", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } - }, - { - "context": "ProjectPanel", - "use_key_equivalents": true, - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry", - "new": "project_panel::NewFile", - "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", - "alt-n": "project_panel::NewDirectory", - "cut": "project_panel::Cut", - "ctrl-x": "project_panel::Cut", - "copy": "project_panel::Copy", - "ctrl-insert": "project_panel::Copy", - "ctrl-c": "project_panel::Copy", - "paste": "project_panel::Paste", - "shift-insert": "project_panel::Paste", - "ctrl-v": "project_panel::Paste", - "alt-copy": "project_panel::CopyPath", - "shift-alt-c": "project_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", - "enter": "project_panel::Rename", - "f2": "project_panel::Rename", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-alt-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", - "shift-find": "project_panel::NewSearchInDirectory", - "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } - }, - { - "context": "ProjectPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "space": "project_panel::Open" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "enter": "menu::Confirm", - "alt-y": "git::StageFile", - "shift-alt-y": "git::UnstageFile", - "space": "git::ToggleStaged", - "shift-space": "git::StageRange", - "tab": "git_panel::FocusEditor", - "shift-tab": "git_panel::FocusEditor", - "escape": "git_panel::ToggleFocus", - "alt-enter": "menu::SecondaryConfirm", - "delete": ["git::RestoreFile", { "skip_prompt": false }], - "backspace": ["git::RestoreFile", { "skip_prompt": false }], - "shift-delete": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }] - } - }, - { - "context": "GitPanel && CommitEditor", - "use_key_equivalents": true, - "bindings": { - "escape": "git::Cancel" - } - }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "GitPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-g ctrl-g": "git::Fetch", - "ctrl-g up": "git::Push", - "ctrl-g down": "git::Pull", - "ctrl-g shift-up": "git::ForcePush", - "ctrl-g d": "git::Diff", - "ctrl-g backspace": "git::RestoreTrackedFiles", - "ctrl-g shift-backspace": "git::TrashUntrackedFiles", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend" - } - }, - { - "context": "GitDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } - }, - { - "context": "AskPass > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm" - } - }, - { - "context": "CommitEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "git_panel::FocusChanges", - "tab": "git_panel::FocusChanges", - "shift-tab": "git_panel::FocusChanges", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-up": "git_panel::FocusChanges", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "DebugPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-t": "debugger::ToggleThreadPicker", - "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } - }, - { - "context": "VariableList", - "use_key_equivalents": true, - "bindings": { - "left": "variable_list::CollapseSelectedEntry", - "right": "variable_list::ExpandSelectedEntry", - "enter": "variable_list::EditVariable", - "ctrl-c": "variable_list::CopyVariableValue", - "ctrl-alt-c": "variable_list::CopyVariableName", - "delete": "variable_list::RemoveWatch", - "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } - }, - { - "context": "BreakpointList", - "use_key_equivalents": true, - "bindings": { - "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint", - "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } - }, - { - "context": "CollabPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } - }, - { - "context": "CollabPanel", - "use_key_equivalents": true, - "bindings": { - "alt-up": "collab_panel::MoveChannelUp", - "alt-down": "collab_panel::MoveChannelDown" - } - }, - { - "context": "(CollabPanel && editing) > Editor", - "use_key_equivalents": true, - "bindings": { - "space": "collab_panel::InsertSpace" - } - }, - { - "context": "ChannelModal", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "file_finder::Toggle", - "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-p": "file_finder::SelectPrevious", - "ctrl-j": "pane::SplitDown", - "ctrl-k": "pane::SplitUp", - "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } - }, - { - "context": "TabSwitcher", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-tab": "menu::SelectPrevious", - "ctrl-up": "menu::SelectPrevious", - "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-alt-space": "terminal::ShowCharacterPalette", - "copy": "terminal::Copy", - "ctrl-insert": "terminal::Copy", - "ctrl-shift-c": "terminal::Copy", - "paste": "terminal::Paste", - "shift-insert": "terminal::Paste", - "ctrl-shift-v": "terminal::Paste", - "ctrl-enter": "assistant::InlineAssist", - "alt-b": ["terminal::SendText", "\u001bb"], - "alt-f": ["terminal::SendText", "\u001bf"], - "alt-.": ["terminal::SendText", "\u001b."], - "ctrl-delete": ["terminal::SendText", "\u001bd"], - // Overrides for conflicting keybindings - "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"], - "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], - "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"], - "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"], - "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-shift-a": "editor::SelectAll", - "find": "buffer_search::Deploy", - "ctrl-shift-f": "buffer_search::Deploy", - "ctrl-shift-l": "terminal::Clear", - "ctrl-shift-w": "pane::CloseActiveItem", - "up": ["terminal::SendKeystroke", "up"], - "pageup": ["terminal::SendKeystroke", "pageup"], - "down": ["terminal::SendKeystroke", "down"], - "pagedown": ["terminal::SendKeystroke", "pagedown"], - "escape": ["terminal::SendKeystroke", "escape"], - "enter": ["terminal::SendKeystroke", "enter"], - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown", - "shift-up": "terminal::ScrollLineUp", - "shift-down": "terminal::ScrollLineDown", - "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode", - "ctrl-shift-r": "terminal::RerunTask", - "ctrl-alt-r": "terminal::RerunTask", - "alt-t": "terminal::RerunTask" - } - }, - { - "context": "ZedPredictModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "ConfigureContextServerModal > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "OnboardingAiConfigurationModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "Diagnostics", - "use_key_equivalents": true, - "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } - }, - { - "context": "DebugConsole > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } - }, - { - "context": "RunModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } - }, - { - "context": "MarkdownPreview", - "use_key_equivalents": true, - "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-f": "search::FocusSearch", - "alt-find": "keymap_editor::ToggleKeystrokeSearch", - "alt-f": "keymap_editor::ToggleKeystrokeSearch", - "alt-c": "keymap_editor::ToggleConflictFilter", - "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding", - "ctrl-c": "keymap_editor::CopyAction", - "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } - }, - { - "context": "KeystrokeInput", - "use_key_equivalents": true, - "bindings": { - "enter": "keystroke_input::StartRecording", - "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } - }, - { - "context": "KeybindEditorModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } - }, - { - "context": "KeybindEditorModal > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Onboarding", - "use_key_equivalents": true, - "bindings": { - "ctrl-1": "onboarding::ActivateBasicsPage", - "ctrl-2": "onboarding::ActivateEditingPage", - "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", - "shift-alt-a": "onboarding::OpenAccount" - } - } -] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd3..62e50b3c8c 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,13 +428,11 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", - "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", - "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", diff --git a/assets/settings/default.json b/assets/settings/default.json index 804198090f..f0b9e11e57 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -653,8 +653,6 @@ // "never" "show": "always" }, - // Whether to enable drag-and-drop operations in the project panel. - "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window. "hide_root": false }, diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 5cead67b6d..a79c550671 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -43,8 +43,8 @@ // "args": ["--login"] // } // } - "shell": "system" + "shell": "system", // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index e20a040e9d..ee12b04cde 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -21,12 +21,12 @@ use ui::prelude::*; use util::ResultExt as _; use workspace::{Item, Workspace}; -actions!(dev, [OpenAcpLogs]); +actions!(acp, [OpenDebugTools]); pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &OpenAcpLogs, window, cx| { + workspace.register_action(|workspace, _: &OpenDebugTools, window, cx| { let acp_tools = Box::new(cx.new(|cx| AcpTools::new(workspace.project().clone(), cx))); workspace.add_item_to_active_pane(acp_tools, None, true, window, cx); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 7b70fde56a..899e360ab0 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -664,7 +664,7 @@ impl Thread { } pub fn get_or_init_configured_model(&mut self, cx: &App) -> Option { - if self.configured_model.is_none() { + if self.configured_model.is_none() || self.messages.is_empty() { self.configured_model = LanguageModelRegistry::read_global(cx).default_model(); } self.configured_model.clone() @@ -2097,7 +2097,7 @@ impl Thread { } pub fn summarize(&mut self, cx: &mut Context) { - let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model() else { + let Some(model) = LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { println!("No thread summary model"); return; }; @@ -2416,7 +2416,7 @@ impl Thread { } let Some(ConfiguredModel { model, provider }) = - LanguageModelRegistry::read_global(cx).thread_summary_model() + LanguageModelRegistry::read_global(cx).thread_summary_model(cx) else { return; }; @@ -5410,13 +5410,10 @@ fn main() {{ }), cx, ); - registry.set_thread_summary_model( - Some(ConfiguredModel { - provider, - model: model.clone(), - }), - cx, - ); + registry.set_thread_summary_model(Some(ConfiguredModel { + provider, + model: model.clone(), + })); }) }); diff --git a/crates/agent2/src/agent.rs b/crates/agent2/src/agent.rs index 6fa36d33d5..ecfaea4b49 100644 --- a/crates/agent2/src/agent.rs +++ b/crates/agent2/src/agent.rs @@ -228,7 +228,7 @@ impl NativeAgent { ) -> Entity { let connection = Rc::new(NativeAgentConnection(cx.entity())); let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); + let summarization_model = registry.thread_summary_model(cx).map(|c| c.model); thread_handle.update(cx, |thread, cx| { thread.set_summarization_model(summarization_model, cx); @@ -524,7 +524,7 @@ impl NativeAgent { let registry = LanguageModelRegistry::read_global(cx); let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model().map(|m| m.model); + let summarization_model = registry.thread_summary_model(cx).map(|m| m.model); for session in self.sessions.values_mut() { session.thread.update(cx, |thread, cx| { diff --git a/crates/agent2/src/tests/mod.rs b/crates/agent2/src/tests/mod.rs index fbeee46a48..864fbf8b10 100644 --- a/crates/agent2/src/tests/mod.rs +++ b/crates/agent2/src/tests/mod.rs @@ -72,7 +72,6 @@ async fn test_echo(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_thinking(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -472,7 +471,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) { tool_name: ToolRequiringPermission::name().into(), is_error: true, content: "Permission to run tool denied by user".into(), - output: Some("Permission to run tool denied by user".into()) + output: None }) ] ); @@ -1348,7 +1347,6 @@ async fn test_cancellation(cx: &mut TestAppContext) { } #[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] // TODO: Fix this test on Windows async fn test_in_progress_send_canceled_by_next_send(cx: &mut TestAppContext) { let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; let fake_model = model.as_fake(); @@ -1822,11 +1820,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) { let clock = Arc::new(clock::FakeSystemClock::new()); let client = Client::new(clock, http_client, cx); let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); + Project::init_settings(cx); + agent_settings::init(cx); language_model::init(client.clone(), cx); language_models::init(user_store, client.clone(), cx); - Project::init_settings(cx); LanguageModelRegistry::test(cx); - agent_settings::init(cx); }); cx.executor().forbid_parking(); diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 97ea1caf1d..1b1c014b79 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -732,17 +732,7 @@ impl Thread { stream.update_tool_call_fields( &tool_use.id, acp::ToolCallUpdateFields { - status: Some( - tool_result - .as_ref() - .map_or(acp::ToolCallStatus::Failed, |result| { - if result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - } - }), - ), + status: Some(acp::ToolCallStatus::Completed), raw_output: output, ..Default::default() }, @@ -1567,7 +1557,7 @@ impl Thread { tool_name: tool_use.name, is_error: true, content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: Some(error.to_string().into()), + output: None, }, } })) @@ -2469,30 +2459,6 @@ impl ToolCallEventStreamReceiver { } } - pub async fn expect_update_fields(&mut self) -> acp::ToolCallUpdateFields { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - update, - )))) = event - { - update.fields - } else { - panic!("Expected update fields but got: {:?}", event); - } - } - - pub async fn expect_diff(&mut self) -> Entity { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateDiff( - update, - )))) = event - { - update.diff - } else { - panic!("Expected diff but got: {:?}", event); - } - } - pub async fn expect_terminal(&mut self) -> Entity { let event = self.0.next().await; if let Some(Ok(ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateTerminal( diff --git a/crates/agent2/src/tools/edit_file_tool.rs b/crates/agent2/src/tools/edit_file_tool.rs index f86bfd25f7..5a68d0c70a 100644 --- a/crates/agent2/src/tools/edit_file_tool.rs +++ b/crates/agent2/src/tools/edit_file_tool.rs @@ -273,13 +273,6 @@ impl AgentTool for EditFileTool { let diff = cx.new(|cx| Diff::new(buffer.clone(), cx))?; event_stream.update_diff(diff.clone()); - let _finalize_diff = util::defer({ - let diff = diff.downgrade(); - let mut cx = cx.clone(); - move || { - diff.update(&mut cx, |diff, cx| diff.finalize(cx)).ok(); - } - }); let old_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; let old_text = cx @@ -396,6 +389,8 @@ impl AgentTool for EditFileTool { }) .await; + diff.update(cx, |diff, cx| diff.finalize(cx)).ok(); + let input_path = input.path.display(); if unified_diff.is_empty() { anyhow::ensure!( @@ -1550,100 +1545,6 @@ mod tests { ); } - #[gpui::test] - async fn test_diff_finalization(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({"main.rs": ""})).await; - - let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; - let languages = project.read_with(cx, |project, _cx| project.languages().clone()); - let context_server_registry = - cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - let model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry.clone(), - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // Ensure the diff is finalized after the edit completes. - { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - cx.run_until_parked(); - model.end_last_completion_stream(); - edit.await.unwrap(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - - // Ensure the diff is finalized if an error occurs while editing. - { - model.forbid_requests(); - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - edit.await.unwrap_err(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - model.allow_requests(); - } - - // Ensure the diff is finalized if the tool call gets dropped. - { - let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone())); - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let edit = cx.update(|cx| { - tool.run( - EditFileToolInput { - display_description: "Edit file".into(), - path: path!("/main.rs").into(), - mode: EditFileMode::Edit, - }, - stream_tx, - cx, - ) - }); - stream_rx.expect_update_fields().await; - let diff = stream_rx.expect_diff().await; - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Pending(_)))); - drop(edit); - cx.run_until_parked(); - diff.read_with(cx, |diff, _| assert!(matches!(diff, Diff::Finalized(_)))); - } - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index b4e897374a..9080fc1ab0 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -162,34 +162,12 @@ impl AgentConnection for AcpConnection { let conn = self.connection.clone(); let sessions = self.sessions.clone(); let cwd = cwd.to_path_buf(); - let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = context_server_store - .configured_server_ids() - .iter() - .filter_map(|id| { - let configuration = context_server_store.configuration_for_server(id)?; - let command = configuration.command(); - Some(acp::McpServer { - name: id.0.to_string(), - command: command.path.clone(), - args: command.args.clone(), - env: if let Some(env) = command.env.as_ref() { - env.iter() - .map(|(name, value)| acp::EnvVariable { - name: name.clone(), - value: value.clone(), - }) - .collect() - } else { - vec![] - }, - }) - }) - .collect(); - cx.spawn(async move |cx| { let response = conn - .new_session(acp::NewSessionRequest { mcp_servers, cwd }) + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) .await .map_err(|err| { if err.code == acp::ErrorCode::AUTH_REQUIRED.code { diff --git a/crates/agent_servers/src/acp/v0.rs b/crates/agent_servers/src/acp/v0.rs new file mode 100644 index 0000000000..be96048929 --- /dev/null +++ b/crates/agent_servers/src/acp/v0.rs @@ -0,0 +1,524 @@ +// Translates old acp agents into the new schema +use action_log::ActionLog; +use agent_client_protocol as acp; +use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; +use anyhow::{Context as _, Result, anyhow}; +use futures::channel::oneshot; +use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; +use project::Project; +use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; +use ui::App; +use util::ResultExt as _; + +use crate::AgentServerCommand; +use acp_thread::{AcpThread, AgentConnection, AuthRequired}; + +#[derive(Clone)] +struct OldAcpClientDelegate { + thread: Rc>>, + cx: AsyncApp, + next_tool_call_id: Rc>, + // sent_buffer_versions: HashMap, HashMap>, +} + +impl OldAcpClientDelegate { + fn new(thread: Rc>>, cx: AsyncApp) -> Self { + Self { + thread, + cx, + next_tool_call_id: Rc::new(RefCell::new(0)), + } + } +} + +impl acp_old::Client for OldAcpClientDelegate { + async fn stream_assistant_message_chunk( + &self, + params: acp_old::StreamAssistantMessageChunkParams, + ) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread + .borrow() + .update(cx, |thread, cx| match params.chunk { + acp_old::AssistantMessageChunk::Text { text } => { + thread.push_assistant_content_block(text.into(), false, cx) + } + acp_old::AssistantMessageChunk::Thought { thought } => { + thread.push_assistant_content_block(thought.into(), true, cx) + } + }) + .log_err(); + })?; + + Ok(()) + } + + async fn request_tool_call_confirmation( + &self, + request: acp_old::RequestToolCallConfirmationParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + let old_acp_id = *self.next_tool_call_id.borrow() + 1; + self.next_tool_call_id.replace(old_acp_id); + + let tool_call = into_new_tool_call( + acp::ToolCallId(old_acp_id.to_string().into()), + request.tool_call, + ); + + let mut options = match request.confirmation { + acp_old::ToolCallConfirmation::Edit { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow Edits".to_string(), + )], + acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", root_command), + )], + acp_old::ToolCallConfirmation::Mcp { + server_name, + tool_name, + .. + } => vec![ + ( + acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", server_name), + ), + ( + acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool, + acp::PermissionOptionKind::AllowAlways, + format!("Always Allow {}", tool_name), + ), + ], + acp_old::ToolCallConfirmation::Fetch { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow".to_string(), + )], + acp_old::ToolCallConfirmation::Other { .. } => vec![( + acp_old::ToolCallConfirmationOutcome::AlwaysAllow, + acp::PermissionOptionKind::AllowAlways, + "Always Allow".to_string(), + )], + }; + + options.extend([ + ( + acp_old::ToolCallConfirmationOutcome::Allow, + acp::PermissionOptionKind::AllowOnce, + "Allow".to_string(), + ), + ( + acp_old::ToolCallConfirmationOutcome::Reject, + acp::PermissionOptionKind::RejectOnce, + "Reject".to_string(), + ), + ]); + + let mut outcomes = Vec::with_capacity(options.len()); + let mut acp_options = Vec::with_capacity(options.len()); + + for (index, (outcome, kind, label)) in options.into_iter().enumerate() { + outcomes.push(outcome); + acp_options.push(acp::PermissionOption { + id: acp::PermissionOptionId(index.to_string().into()), + name: label, + kind, + }) + } + + let response = cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) + }) + })?? + .context("Failed to update thread")? + .await; + + let outcome = match response { + Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)], + Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel, + }; + + Ok(acp_old::RequestToolCallConfirmationResponse { + id: acp_old::ToolCallId(old_acp_id), + outcome, + }) + } + + async fn push_tool_call( + &self, + request: acp_old::PushToolCallParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + let old_acp_id = *self.next_tool_call_id.borrow() + 1; + self.next_tool_call_id.replace(old_acp_id); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.upsert_tool_call( + into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request), + cx, + ) + }) + })?? + .context("Failed to update thread")?; + + Ok(acp_old::PushToolCallResponse { + id: acp_old::ToolCallId(old_acp_id), + }) + } + + async fn update_tool_call( + &self, + request: acp_old::UpdateToolCallParams, + ) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.update_tool_call( + acp::ToolCallUpdate { + id: acp::ToolCallId(request.tool_call_id.0.to_string().into()), + fields: acp::ToolCallUpdateFields { + status: Some(into_new_tool_call_status(request.status)), + content: Some( + request + .content + .into_iter() + .map(into_new_tool_call_content) + .collect::>(), + ), + ..Default::default() + }, + }, + cx, + ) + }) + })? + .context("Failed to update thread")??; + + Ok(()) + } + + async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.update_plan( + acp::Plan { + entries: request + .entries + .into_iter() + .map(into_new_plan_entry) + .collect(), + }, + cx, + ) + }) + })? + .context("Failed to update thread")?; + + Ok(()) + } + + async fn read_text_file( + &self, + acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams, + ) -> Result { + let content = self + .cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.read_text_file(path, line, limit, false, cx) + }) + })? + .context("Failed to update thread")? + .await?; + Ok(acp_old::ReadTextFileResponse { content }) + } + + async fn write_text_file( + &self, + acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams, + ) -> Result<(), acp_old::Error> { + self.cx + .update(|cx| { + self.thread + .borrow() + .update(cx, |thread, cx| thread.write_text_file(path, content, cx)) + })? + .context("Failed to update thread")? + .await?; + + Ok(()) + } +} + +fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { + acp::ToolCall { + id, + title: request.label, + kind: acp_kind_from_old_icon(request.icon), + status: acp::ToolCallStatus::InProgress, + content: request + .content + .into_iter() + .map(into_new_tool_call_content) + .collect(), + locations: request + .locations + .into_iter() + .map(into_new_tool_call_location) + .collect(), + raw_input: None, + raw_output: None, + } +} + +fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind { + match icon { + acp_old::Icon::FileSearch => acp::ToolKind::Search, + acp_old::Icon::Folder => acp::ToolKind::Search, + acp_old::Icon::Globe => acp::ToolKind::Search, + acp_old::Icon::Hammer => acp::ToolKind::Other, + acp_old::Icon::LightBulb => acp::ToolKind::Think, + acp_old::Icon::Pencil => acp::ToolKind::Edit, + acp_old::Icon::Regex => acp::ToolKind::Search, + acp_old::Icon::Terminal => acp::ToolKind::Execute, + } +} + +fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus { + match status { + acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress, + acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed, + acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed, + } +} + +fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent { + match content { + acp_old::ToolCallContent::Markdown { markdown } => markdown.into(), + acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff { + diff: into_new_diff(diff), + }, + } +} + +fn into_new_diff(diff: acp_old::Diff) -> acp::Diff { + acp::Diff { + path: diff.path, + old_text: diff.old_text, + new_text: diff.new_text, + } +} + +fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation { + acp::ToolCallLocation { + path: location.path, + line: location.line, + } +} + +fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry { + acp::PlanEntry { + content: entry.content, + priority: into_new_plan_priority(entry.priority), + status: into_new_plan_status(entry.status), + } +} + +fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority { + match priority { + acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low, + acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, + acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High, + } +} + +fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus { + match status { + acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, + acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, + acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, + } +} + +pub struct AcpConnection { + pub name: &'static str, + pub connection: acp_old::AgentConnection, + pub _child_status: Task>, + pub current_thread: Rc>>, +} + +impl AcpConnection { + pub fn stdio( + name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Task> { + let root_dir = root_dir.to_path_buf(); + + cx.spawn(async move |cx| { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + let stdin = child.stdin.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + log::trace!("Spawned (pid: {})", child.id()); + + let foreground_executor = cx.foreground_executor().clone(); + + let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); + + let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( + OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), + stdin, + stdout, + move |fut| foreground_executor.spawn(fut).detach(), + ); + + let io_task = cx.background_spawn(async move { + io_fut.await.log_err(); + }); + + let child_status = cx.background_spawn(async move { + let result = match child.status().await { + Err(e) => Err(anyhow!(e)), + Ok(result) if result.success() => Ok(()), + Ok(result) => Err(anyhow!(result)), + }; + drop(io_task); + result + }); + + Ok(Self { + name, + connection, + _child_status: child_status, + current_thread: thread_rc, + }) + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut App, + ) -> Task>> { + let task = self.connection.request_any( + acp_old::InitializeParams { + protocol_version: acp_old::ProtocolVersion::latest(), + } + .into_any(), + ); + let current_thread = self.current_thread.clone(); + cx.spawn(async move |cx| { + let result = task.await?; + let result = acp_old::InitializeParams::response_from_any(result)?; + + if !result.is_authenticated { + anyhow::bail!(AuthRequired::new()) + } + + cx.update(|cx| { + let thread = cx.new(|cx| { + let session_id = acp::SessionId("acp-old-no-id".into()); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + AcpThread::new(self.name, self.clone(), project, action_log, session_id) + }); + current_thread.replace(thread.downgrade()); + thread + }) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &[] + } + + fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let task = self + .connection + .request_any(acp_old::AuthenticateParams.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let chunks = params + .prompt + .into_iter() + .filter_map(|block| match block { + acp::ContentBlock::Text(text) => { + Some(acp_old::UserMessageChunk::Text { text: text.text }) + } + acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path { + path: link.uri.into(), + }), + _ => None, + }) + .collect(); + + let task = self + .connection + .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); + cx.foreground_executor().spawn(async move { + task.await?; + anyhow::Ok(acp::PromptResponse { + stop_reason: acp::StopReason::EndTurn, + }) + }) + } + + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + acp::PromptCapabilities { + image: false, + audio: false, + embedded_context: false, + } + } + + fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { + let task = self + .connection + .request_any(acp_old::CancelSendMessageParams.into_any()); + cx.foreground_executor() + .spawn(async move { + task.await?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + + fn into_any(self: Rc) -> Rc { + self + } +} diff --git a/crates/agent_servers/src/acp/v1.rs b/crates/agent_servers/src/acp/v1.rs new file mode 100644 index 0000000000..1945ad2483 --- /dev/null +++ b/crates/agent_servers/src/acp/v1.rs @@ -0,0 +1,376 @@ +use acp_tools::AcpConnectionRegistry; +use action_log::ActionLog; +use agent_client_protocol::{self as acp, Agent as _, ErrorCode}; +use anyhow::anyhow; +use collections::HashMap; +use futures::AsyncBufReadExt as _; +use futures::channel::oneshot; +use futures::io::BufReader; +use project::Project; +use serde::Deserialize; +use std::path::Path; +use std::rc::Rc; +use std::{any::Any, cell::RefCell}; + +use anyhow::{Context as _, Result}; +use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity}; + +use crate::{AgentServerCommand, acp::UnsupportedVersion}; +use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError}; + +pub struct AcpConnection { + server_name: &'static str, + connection: Rc, + sessions: Rc>>, + auth_methods: Vec, + prompt_capabilities: acp::PromptCapabilities, + _io_task: Task>, +} + +pub struct AcpSession { + thread: WeakEntity, + suppress_abort_err: bool, +} + +const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1; + +impl AcpConnection { + pub async fn stdio( + server_name: &'static str, + command: AgentServerCommand, + root_dir: &Path, + cx: &mut AsyncApp, + ) -> Result { + let mut child = util::command::new_smol_command(&command.path) + .args(command.args.iter().map(|arg| arg.as_str())) + .envs(command.env.iter().flatten()) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .kill_on_drop(true) + .spawn()?; + + let stdout = child.stdout.take().context("Failed to take stdout")?; + let stdin = child.stdin.take().context("Failed to take stdin")?; + let stderr = child.stderr.take().context("Failed to take stderr")?; + log::trace!("Spawned (pid: {})", child.id()); + + let sessions = Rc::new(RefCell::new(HashMap::default())); + + let client = ClientDelegate { + sessions: sessions.clone(), + cx: cx.clone(), + }; + let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, { + let foreground_executor = cx.foreground_executor().clone(); + move |fut| { + foreground_executor.spawn(fut).detach(); + } + }); + + let io_task = cx.background_spawn(io_task); + + cx.background_spawn(async move { + let mut stderr = BufReader::new(stderr); + let mut line = String::new(); + while let Ok(n) = stderr.read_line(&mut line).await + && n > 0 + { + log::warn!("agent stderr: {}", &line); + line.clear(); + } + }) + .detach(); + + cx.spawn({ + let sessions = sessions.clone(); + async move |cx| { + let status = child.status().await?; + + for session in sessions.borrow().values() { + session + .thread + .update(cx, |thread, cx| { + thread.emit_load_error(LoadError::Exited { status }, cx) + }) + .ok(); + } + + anyhow::Ok(()) + } + }) + .detach(); + + let connection = Rc::new(connection); + + cx.update(|cx| { + AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| { + registry.set_active_connection(server_name, &connection, cx) + }); + })?; + + let response = connection + .initialize(acp::InitializeRequest { + protocol_version: acp::VERSION, + client_capabilities: acp::ClientCapabilities { + fs: acp::FileSystemCapability { + read_text_file: true, + write_text_file: true, + }, + }, + }) + .await?; + + if response.protocol_version < MINIMUM_SUPPORTED_VERSION { + return Err(UnsupportedVersion.into()); + } + + Ok(Self { + auth_methods: response.auth_methods, + connection, + server_name, + sessions, + prompt_capabilities: response.agent_capabilities.prompt_capabilities, + _io_task: io_task, + }) + } +} + +impl AgentConnection for AcpConnection { + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut App, + ) -> Task>> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let cwd = cwd.to_path_buf(); + cx.spawn(async move |cx| { + let response = conn + .new_session(acp::NewSessionRequest { + mcp_servers: vec![], + cwd, + }) + .await + .map_err(|err| { + if err.code == acp::ErrorCode::AUTH_REQUIRED.code { + let mut error = AuthRequired::new(); + + if err.message != acp::ErrorCode::AUTH_REQUIRED.message { + error = error.with_description(err.message); + } + + anyhow!(error) + } else { + anyhow!(err) + } + })?; + + let session_id = response.session_id; + let action_log = cx.new(|_| ActionLog::new(project.clone()))?; + let thread = cx.new(|_cx| { + AcpThread::new( + self.server_name, + self.clone(), + project, + action_log, + session_id.clone(), + ) + })?; + + let session = AcpSession { + thread: thread.downgrade(), + suppress_abort_err: false, + }; + sessions.borrow_mut().insert(session_id, session); + + Ok(thread) + }) + } + + fn auth_methods(&self) -> &[acp::AuthMethod] { + &self.auth_methods + } + + fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task> { + let conn = self.connection.clone(); + cx.foreground_executor().spawn(async move { + let result = conn + .authenticate(acp::AuthenticateRequest { + method_id: method_id.clone(), + }) + .await?; + + Ok(result) + }) + } + + fn prompt( + &self, + _id: Option, + params: acp::PromptRequest, + cx: &mut App, + ) -> Task> { + let conn = self.connection.clone(); + let sessions = self.sessions.clone(); + let session_id = params.session_id.clone(); + cx.foreground_executor().spawn(async move { + let result = conn.prompt(params).await; + + let mut suppress_abort_err = false; + + if let Some(session) = sessions.borrow_mut().get_mut(&session_id) { + suppress_abort_err = session.suppress_abort_err; + session.suppress_abort_err = false; + } + + match result { + Ok(response) => Ok(response), + Err(err) => { + if err.code != ErrorCode::INTERNAL_ERROR.code { + anyhow::bail!(err) + } + + let Some(data) = &err.data else { + anyhow::bail!(err) + }; + + // Temporary workaround until the following PR is generally available: + // https://github.com/google-gemini/gemini-cli/pull/6656 + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct ErrorDetails { + details: Box, + } + + match serde_json::from_value(data.clone()) { + Ok(ErrorDetails { details }) => { + if suppress_abort_err && details.contains("This operation was aborted") + { + Ok(acp::PromptResponse { + stop_reason: acp::StopReason::Cancelled, + }) + } else { + Err(anyhow!(details)) + } + } + Err(_) => Err(anyhow!(err)), + } + } + } + }) + } + + fn prompt_capabilities(&self) -> acp::PromptCapabilities { + self.prompt_capabilities + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) { + session.suppress_abort_err = true; + } + let conn = self.connection.clone(); + let params = acp::CancelNotification { + session_id: session_id.clone(), + }; + cx.foreground_executor() + .spawn(async move { conn.cancel(params).await }) + .detach(); + } + + fn into_any(self: Rc) -> Rc { + self + } +} + +struct ClientDelegate { + sessions: Rc>>, + cx: AsyncApp, +} + +impl acp::Client for ClientDelegate { + async fn request_permission( + &self, + arguments: acp::RequestPermissionRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let rx = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx) + })?; + + let result = rx?.await; + + let outcome = match result { + Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option }, + Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, + }; + + Ok(acp::RequestPermissionResponse { outcome }) + } + + async fn write_text_file( + &self, + arguments: acp::WriteTextFileRequest, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.write_text_file(arguments.path, arguments.content, cx) + })?; + + task.await?; + + Ok(()) + } + + async fn read_text_file( + &self, + arguments: acp::ReadTextFileRequest, + ) -> Result { + let cx = &mut self.cx.clone(); + let task = self + .sessions + .borrow() + .get(&arguments.session_id) + .context("Failed to get session")? + .thread + .update(cx, |thread, cx| { + thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) + })?; + + let content = task.await?; + + Ok(acp::ReadTextFileResponse { content }) + } + + async fn session_notification( + &self, + notification: acp::SessionNotification, + ) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + let sessions = self.sessions.borrow(); + let session = sessions + .get(¬ification.session_id) + .context("Failed to get session")?; + + session.thread.update(cx, |thread, cx| { + thread.handle_session_update(notification.update, cx) + })??; + + Ok(()) + } +} diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs index becf6953fd..0e4080d689 100644 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ b/crates/agent_ui/src/acp/entry_view_state.rs @@ -6,7 +6,7 @@ use agent2::HistoryStore; use collections::HashMap; use editor::{Editor, EditorMode, MinimapVisibility}; use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle, + AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, TextStyleRefinement, WeakEntity, Window, }; use language::language_settings::SoftWrap; @@ -154,22 +154,10 @@ impl EntryViewState { }); } } - AgentThreadEntry::AssistantMessage(message) => { - let entry = if let Some(Entry::AssistantMessage(entry)) = - self.entries.get_mut(index) - { - entry - } else { - self.set_entry( - index, - Entry::AssistantMessage(AssistantMessageEntry::default()), - ); - let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else { - unreachable!() - }; - entry - }; - entry.sync(message); + AgentThreadEntry::AssistantMessage(_) => { + if index == self.entries.len() { + self.entries.push(Entry::empty()) + } } }; } @@ -189,7 +177,7 @@ impl EntryViewState { pub fn settings_changed(&mut self, cx: &mut App) { for entry in self.entries.iter() { match entry { - Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} + Entry::UserMessage { .. } => {} Entry::Content(response_views) => { for view in response_views.values() { if let Ok(diff_editor) = view.clone().downcast::() { @@ -220,29 +208,9 @@ pub enum ViewEvent { MessageEditorEvent(Entity, MessageEditorEvent), } -#[derive(Default, Debug)] -pub struct AssistantMessageEntry { - scroll_handles_by_chunk_index: HashMap, -} - -impl AssistantMessageEntry { - pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option { - self.scroll_handles_by_chunk_index.get(&ix).cloned() - } - - pub fn sync(&mut self, message: &acp_thread::AssistantMessage) { - if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() { - let ix = message.chunks.len() - 1; - let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default(); - handle.scroll_to_bottom(); - } - } -} - #[derive(Debug)] pub enum Entry { UserMessage(Entity), - AssistantMessage(AssistantMessageEntry), Content(HashMap), } @@ -250,7 +218,7 @@ impl Entry { pub fn message_editor(&self) -> Option<&Entity> { match self { Self::UserMessage(editor) => Some(editor), - Self::AssistantMessage(_) | Self::Content(_) => None, + Entry::Content(_) => None, } } @@ -271,16 +239,6 @@ impl Entry { .map(|entity| entity.downcast::().unwrap()) } - pub fn scroll_handle_for_assistant_message_chunk( - &self, - chunk_ix: usize, - ) -> Option { - match self { - Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix), - Self::UserMessage(_) | Self::Content(_) => None, - } - } - fn content_map(&self) -> Option<&HashMap> { match self { Self::Content(map) => Some(map), @@ -296,7 +254,7 @@ impl Entry { pub fn has_content(&self) -> bool { match self { Self::Content(map) => !map.is_empty(), - Self::UserMessage(_) | Self::AssistantMessage(_) => false, + Self::UserMessage(_) => false, } } } diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs index a49dae25b3..967d977b82 100644 --- a/crates/agent_ui/src/acp/thread_history.rs +++ b/crates/agent_ui/src/acp/thread_history.rs @@ -5,15 +5,15 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::StringMatchCandidate; use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; use std::{fmt::Display, ops::Range}; use text::Bias; use time::{OffsetDateTime, UtcOffset}; use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, - Tooltip, prelude::*, + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tooltip, WithScrollbar, + prelude::*, }; pub struct AcpThreadHistory { @@ -26,8 +26,6 @@ pub struct AcpThreadHistory { visible_items: Vec, - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, local_timezone: UtcOffset, _update_task: Task<()>, @@ -90,7 +88,6 @@ impl AcpThreadHistory { }); let scroll_handle = UniformListScrollHandle::default(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { history_store, @@ -99,8 +96,6 @@ impl AcpThreadHistory { hovered_index: None, visible_items: Default::default(), search_editor, - scrollbar_visibility: true, - scrollbar_state, local_timezone: UtcOffset::from_whole_seconds( chrono::Local::now().offset().local_minus_utc(), ) @@ -339,43 +334,6 @@ impl AcpThreadHistory { task.detach_and_log_err(cx); } - fn render_scrollbar(&self, cx: &mut Context) -> Option> { - if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) { - return None; - } - - Some( - div() - .occlude() - .id("thread-history-scroll") - .h_full() - .bg(cx.theme().colors().panel_background.opacity(0.8)) - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .absolute() - .right_1() - .top_0() - .bottom_0() - .w_4() - .pl_1() - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - fn render_list_items( &mut self, range: Range, @@ -491,7 +449,7 @@ impl Focusable for AcpThreadHistory { } impl Render for AcpThreadHistory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("ThreadHistory") .size_full() @@ -542,22 +500,23 @@ impl Render for AcpThreadHistory { ), ) } else { - view.pr_5() - .child( - uniform_list( - "thread-history", - self.visible_items.len(), - cx.processor(|this, range: Range, window, cx| { - this.render_list_items(range, window, cx) - }), - ) - .p_1() - .track_scroll(self.scroll_handle.clone()) - .flex_grow(), + view.pr_5().child( + uniform_list( + "thread-history", + self.visible_items.len(), + cx.processor(|this, range: Range, window, cx| { + this.render_list_items(range, window, cx) + }), ) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) - }) + .p_1() + .track_scroll(self.scroll_handle.clone()) + .flex_grow() + .vertical_scrollbar_for( + self.scroll_handle.clone(), + window, + cx, + ), + ) } }) } diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c68c3a3e93..213020b05f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -20,11 +20,11 @@ use file_icons::FileIcons; use fs::Fs; use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, + ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, TextStyle, + TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, + ease_in_out, linear_color_stop, linear_gradient, list, percentage, point, prelude::*, + pulsating_between, }; use language::Buffer; @@ -43,7 +43,7 @@ use text::Anchor; use theme::ThemeSettings; use ui::{ Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*, + SpinnerLabel, Tooltip, WithScrollbar, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -66,6 +66,7 @@ use crate::{ KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector, }; +const RESPONSE_PADDING_X: Pixels = px(19.); pub const MIN_EDITOR_LINES: usize = 4; pub const MAX_EDITOR_LINES: usize = 8; @@ -267,7 +268,6 @@ pub struct AcpThreadView { thread_error: Option, thread_feedback: ThreadFeedbackState, list_state: ListState, - scrollbar_state: ScrollbarState, auth_task: Option>, expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, @@ -278,7 +278,6 @@ pub struct AcpThreadView { editing_message: Option, prompt_capabilities: Rc>, is_loading_contents: bool, - install_command_markdown: Entity, _cancel_task: Option>, _subscriptions: [Subscription; 3], } @@ -375,8 +374,7 @@ impl AcpThreadView { profile_selector: None, notifications: Vec::new(), notification_subscriptions: HashMap::default(), - list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + list_state: list_state, thread_retry_status: None, thread_error: None, thread_feedback: Default::default(), @@ -392,7 +390,6 @@ impl AcpThreadView { hovered_recent_history_item: None, prompt_capabilities, is_loading_contents: false, - install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)), _subscriptions: subscriptions, _cancel_task: None, focus_handle: cx.focus_handle(), @@ -668,12 +665,7 @@ impl AcpThreadView { match &self.thread_state { ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), ThreadState::Loading { .. } => "Loading…".into(), - ThreadState::LoadError(error) => match error { - LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(), - LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), - LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(), - LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(), - }, + ThreadState::LoadError(_) => "Failed to load".into(), } } @@ -1340,10 +1332,6 @@ impl AcpThreadView { window: &mut Window, cx: &Context, ) -> AnyElement { - let is_generating = self - .thread() - .is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle); - let primary = match &entry { AgentThreadEntry::UserMessage(message) => { let Some(editor) = self @@ -1503,20 +1491,6 @@ impl AcpThreadView { .into_any() } AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let is_last = entry_ix + 1 == total_entries; - let pending_thinking_chunk_ix = if is_generating && is_last { - chunks - .iter() - .enumerate() - .next_back() - .filter(|(_, segment)| { - matches!(segment, AssistantMessageChunk::Thought { .. }) - }) - .map(|(index, _)| index) - } else { - None - }; - let style = default_markdown_style(false, false, window, cx); let message_body = v_flex() .w_full() @@ -1535,7 +1509,6 @@ impl AcpThreadView { entry_ix, chunk_ix, md.clone(), - Some(chunk_ix) == pending_thinking_chunk_ix, window, cx, ) @@ -1549,7 +1522,7 @@ impl AcpThreadView { v_flex() .px_5() .py_1() - .when(is_last, |this| this.pb_4()) + .when(entry_ix + 1 == total_entries, |this| this.pb_4()) .w_full() .text_ui(cx) .child(message_body) @@ -1558,7 +1531,7 @@ impl AcpThreadView { AgentThreadEntry::ToolCall(tool_call) => { let has_terminals = tool_call.terminals().next().is_some(); - div().w_full().map(|this| { + div().w_full().py_1().px_5().map(|this| { if has_terminals { this.children(tool_call.terminals().map(|terminal| { self.render_terminal_tool_call( @@ -1634,90 +1607,64 @@ impl AcpThreadView { entry_ix: usize, chunk_ix: usize, chunk: Entity, - pending: bool, window: &Window, cx: &Context, ) -> AnyElement { let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-card-header"); - let key = (entry_ix, chunk_ix); - let is_open = self.expanded_thinking_blocks.contains(&key); - let editor_bg = cx.theme().colors().editor_background; - let gradient_overlay = div() - .rounded_b_lg() - .h_full() - .absolute() - .w_full() - .bottom_0() - .left_0() - .bg(linear_gradient( - 180., - linear_color_stop(editor_bg, 1.), - linear_color_stop(editor_bg.opacity(0.2), 0.), - )); - - let scroll_handle = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); v_flex() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) .child( h_flex() .id(header_id) .group(&card_header_id) .relative() .w_full() - .py_0p5() - .px_1p5() - .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .justify_between() - .border_b_1() - .border_color(self.tool_card_border_color(cx)) + .gap_1p5() .child( h_flex() - .h(window.line_height()) - .gap_1p5() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ) + .size_4() + .justify_center() .child( div() - .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .map(|this| { - if pending { - this.child("Thinking") - } else { - this.child("Thought Process") - } - }), + .group_hover(&card_header_id, |s| s.invisible().w_0()) + .child( + Icon::new(IconName::ToolThink) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .absolute() + .inset_0() + .invisible() + .justify_center() + .group_hover(&card_header_id, |s| s.visible()) + .child( + Disclosure::new(("expand", entry_ix), is_open) + .opened_icon(IconName::ChevronUp) + .closed_icon(IconName::ChevronRight) + .on_click(cx.listener({ + move |this, _event, _window, cx| { + if is_open { + this.expanded_thinking_blocks.remove(&key); + } else { + this.expanded_thinking_blocks.insert(key); + } + cx.notify(); + } + })), + ), ), ) .child( - Disclosure::new(("expand", entry_ix), is_open) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown) - .visible_on_hover(&card_header_id) - .on_click(cx.listener({ - move |this, _event, _window, cx| { - if is_open { - this.expanded_thinking_blocks.remove(&key); - } else { - this.expanded_thinking_blocks.insert(key); - } - cx.notify(); - } - })), + div() + .text_size(self.tool_name_font_size()) + .text_color(cx.theme().colors().text_muted) + .child("Thinking"), ) .on_click(cx.listener({ move |this, _event, _window, cx| { @@ -1730,28 +1677,22 @@ impl AcpThreadView { } })), ) - .child( - div() - .relative() - .bg(editor_bg) - .rounded_b_lg() - .child( - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .p_2() - .when(!is_open, |this| this.max_h_20()) - .text_ui_sm(cx) - .overflow_hidden() - .child(self.render_markdown( - chunk, - default_markdown_style(false, false, window, cx), - )), - ) - .when(!is_open && pending, |this| this.child(gradient_overlay)), - ) + .when(is_open, |this| { + this.child( + div() + .relative() + .mt_1p5() + .ml(px(7.)) + .pl_4() + .border_l_1() + .border_color(self.tool_card_border_color(cx)) + .text_ui_sm(cx) + .child(self.render_markdown( + chunk, + default_markdown_style(false, false, window, cx), + )), + ) + }) .into_any_element() } @@ -1762,6 +1703,7 @@ impl AcpThreadView { window: &Window, cx: &Context, ) -> Div { + let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix)); let card_header_id = SharedString::from("inner-tool-call-header"); let tool_icon = @@ -1790,7 +1732,11 @@ impl AcpThreadView { _ => false, }; - let has_location = tool_call.locations.len() == 1; + let failed_tool_call = matches!( + tool_call.status, + ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed + ); + let needs_confirmation = matches!( tool_call.status, ToolCallStatus::WaitingForConfirmation { .. } @@ -1803,31 +1749,23 @@ impl AcpThreadView { let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - let gradient_overlay = { + let gradient_overlay = |color: Hsla| { div() .absolute() .top_0() .right_0() .w_12() .h_full() - .map(|this| { - if use_card_layout { - this.bg(linear_gradient( - 90., - linear_color_stop(self.tool_card_header_bg(cx), 1.), - linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.), - )) - } else { - this.bg(linear_gradient( - 90., - linear_color_stop(cx.theme().colors().panel_background, 1.), - linear_color_stop( - cx.theme().colors().panel_background.opacity(0.2), - 0., - ), - )) - } - }) + .bg(linear_gradient( + 90., + linear_color_stop(color, 1.), + linear_color_stop(color.opacity(0.2), 0.), + )) + }; + let gradient_color = if use_card_layout { + self.tool_card_header_bg(cx) + } else { + cx.theme().colors().panel_background }; let tool_output_display = if is_open { @@ -1878,58 +1816,40 @@ impl AcpThreadView { }; v_flex() - .map(|this| { - if use_card_layout { - this.my_2() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - } else { - this.my_1() - } + .when(use_card_layout, |this| { + this.rounded_md() + .border_1() + .border_color(self.tool_card_border_color(cx)) + .bg(cx.theme().colors().editor_background) + .overflow_hidden() }) - .map(|this| { - if has_location && !use_card_layout { - this.ml_4() - } else { - this.ml_5() - } - }) - .mr_5() .child( h_flex() + .id(header_id) .group(&card_header_id) .relative() .w_full() + .max_w_full() .gap_1() - .justify_between() .when(use_card_layout, |this| { - this.p_0p5() + this.pl_1p5() + .pr_1() + .py_0p5() .rounded_t_md() - .bg(self.tool_card_header_bg(cx)) - .when(is_open && !failed_or_canceled, |this| { + .when(is_open && !failed_tool_call, |this| { this.border_b_1() .border_color(self.tool_card_border_color(cx)) }) + .bg(self.tool_card_header_bg(cx)) }) .child( h_flex() .relative() .w_full() - .h(window.line_height()) + .h(window.line_height() - px(2.)) .text_size(self.tool_name_font_size()) - .gap_1p5() - .when(has_location || use_card_layout, |this| this.px_1()) - .when(has_location, |this| { - this.cursor(CursorStyle::PointingHand) - .rounded_sm() - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) - }) - .overflow_hidden() .child(tool_icon) - .child(if has_location { + .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] .path .file_name() @@ -1940,6 +1860,13 @@ impl AcpThreadView { h_flex() .id(("open-tool-call-location", entry_ix)) .w_full() + .max_w_full() + .px_1p5() + .rounded_sm() + .overflow_x_scroll() + .hover(|label| { + label.bg(cx.theme().colors().element_hover.opacity(0.5)) + }) .map(|this| { if use_card_layout { this.text_color(cx.theme().colors().text) @@ -1949,28 +1876,31 @@ impl AcpThreadView { }) .child(name) .tooltip(Tooltip::text("Jump to File")) + .cursor(gpui::CursorStyle::PointingHand) .on_click(cx.listener(move |this, _, window, cx| { this.open_tool_call_location(entry_ix, 0, window, cx); })) .into_any_element() } else { h_flex() + .relative() .w_full() - .child(self.render_markdown( + .max_w_full() + .ml_1p5() + .overflow_hidden() + .child(h_flex().pr_8().child(self.render_markdown( tool_call.label.clone(), default_markdown_style(false, true, window, cx), - )) + ))) + .child(gradient_overlay(gradient_color)) .into_any() - }) - .when(!has_location, |this| this.child(gradient_overlay)), + }), ) - .when(is_collapsible || failed_or_canceled, |this| { - this.child( - h_flex() - .px_1() - .gap_px() - .when(is_collapsible, |this| { - this.child( + .child( + h_flex() + .gap_px() + .when(is_collapsible, |this| { + this.child( Disclosure::new(("expand", entry_ix), is_open) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) @@ -1987,16 +1917,15 @@ impl AcpThreadView { } })), ) - }) - .when(failed_or_canceled, |this| { - this.child( - Icon::new(IconName::Close) - .color(Color::Error) - .size(IconSize::Small), - ) - }), - ) - }), + }) + .when(failed_or_canceled, |this| { + this.child( + Icon::new(IconName::Close) + .color(Color::Error) + .size(IconSize::Small), + ) + }), + ), ) .children(tool_output_display) } @@ -2037,7 +1966,7 @@ impl AcpThreadView { v_flex() .mt_1p5() - .ml(rems(0.4)) + .ml(px(7.)) .px_3p5() .gap_2() .border_l_1() @@ -2094,7 +2023,7 @@ impl AcpThreadView { let button_id = SharedString::from(format!("item-{}", uri)); div() - .ml(rems(0.4)) + .ml(px(7.)) .pl_2p5() .border_l_1() .border_color(self.tool_card_border_color(cx)) @@ -2282,12 +2211,6 @@ impl AcpThreadView { started_at.elapsed() }; - let header_id = - SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id())); - let header_group = SharedString::from(format!( - "terminal-tool-header-group-{}", - terminal.entity_id() - )); let header_bg = cx .theme() .colors() @@ -2303,7 +2226,10 @@ impl AcpThreadView { let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); let header = h_flex() - .id(header_id) + .id(SharedString::from(format!( + "terminal-tool-header-{}", + terminal.entity_id() + ))) .flex_none() .gap_1() .justify_between() @@ -2367,6 +2293,23 @@ impl AcpThreadView { ), ) }) + .when(tool_failed || command_failed, |header| { + header.child( + div() + .id(("terminal-tool-error-code-indicator", terminal.entity_id())) + .child( + Icon::new(IconName::Close) + .size(IconSize::Small) + .color(Color::Error), + ) + .when_some(output.and_then(|o| o.exit_status), |this, status| { + this.tooltip(Tooltip::text(format!( + "Exited with code {}", + status.code().unwrap_or(-1), + ))) + }), + ) + }) .when(truncated_output, |header| { let tooltip = if let Some(output) = output { if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { @@ -2419,7 +2362,6 @@ impl AcpThreadView { ) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) - .visible_on_hover(&header_group) .on_click(cx.listener({ let id = tool_call.id.clone(); move |this, _event, _window, _cx| { @@ -2428,26 +2370,8 @@ impl AcpThreadView { } else { this.expanded_tool_calls.insert(id.clone()); } - } - })), - ) - .when(tool_failed || command_failed, |header| { - header.child( - div() - .id(("terminal-tool-error-code-indicator", terminal.entity_id())) - .child( - Icon::new(IconName::Close) - .size(IconSize::Small) - .color(Color::Error), - ) - .when_some(output.and_then(|o| o.exit_status), |this, status| { - this.tooltip(Tooltip::text(format!( - "Exited with code {}", - status.code().unwrap_or(-1), - ))) - }), - ) - }); + }})), + ); let terminal_view = self .entry_view_state @@ -2457,8 +2381,7 @@ impl AcpThreadView { let show_output = is_expanded && terminal_view.is_some(); v_flex() - .my_2() - .mx_5() + .mb_2() .border_1() .when(tool_failed || command_failed, |card| card.border_dashed()) .border_color(border_color) @@ -2466,10 +2389,9 @@ impl AcpThreadView { .overflow_hidden() .child( v_flex() - .group(&header_group) .py_1p5() - .pr_1p5() .pl_2() + .pr_1p5() .gap_0p5() .bg(header_bg) .text_xs() @@ -2841,26 +2763,125 @@ impl AcpThreadView { ) } - fn render_load_error( - &self, - e: &LoadError, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (message, action_slot): (SharedString, _) = match e { + fn render_load_error(&self, e: &LoadError, cx: &Context) -> AnyElement { + let (message, action_slot) = match e { LoadError::NotInstalled { - error_message: _, - install_message: _, + error_message, + install_message, install_command, } => { - return self.render_not_installed(install_command.clone(), false, window, cx); + let install_command = install_command.clone(); + let button = Button::new("install", install_message) + .tooltip(Tooltip::text(install_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id()); + + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(install_command.clone()), + full_label: install_command.clone(), + label: install_command.clone(), + command: Some(install_command.clone()), + args: Vec::new(), + command_label: install_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })); + + (error_message.clone(), Some(button.into_any_element())) } LoadError::Unsupported { - error_message: _, - upgrade_message: _, + error_message, + upgrade_message, upgrade_command, } => { - return self.render_not_installed(upgrade_command.clone(), true, window, cx); + let upgrade_command = upgrade_command.clone(); + let button = Button::new("upgrade", upgrade_message) + .tooltip(Tooltip::text(upgrade_command.clone())) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .icon(IconName::Download) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, window, cx| { + telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id()); + + let task = this + .workspace + .update(cx, |workspace, cx| { + let project = workspace.project().read(cx); + let cwd = project.first_project_directory(cx); + let shell = project.terminal_settings(&cwd, cx).shell.clone(); + let spawn_in_terminal = task::SpawnInTerminal { + id: task::TaskId(upgrade_command.to_string()), + full_label: upgrade_command.clone(), + label: upgrade_command.clone(), + command: Some(upgrade_command.clone()), + args: Vec::new(), + command_label: upgrade_command.clone(), + cwd, + env: Default::default(), + use_new_terminal: true, + allow_concurrent_runs: true, + reveal: Default::default(), + reveal_target: Default::default(), + hide: Default::default(), + shell, + show_summary: true, + show_command: true, + show_rerun: false, + }; + workspace.spawn_in_terminal(spawn_in_terminal, window, cx) + }) + .ok(); + let Some(task) = task else { return }; + cx.spawn_in(window, async move |this, cx| { + if let Some(Ok(_)) = task.await { + this.update_in(cx, |this, window, cx| { + this.reset(window, cx); + }) + .ok(); + } + }) + .detach() + })); + + (error_message.clone(), Some(button.into_any_element())) } LoadError::Exited { .. } => ("Server exited with status {status}".into(), None), LoadError::Other(msg) => ( @@ -2878,121 +2899,6 @@ impl AcpThreadView { .into_any_element() } - fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context) { - telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id()); - let task = self - .workspace - .update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - let cwd = project.first_project_directory(cx); - let shell = project.terminal_settings(&cwd, cx).shell.clone(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId(install_command.clone()), - full_label: install_command.clone(), - label: install_command.clone(), - command: Some(install_command.clone()), - args: Vec::new(), - command_label: install_command.clone(), - cwd, - env: Default::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: Default::default(), - reveal_target: Default::default(), - hide: Default::default(), - shell, - show_summary: true, - show_command: true, - show_rerun: false, - }; - workspace.spawn_in_terminal(spawn_in_terminal, window, cx) - }) - .ok(); - let Some(task) = task else { return }; - cx.spawn_in(window, async move |this, cx| { - if let Some(Ok(_)) = task.await { - this.update_in(cx, |this, window, cx| { - this.reset(window, cx); - }) - .ok(); - } - }) - .detach() - } - - fn render_not_installed( - &self, - install_command: String, - is_upgrade: bool, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - self.install_command_markdown.update(cx, |markdown, cx| { - if !markdown.source().contains(&install_command) { - markdown.replace(format!("```\n{}\n```", install_command), cx); - } - }); - - let (heading_label, description_label, button_label, or_label) = if is_upgrade { - ( - "Upgrade Gemini CLI in Zed", - "Get access to the latest version with support for Zed.", - "Upgrade Gemini CLI", - "Or, to upgrade it manually:", - ) - } else { - ( - "Get Started with Gemini CLI in Zed", - "Use Google's new coding agent directly in Zed.", - "Install Gemini CLI", - "Or, to install it manually:", - ) - }; - - v_flex() - .w_full() - .p_3p5() - .gap_2p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(linear_gradient( - 180., - linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.), - linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.), - )) - .child( - v_flex().gap_0p5().child(Label::new(heading_label)).child( - Label::new(description_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .child( - Button::new("install_gemini", button_label) - .full_width() - .size(ButtonSize::Medium) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .label_size(LabelSize::Small) - .icon(IconName::TerminalGhost) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .on_click(cx.listener(move |this, _, window, cx| { - this.install_agent(install_command.clone(), window, cx) - })), - ) - .child( - Label::new(or_label) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(MarkdownElement::new( - self.install_command_markdown.clone(), - default_markdown_style(false, false, window, cx), - )) - .into_any_element() - } - fn render_activity_bar( &self, thread_entity: &Entity, @@ -4244,14 +4150,13 @@ impl AcpThreadView { ) -> impl IntoElement { let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating); if is_generating { - return h_flex().id("thread-controls-container").child( + return h_flex().id("thread-controls-container").ml_1().child( div() .py_2() - .px_5() + .px(rems_from_px(22.)) .child(SpinnerLabel::new().size(LabelSize::Small)), ); } - let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) @@ -4277,10 +4182,12 @@ impl AcpThreadView { .id("thread-controls-container") .group("thread-controls-container") .w_full() - .py_2() - .px_5() + .mr_1() + .pt_1() + .pb_2() + .px(RESPONSE_PADDING_X) .gap_px() - .opacity(0.6) + .opacity(0.4) .hover(|style| style.opacity(1.)) .flex_wrap() .justify_end(); @@ -4291,50 +4198,56 @@ impl AcpThreadView { .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) { let feedback = self.thread_feedback.feedback; - - container = container - .child( - div().visible_on_hover("thread-controls-container").child( - Label::new(match feedback { + container = container.child( + div().visible_on_hover("thread-controls-container").child( + Label::new( + match feedback { Some(ThreadFeedback::Positive) => "Thanks for your feedback!", - Some(ThreadFeedback::Negative) => { - "We appreciate your feedback and will use it to improve." - } - None => { - "Rating the thread sends all of your current conversation to the Zed team." - } - }) - .color(Color::Muted) - .size(LabelSize::XSmall) - .truncate(), - ), - ) - .child( - IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Positive) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Helpful Response")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click(ThreadFeedback::Positive, window, cx); - })), - ) - .child( - IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(match feedback { - Some(ThreadFeedback::Negative) => Color::Accent, - _ => Color::Ignored, - }) - .tooltip(Tooltip::text("Not Helpful")) - .on_click(cx.listener(move |this, _, window, cx| { - this.handle_feedback_click(ThreadFeedback::Negative, window, cx); - })), - ); + Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.", + None => "Rating the thread sends all of your current conversation to the Zed team.", + } + ) + .color(Color::Muted) + .size(LabelSize::XSmall) + .truncate(), + ), + ).child( + h_flex() + .child( + IconButton::new("feedback-thumbs-up", IconName::ThumbsUp) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Positive) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Helpful Response")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Positive, + window, + cx, + ); + })), + ) + .child( + IconButton::new("feedback-thumbs-down", IconName::ThumbsDown) + .shape(ui::IconButtonShape::Square) + .icon_size(IconSize::Small) + .icon_color(match feedback { + Some(ThreadFeedback::Negative) => Color::Accent, + _ => Color::Ignored, + }) + .tooltip(Tooltip::text("Not Helpful")) + .on_click(cx.listener(move |this, _, window, cx| { + this.handle_feedback_click( + ThreadFeedback::Negative, + window, + cx, + ); + })), + ) + ) } container.child(open_as_markdown).child(scroll_to_top) @@ -4405,39 +4318,6 @@ impl AcpThreadView { cx.notify(); } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .id("acp-thread-scrollbar") - .occlude() - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - fn render_token_limit_callout( &self, line_height: Pixels, @@ -4950,23 +4830,27 @@ impl Render for AcpThreadView { configuration_view, pending_auth_method, .. - } => self.render_auth_required_state( - connection, - description.as_ref(), - configuration_view.as_ref(), - pending_auth_method.as_ref(), - window, - cx, - ), + } => self + .render_auth_required_state( + connection, + description.as_ref(), + configuration_view.as_ref(), + pending_auth_method.as_ref(), + window, + cx, + ) + .into_any(), ThreadState::Loading { .. } => v_flex() .flex_1() - .child(self.render_recent_history(window, cx)), + .child(self.render_recent_history(window, cx)) + .into_any(), ThreadState::LoadError(e) => v_flex() .flex_1() .size_full() .items_center() .justify_end() - .child(self.render_load_error(e, window, cx)), + .child(self.render_load_error(e, cx)) + .into_any(), ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { if has_messages { this.child( @@ -4986,9 +4870,11 @@ impl Render for AcpThreadView { .flex_grow() .into_any(), ) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) + .into_any() } else { this.child(self.render_recent_history(window, cx)) + .into_any() } }), }) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2..575d8a3a56 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -22,10 +22,9 @@ use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, Selec use gpui::{ AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry, ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, - ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, - StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, - UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage, - pulsating_between, + ListAlignment, ListOffset, ListState, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, + Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, + WindowHandle, linear_color_stop, linear_gradient, list, percentage, pulsating_between, }; use language::{Buffer, Language, LanguageRegistry}; use language_model::{ @@ -46,8 +45,7 @@ use std::time::Duration; use text::ToPoint; use theme::ThemeSettings; use ui::{ - Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, - Tooltip, prelude::*, + Banner, Disclosure, KeyBinding, PopoverMenuHandle, TextSize, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; use util::markdown::MarkdownCodeBlock; @@ -68,7 +66,6 @@ pub struct ActiveThread { save_thread_task: Option>, messages: Vec, list_state: ListState, - scrollbar_state: ScrollbarState, rendered_messages_by_id: HashMap, rendered_tool_uses: HashMap, editing_message: Option<(MessageId, EditingMessageState)>, @@ -799,8 +796,7 @@ impl ActiveThread { expanded_tool_uses: HashMap::default(), expanded_thinking_segments: HashMap::default(), expanded_code_blocks: HashMap::default(), - list_state: list_state.clone(), - scrollbar_state: ScrollbarState::new(list_state).parent_entity(&cx.entity()), + list_state, editing_message: None, last_error: None, copied_code_block_ids: HashSet::default(), @@ -3491,39 +3487,6 @@ impl ActiveThread { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("active-thread-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - pub fn is_codeblock_expanded(&self, message_id: MessageId, ix: usize) -> bool { self.expanded_code_blocks .get(&(message_id, ix)) @@ -3557,13 +3520,13 @@ pub enum ActiveThreadEvent { impl EventEmitter for ActiveThread {} impl Render for ActiveThread { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .relative() .bg(cx.theme().colors().panel_background) .child(list(self.list_state.clone(), cx.processor(Self::render_message)).flex_grow()) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) } } diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 224f49cc3e..2b2cec5539 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,23 +3,20 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; -use agent_servers::{AgentServerCommand, AgentServerSettings, AllAgentServersSettings, Gemini}; +use agent_servers::{AgentServerCommand, AllAgentServersSettings, Gemini}; use agent_settings::AgentSettings; -use anyhow::Result; use assistant_tool::{ToolSource, ToolWorkingSet}; use cloud_llm_client::Plan; use collections::HashMap; use context_server::ContextServerId; -use editor::{Editor, SelectionEffects, scroll::Autoscroll}; use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, - EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, - WeakEntity, percentage, + Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle, + Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage, }; use language::LanguageRegistry; use language_model::{ @@ -34,10 +31,10 @@ use project::{ use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, - Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, + Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*, }; use util::ResultExt as _; -use workspace::{Workspace, create_and_open_local_file}; +use workspace::Workspace; use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; @@ -61,7 +58,6 @@ pub struct AgentConfiguration { tools: Entity, _registry_subscription: Subscription, scroll_handle: ScrollHandle, - scrollbar_state: ScrollbarState, gemini_is_installed: bool, _check_for_gemini: Task<()>, } @@ -105,7 +101,6 @@ impl AgentConfiguration { .detach(); let scroll_handle = ScrollHandle::new(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { fs, @@ -120,7 +115,6 @@ impl AgentConfiguration { tools, _registry_subscription: registry_subscription, scroll_handle, - scrollbar_state, gemini_is_installed: false, _check_for_gemini: Task::ready(()), }; @@ -1061,39 +1055,10 @@ impl AgentConfiguration { .child( v_flex() .gap_0p5() - .child( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child(Headline::new("External Agents")) - .child( - Button::new("add-agent", "Add Agent") - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click( - move |_, window, cx| { - if let Some(workspace) = window.root().flatten() { - let workspace = workspace.downgrade(); - window - .spawn(cx, async |cx| { - open_new_agent_servers_entry_in_settings_editor( - workspace, - cx, - ).await - }) - .detach_and_log_err(cx); - } - } - ), - ) - ) + .child(Headline::new("External Agents")) .child( Label::new( - "Bring the agent of your choice to Zed via our new Agent Client Protocol.", + "Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.", ) .color(Color::Muted), ), @@ -1241,32 +1206,7 @@ impl Render for AgentConfiguration { .child(self.render_context_servers_section(window, cx)) .child(self.render_provider_configuration_section(cx)), ) - .child( - div() - .id("assistant-configuration-scrollbar") - .occlude() - .absolute() - .right(px(3.)) - .top_0() - .bottom_0() - .pb_6() - .w(px(12.)) - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) + .vertical_scrollbar(window, cx) } } @@ -1356,109 +1296,3 @@ fn show_unable_to_uninstall_extension_with_context_server( workspace.toggle_status_toast(status_toast, cx); } - -async fn open_new_agent_servers_entry_in_settings_editor( - workspace: WeakEntity, - cx: &mut AsyncWindowContext, -) -> Result<()> { - let settings_editor = workspace - .update_in(cx, |_, window, cx| { - create_and_open_local_file(paths::settings_file(), window, cx, || { - settings::initial_user_settings_content().as_ref().into() - }) - })? - .await? - .downcast::() - .unwrap(); - - settings_editor - .downgrade() - .update_in(cx, |item, window, cx| { - let text = item.buffer().read(cx).snapshot(cx).text(); - - let settings = cx.global::(); - - let mut unique_server_name = None; - let edits = settings.edits_for_update::(&text, |file| { - let server_name: Option = (0..u8::MAX) - .map(|i| { - if i == 0 { - "your_agent".into() - } else { - format!("your_agent_{}", i).into() - } - }) - .find(|name| !file.custom.contains_key(name)); - if let Some(server_name) = server_name { - unique_server_name = Some(server_name.clone()); - file.custom.insert( - server_name, - AgentServerSettings { - command: AgentServerCommand { - path: "path_to_executable".into(), - args: vec![], - env: Some(HashMap::default()), - }, - }, - ); - } - }); - - if edits.is_empty() { - return; - } - - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - item.edit(edits, cx); - if let Some((unique_server_name, buffer)) = - unique_server_name.zip(item.buffer().read(cx).as_singleton()) - { - let snapshot = buffer.read(cx).snapshot(); - if let Some(range) = - find_text_in_buffer(&unique_server_name, ranges[0].start, &snapshot) - { - item.change_selections( - SelectionEffects::scroll(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_ranges(vec![range]); - }, - ); - } - } - }) -} - -fn find_text_in_buffer( - text: &str, - start: usize, - snapshot: &language::BufferSnapshot, -) -> Option> { - let chars = text.chars().collect::>(); - - let mut offset = start; - let mut char_offset = 0; - for c in snapshot.chars_at(start) { - if char_offset >= chars.len() { - break; - } - offset += 1; - - if c == chars[char_offset] { - char_offset += 1; - } else { - char_offset = 0; - } - } - - if char_offset == chars.len() { - Some(offset.saturating_sub(chars.len())..offset) - } else { - None - } -} diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d1cf748733..269aec3365 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -14,7 +14,6 @@ use zed_actions::agent::ReauthenticateAgent; use crate::acp::{AcpThreadHistory, ThreadHistoryEvent}; use crate::agent_diff::AgentDiffThread; -use crate::ui::AcpOnboardingModal; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -78,10 +77,7 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{ - OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding, - ToggleModelSelector, - }, + agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; @@ -205,9 +201,6 @@ pub fn init(cx: &mut App) { .register_action(|workspace, _: &OpenOnboardingModal, window, cx| { AgentOnboardingModal::toggle(workspace, window, cx) }) - .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { - AcpOnboardingModal::toggle(workspace, window, cx) - }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); @@ -598,6 +591,17 @@ impl AgentPanel { None }; + // Wait for the Gemini/Native feature flag to be available. + let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; + if !client.status().borrow().is_signed_out() { + cx.update(|_, cx| { + cx.wait_for_flag_or_timeout::( + Duration::from_secs(2), + ) + })? + .await; + } + let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( @@ -1848,6 +1852,19 @@ impl AgentPanel { menu } + pub fn set_selected_agent( + &mut self, + agent: AgentType, + window: &mut Window, + cx: &mut Context, + ) { + if self.selected_agent != agent { + self.selected_agent = agent.clone(); + self.serialize(cx); + } + self.new_agent_thread(agent, window, cx); + } + pub fn selected_agent(&self) -> AgentType { self.selected_agent.clone() } @@ -1858,11 +1875,6 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { - if self.selected_agent != agent { - self.selected_agent = agent.clone(); - self.serialize(cx); - } - match agent { AgentType::Zed => { window.dispatch_action( @@ -2543,7 +2555,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::NativeAgent, window, cx, @@ -2569,7 +2581,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::TextThread, window, cx, @@ -2597,7 +2609,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::Gemini, window, cx, @@ -2624,7 +2636,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::ClaudeCode, window, cx, @@ -2657,7 +2669,7 @@ impl AgentPanel { workspace.panel::(cx) { panel.update(cx, |panel, cx| { - panel.new_agent_thread( + panel.set_selected_agent( AgentType::Custom { name: agent_name .clone(), @@ -2681,9 +2693,9 @@ impl AgentPanel { }) .when(cx.has_flag::(), |menu| { menu.separator().link( - "Add Other Agents", + "Add Your Own Agent", OpenBrowser { - url: zed_urls::external_agents_docs(cx), + url: "https://agentclientprotocol.com/".into(), } .boxed_clone(), ) diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index 3633e533da..aceca79dbf 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -6,8 +6,7 @@ use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ - AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, - LanguageModelRegistry, + ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, }; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate}; @@ -77,7 +76,6 @@ pub struct LanguageModelPickerDelegate { all_models: Arc, filtered_entries: Vec, selected_index: usize, - _authenticate_all_providers_task: Task<()>, _subscriptions: Vec, } @@ -98,7 +96,6 @@ impl LanguageModelPickerDelegate { selected_index: Self::get_active_model_index(&entries, get_active_model(cx)), filtered_entries: entries, get_active_model: Arc::new(get_active_model), - _authenticate_all_providers_task: Self::authenticate_all_providers(cx), _subscriptions: vec![cx.subscribe_in( &LanguageModelRegistry::global(cx), window, @@ -142,56 +139,6 @@ impl LanguageModelPickerDelegate { .unwrap_or(0) } - /// Authenticates all providers in the [`LanguageModelRegistry`]. - /// - /// We do this so that we can populate the language selector with all of the - /// models from the configured providers. - fn authenticate_all_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.spawn(async move |_cx| { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - if matches!(err, AuthenticateError::CredentialsNotFound) { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } else { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err}", - provider_name.0 - ); - } - } - } - } - } - }) - } - pub fn active_model(&self, cx: &App) -> Option { (self.get_active_model)(cx) } diff --git a/crates/agent_ui/src/thread_history.rs b/crates/agent_ui/src/thread_history.rs index 4ec2078e5d..32c4de1d42 100644 --- a/crates/agent_ui/src/thread_history.rs +++ b/crates/agent_ui/src/thread_history.rs @@ -4,14 +4,14 @@ use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; use editor::{Editor, EditorEvent}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Stateful, Task, + App, ClickEvent, Empty, Entity, FocusHandle, Focusable, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list, }; use std::{fmt::Display, ops::Range, sync::Arc}; use time::{OffsetDateTime, UtcOffset}; use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, - Tooltip, prelude::*, + HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, Tooltip, + WithScrollbar, prelude::*, }; use util::ResultExt; @@ -30,8 +30,6 @@ pub struct ThreadHistory { separated_item_indexes: Vec, _separated_items_task: Option>, search_state: SearchState, - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, _subscriptions: Vec, } @@ -90,7 +88,6 @@ impl ThreadHistory { }); let scroll_handle = UniformListScrollHandle::default(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { agent_panel, @@ -103,8 +100,6 @@ impl ThreadHistory { separated_items: Default::default(), separated_item_indexes: Default::default(), search_editor, - scrollbar_visibility: true, - scrollbar_state, _subscriptions: vec![search_editor_subscription, history_store_subscription], _separated_items_task: None, }; @@ -363,43 +358,6 @@ impl ThreadHistory { cx.notify(); } - fn render_scrollbar(&self, cx: &mut Context) -> Option> { - if !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) { - return None; - } - - Some( - div() - .occlude() - .id("thread-history-scroll") - .h_full() - .bg(cx.theme().colors().panel_background.opacity(0.8)) - .border_l_1() - .border_color(cx.theme().colors().border_variant) - .absolute() - .right_1() - .top_0() - .bottom_0() - .w_4() - .pl_1() - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { if let Some(entry) = self.get_match(self.selected_index) { let task_result = match entry { @@ -536,7 +494,7 @@ impl Focusable for ThreadHistory { } impl Render for ThreadHistory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .key_context("ThreadHistory") .size_full() @@ -601,9 +559,14 @@ impl Render for ThreadHistory { .track_scroll(self.scroll_handle.clone()) .flex_grow(), ) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) - }) + .custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical) + .tracked_scroll_handle(self.scroll_handle.clone()) + .width_sm() + .with_track_along(ScrollAxes::Vertical), + window, + cx, + ) } }) } diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 600698b07e..ada973cddf 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,4 +1,3 @@ -mod acp_onboarding_modal; mod agent_notification; mod burn_mode_tooltip; mod context_pill; @@ -7,7 +6,6 @@ mod onboarding_modal; pub mod preview; mod unavailable_editing_tooltip; -pub use acp_onboarding_modal::*; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; diff --git a/crates/agent_ui/src/ui/acp_onboarding_modal.rs b/crates/agent_ui/src/ui/acp_onboarding_modal.rs deleted file mode 100644 index 0ed9de7221..0000000000 --- a/crates/agent_ui/src/ui/acp_onboarding_modal.rs +++ /dev/null @@ -1,254 +0,0 @@ -use client::zed_urls; -use gpui::{ - ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, - linear_color_stop, linear_gradient, -}; -use ui::{TintColor, Vector, VectorName, prelude::*}; -use workspace::{ModalView, Workspace}; - -use crate::agent_panel::{AgentPanel, AgentType}; - -macro_rules! acp_onboarding_event { - ($name:expr) => { - telemetry::event!($name, source = "ACP Onboarding"); - }; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => { - telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+); - }; -} - -pub struct AcpOnboardingModal { - focus_handle: FocusHandle, - workspace: Entity, -} - -impl AcpOnboardingModal { - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let workspace_entity = cx.entity(); - workspace.toggle_modal(window, cx, |_window, cx| Self { - workspace: workspace_entity, - focus_handle: cx.focus_handle(), - }); - } - - fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.workspace.update(cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel.new_agent_thread(AgentType::Gemini, window, cx); - }); - } - }); - - cx.emit(DismissEvent); - - acp_onboarding_event!("Open Panel Clicked"); - } - - fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context) { - cx.open_url(&zed_urls::external_agents_docs(cx)); - cx.notify(); - - acp_onboarding_event!("Documentation Link Clicked"); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl EventEmitter for AcpOnboardingModal {} - -impl Focusable for AcpOnboardingModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for AcpOnboardingModal {} - -impl Render for AcpOnboardingModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let illustration_element = |label: bool, opacity: f32| { - h_flex() - .px_1() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.05)) - .border_1() - .border_color(cx.theme().colors().border) - .border_dashed() - .child( - Icon::new(IconName::Stop) - .size(IconSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))), - ) - .map(|this| { - if label { - this.child( - Label::new("Your Agent Here") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - div().w_16().h_1().rounded_full().bg(cx - .theme() - .colors() - .element_active - .opacity(0.6)), - ) - } - }) - .opacity(opacity) - }; - - let illustration = h_flex() - .relative() - .h(rems_from_px(126.)) - .bg(cx.theme().colors().editor_background) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .justify_center() - .gap_8() - .rounded_t_md() - .overflow_hidden() - .child( - div().absolute().inset_0().w(px(515.)).h(px(126.)).child( - Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))), - ), - ) - .child(div().absolute().inset_0().size_full().bg(linear_gradient( - 0., - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.1), - 0.9, - ), - linear_color_stop( - cx.theme().colors().elevated_surface_background.opacity(0.), - 0., - ), - ))) - .child( - div() - .absolute() - .inset_0() - .size_full() - .bg(gpui::black().opacity(0.15)), - ) - .child( - h_flex() - .gap_4() - .child( - Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.)) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ) - .child( - Vector::new( - VectorName::AcpLogoSerif, - rems_from_px(111.), - rems_from_px(41.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))), - ), - ) - .child( - v_flex() - .gap_1p5() - .child(illustration_element(false, 0.15)) - .child(illustration_element(true, 0.3)) - .child( - h_flex() - .pl_1() - .pr_2() - .py_0p5() - .gap_1() - .rounded_sm() - .bg(cx.theme().colors().element_active.opacity(0.2)) - .border_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::AiGemini) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)), - ) - .child(illustration_element(true, 0.3)) - .child(illustration_element(false, 0.15)), - ); - - let heading = v_flex() - .w_full() - .gap_1() - .child( - Label::new("Now Available") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large)); - - let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration."; - - let open_panel_button = Button::new("open-panel", "Start with Gemini CLI") - .icon_size(IconSize::Indicator) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::open_panel)); - - let docs_button = Button::new("add-other-agents", "Add Other Agents") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .full_width() - .on_click(cx.listener(Self::view_docs)); - - let close_button = h_flex().absolute().top_2().right_2().child( - IconButton::new("cancel", IconName::Close).on_click(cx.listener( - |_, _: &ClickEvent, _window, cx| { - acp_onboarding_event!("Canceled", trigger = "X click"); - cx.emit(DismissEvent); - }, - )), - ); - - v_flex() - .id("acp-onboarding") - .key_context("AcpOnboardingModal") - .relative() - .w(rems(34.)) - .h_full() - .elevation_3(cx) - .track_focus(&self.focus_handle(cx)) - .overflow_hidden() - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { - acp_onboarding_event!("Canceled", trigger = "Action"); - cx.emit(DismissEvent); - })) - .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| { - this.focus_handle.focus(window); - })) - .child(illustration) - .child( - v_flex() - .p_4() - .gap_2() - .child(heading) - .child(Label::new(copy).color(Color::Muted)) - .child( - v_flex() - .w_full() - .mt_2() - .gap_1() - .child(open_panel_button) - .child(docs_button), - ), - ) - .child(close_button) - } -} diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 7193c09947..9df41906d7 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -43,11 +43,3 @@ pub fn ai_privacy_and_security(cx: &App) -> String { server_url = server_url(cx) ) } - -/// Returns the URL to Zed AI's external agents documentation. -pub fn external_agents_docs(cx: &App) -> String { - format!( - "{server_url}/docs/ai/external-agents", - server_url = server_url(cx) - ) -} diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs index 01cf403083..5be97c36bc 100644 --- a/crates/command_palette/src/persistence.rs +++ b/crates/command_palette/src/persistence.rs @@ -1,10 +1,7 @@ use anyhow::Result; use db::{ - query, - sqlez::{ - bindable::Column, domain::Domain, statement::Statement, - thread_safe_connection::ThreadSafeConnection, - }, + define_connection, query, + sqlez::{bindable::Column, statement::Statement}, sqlez_macros::sql, }; use serde::{Deserialize, Serialize}; @@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation { } } -pub struct CommandPaletteDB(ThreadSafeConnection); - -impl Domain for CommandPaletteDB { - const NAME: &str = stringify!(CommandPaletteDB); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> = + &[sql!( CREATE TABLE IF NOT EXISTS command_invocations( id INTEGER PRIMARY KEY AUTOINCREMENT, command_name TEXT NOT NULL, @@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB { last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL ) STRICT; )]; -} - -db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []); +); impl CommandPaletteDB { pub async fn write_command_invocation( diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 0802bd8bb7..8b790cbec8 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -110,14 +110,11 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection { } /// Implements a basic DB wrapper for a given domain -/// -/// Arguments: -/// - static variable name for connection -/// - type of connection wrapper -/// - dependencies, whose migrations should be run prior to this domain's migrations #[macro_export] -macro_rules! static_connection { - ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { +macro_rules! define_connection { + (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + impl ::std::ops::Deref for $t { type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; @@ -126,6 +123,16 @@ macro_rules! static_connection { } } + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + impl $t { #[cfg(any(test, feature = "test-support"))] pub async fn open_test_db(name: &'static str) -> Self { @@ -135,8 +142,7 @@ macro_rules! static_connection { #[cfg(any(test, feature = "test-support"))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id)))) + $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] @@ -147,10 +153,46 @@ macro_rules! static_connection { } else { $crate::RELEASE_CHANNEL.dev_name() }; - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope))) + $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) }); - } + }; + (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + + impl ::std::ops::Deref for $t { + type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + + #[cfg(any(test, feature = "test-support"))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id)))) + }); + + #[cfg(not(any(test, feature = "test-support")))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + let db_dir = $crate::database_dir(); + let scope = if false $(|| stringify!($global) == "global")? { + "global" + } else { + $crate::RELEASE_CHANNEL.dev_name() + }; + $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope))) + }); + }; } pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) @@ -177,12 +219,17 @@ mod tests { enum BadDB {} impl Domain for BadDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[ - sql!(CREATE TABLE test(value);), - // failure because test already exists - sql!(CREATE TABLE test(value);), - ]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ] + } } let tempdir = tempfile::Builder::new() @@ -204,15 +251,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() @@ -248,16 +305,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; + fn name() -> &'static str { + "db_tests" + } - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 8ea877b35b..256b789c9b 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -2,26 +2,16 @@ use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; -use crate::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - write_and_log, -}; +use crate::{define_connection, query, write_and_log}; -pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for KeyValueStore { - const NAME: &str = stringify!(KeyValueStore); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); +); pub trait Dismissable { const KEY: &'static str; @@ -101,19 +91,15 @@ mod tests { } } -pub struct GlobalKeyValueStore(ThreadSafeConnection); - -impl Domain for GlobalKeyValueStore { - const NAME: &str = stringify!(GlobalKeyValueStore); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global); + global +); impl GlobalKeyValueStore { query! { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 233dba4c52..a9518314e3 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -10,7 +10,7 @@ use db::kvp::KEY_VALUE_STORE; use editor::Editor; use gpui::{ Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, - Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, + Task, UniformListScrollHandle, WeakEntity, actions, uniform_list, }; use language::Point; use project::{ @@ -23,8 +23,8 @@ use project::{ worktree_store::WorktreeStore, }; use ui::{ - Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, Scrollbar, - ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, + Divider, DividerColor, FluentBuilder as _, Indicator, IntoElement, ListItem, Render, + StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*, }; use workspace::Workspace; use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint}; @@ -49,7 +49,6 @@ pub(crate) struct BreakpointList { breakpoint_store: Entity, dap_store: Entity, worktree_store: Entity, - scrollbar_state: ScrollbarState, breakpoints: Vec, session: Option>, focus_handle: FocusHandle, @@ -87,7 +86,6 @@ impl BreakpointList { let dap_store = project.dap_store(); let focus_handle = cx.focus_handle(); let scroll_handle = UniformListScrollHandle::new(); - let scrollbar_state = ScrollbarState::new(scroll_handle.clone()); let adapter_name = session.as_ref().map(|session| session.read(cx).adapter()); cx.new(|cx| { @@ -95,7 +93,6 @@ impl BreakpointList { breakpoint_store, dap_store, worktree_store, - scrollbar_state, breakpoints: Default::default(), workspace, session, @@ -576,39 +573,6 @@ impl BreakpointList { .flex_1() } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("breakpoint-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone()).map(|s| s.auto_hide(cx))) - } - pub(crate) fn render_control_strip(&self) -> AnyElement { let selection_kind = self.selection_kind(); let focus_handle = self.focus_handle.clone(); @@ -789,7 +753,7 @@ impl Render for BreakpointList { .size_full() .pt_1() .child(self.render_list(cx)) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) .when_some(self.strip_mode, |this, _| { this.child(Divider::horizontal().color(DividerColor::Border)) .child( diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index e7b7963d3f..65dfd5fe0a 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -9,7 +9,7 @@ use std::{ use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable, - MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle, + MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Subscription, Task, TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, uniform_list, }; @@ -19,7 +19,7 @@ use settings::Settings; use theme::ThemeSettings; use ui::{ ContextMenu, Divider, DropdownMenu, FluentBuilder, IntoElement, PopoverMenuHandle, Render, - Scrollbar, ScrollbarState, StatefulInteractiveElement, Tooltip, prelude::*, + ScrollableHandle, StatefulInteractiveElement, Tooltip, WithScrollbar, prelude::*, }; use workspace::Workspace; @@ -30,7 +30,6 @@ actions!(debugger, [GoToSelectedAddress]); pub(crate) struct MemoryView { workspace: WeakEntity, scroll_handle: UniformListScrollHandle, - scroll_state: ScrollbarState, stack_frame_list: WeakEntity, focus_handle: FocusHandle, view_state: ViewState, @@ -121,11 +120,10 @@ impl ViewState { } } -struct ScrollbarDragging; - static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> = LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}")))); static UNKNOWN_BYTE: SharedString = SharedString::new_static("??"); + impl MemoryView { pub(crate) fn new( session: Entity, @@ -139,10 +137,8 @@ impl MemoryView { let query_editor = cx.new(|cx| Editor::single_line(window, cx)); - let scroll_state = ScrollbarState::new(scroll_handle.clone()); let mut this = Self { workspace, - scroll_state, scroll_handle, stack_frame_list, focus_handle: cx.focus_handle(), @@ -162,43 +158,6 @@ impl MemoryView { this } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("memory-view-vertical-scrollbar") - .on_drag_move(cx.listener(|this, evt, _, cx| { - let did_handle = this.handle_scroll_drag(evt); - cx.notify(); - if did_handle { - cx.stop_propagation() - } - })) - .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scroll_state.clone()).map(|s| s.auto_hide(cx))) - } - fn render_memory(&self, cx: &mut Context) -> UniformList { let weak = cx.weak_entity(); let session = self.session.clone(); @@ -233,10 +192,9 @@ impl MemoryView { .track_scroll(self.scroll_handle.clone()) .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| { let delta = evt.delta.pixel_delta(window.line_height()); - let scroll_handle = this.scroll_state.scroll_handle(); - let size = scroll_handle.content_size(); - let viewport = scroll_handle.viewport(); - let current_offset = scroll_handle.offset(); + let size = this.scroll_handle.content_size(); + let viewport = this.scroll_handle.viewport(); + let current_offset = this.scroll_handle.offset(); let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32; let last_entry_offset_boundary = size.height - first_entry_offset_boundary; if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() { @@ -245,7 +203,8 @@ impl MemoryView { } else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height { this.view_state.schedule_scroll_down(); } - scroll_handle.set_offset(current_offset + point(px(0.), delta.y)); + this.scroll_handle + .set_offset(current_offset + point(px(0.), delta.y)); })) } fn render_query_bar(&self, cx: &Context) -> impl IntoElement { @@ -297,7 +256,7 @@ impl MemoryView { } let row_count = self.view_state.row_count(); debug_assert!(row_count > 1); - let scroll_handle = self.scroll_state.scroll_handle(); + let scroll_handle = &self.scroll_handle; let viewport = scroll_handle.viewport(); if viewport.bottom() < evt.event.position.y { @@ -307,13 +266,15 @@ impl MemoryView { } } - fn handle_scroll_drag(&mut self, evt: &DragMoveEvent) -> bool { - if !self.scroll_state.is_dragging() { - return false; - } + #[allow(unused)] + fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<()>) -> bool { + // todo! + // if !self.scroll_state.is_dragging() { + // return false; + // } let row_count = self.view_state.row_count(); debug_assert!(row_count > 1); - let scroll_handle = self.scroll_state.scroll_handle(); + let scroll_handle = &self.scroll_handle; let viewport = scroll_handle.viewport(); if viewport.bottom() < evt.event.position.y { @@ -943,7 +904,7 @@ impl Render for MemoryView { ) .with_priority(1) })) - .child(self.render_vertical_scrollbar(cx)), + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx), ) } } diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs index 7743cfbdee..4ea763c92c 100644 --- a/crates/debugger_ui/src/session/running/module_list.rs +++ b/crates/debugger_ui/src/session/running/module_list.rs @@ -1,15 +1,15 @@ use anyhow::anyhow; use dap::Module; use gpui::{ - AnyElement, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful, - Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list, + AnyElement, Entity, FocusHandle, Focusable, ScrollStrategy, Subscription, Task, + UniformListScrollHandle, WeakEntity, uniform_list, }; use project::{ ProjectItem as _, ProjectPath, debugger::session::{Session, SessionEvent}, }; use std::{ops::Range, path::Path, sync::Arc}; -use ui::{Scrollbar, ScrollbarState, prelude::*}; +use ui::{WithScrollbar, prelude::*}; use workspace::Workspace; pub struct ModuleList { @@ -18,7 +18,6 @@ pub struct ModuleList { session: Entity, workspace: WeakEntity, focus_handle: FocusHandle, - scrollbar_state: ScrollbarState, entries: Vec, _rebuild_task: Option>, _subscription: Subscription, @@ -44,7 +43,6 @@ impl ModuleList { let scroll_handle = UniformListScrollHandle::new(); Self { - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, session, workspace, @@ -167,38 +165,6 @@ impl ModuleList { self.session .update(cx, |session, cx| session.modules(cx).to_vec()) } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("module-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { let Some(ix) = self.selected_ix else { return }; @@ -313,6 +279,6 @@ impl Render for ModuleList { .size_full() .p_1() .child(self.render_list(window, cx)) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx) } } diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs index a4ea4ab654..b3c500b919 100644 --- a/crates/debugger_ui/src/session/running/stack_frame_list.rs +++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs @@ -5,8 +5,8 @@ use std::time::Duration; use anyhow::{Context as _, Result, anyhow}; use dap::StackFrameId; use gpui::{ - AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton, - Stateful, Subscription, Task, WeakEntity, list, + AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, Subscription, + Task, WeakEntity, list, }; use util::debug_panic; @@ -15,7 +15,7 @@ use language::PointUtf16; use project::debugger::breakpoint_store::ActiveStackFrame; use project::debugger::session::{Session, SessionEvent, StackFrame}; use project::{ProjectItem, ProjectPath}; -use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*}; +use ui::{Tooltip, WithScrollbar, prelude::*}; use workspace::{ItemHandle, Workspace}; use super::RunningState; @@ -35,7 +35,6 @@ pub struct StackFrameList { workspace: WeakEntity, selected_ix: Option, opened_stack_frame_id: Option, - scrollbar_state: ScrollbarState, list_state: ListState, error: Option, _refresh_task: Task<()>, @@ -71,7 +70,6 @@ impl StackFrameList { }); let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); - let scrollbar_state = ScrollbarState::new(list_state.clone()); let mut this = Self { session, @@ -84,7 +82,6 @@ impl StackFrameList { selected_ix: None, opened_stack_frame_id: None, list_state, - scrollbar_state, _refresh_task: Task::ready(()), }; this.schedule_refresh(true, window, cx); @@ -581,39 +578,6 @@ impl StackFrameList { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("stack-frame-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } - fn select_ix(&mut self, ix: Option, cx: &mut Context) { self.selected_ix = ix; cx.notify(); @@ -740,7 +704,7 @@ impl Render for StackFrameList { ) }) .child(self.render_list(window, cx)) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_state.clone(), window, cx) } } diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index b396f0921e..494e6c7f86 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -8,9 +8,8 @@ use dap::{ use editor::Editor; use gpui::{ Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity, - FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription, - TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, - uniform_list, + FocusHandle, Focusable, Hsla, MouseDownEvent, Point, Subscription, TextStyleRefinement, + UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::debugger::{ @@ -18,7 +17,7 @@ use project::debugger::{ session::{Session, SessionEvent, Watcher}, }; use std::{collections::HashMap, ops::Range, sync::Arc}; -use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*}; +use ui::{ContextMenu, ListItem, ScrollableHandle, Tooltip, WithScrollbar, prelude::*}; use util::{debug_panic, maybe}; actions!( @@ -189,7 +188,6 @@ pub struct VariableList { entry_states: HashMap, selected_stack_frame_id: Option, list_handle: UniformListScrollHandle, - scrollbar_state: ScrollbarState, session: Entity, selection: Option, open_context_menu: Option<(Entity, Point, Subscription)>, @@ -235,7 +233,6 @@ impl VariableList { let list_state = UniformListScrollHandle::default(); Self { - scrollbar_state: ScrollbarState::new(list_state.clone()), list_handle: list_state, session, focus_handle, @@ -1500,39 +1497,6 @@ impl VariableList { ) .into_any() } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("variable-list-vertical-scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } impl Focusable for VariableList { @@ -1542,7 +1506,7 @@ impl Focusable for VariableList { } impl Render for VariableList { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .track_focus(&self.focus_handle) .key_context("VariableList") @@ -1587,7 +1551,7 @@ impl Render for VariableList { ) .with_priority(1) })) - .child(self.render_vertical_scrollbar(cx)) + .vertical_scrollbar_for(self.list_handle.clone(), window, cx) } } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c8c3dc54b7..c900eb692a 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); -static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { - load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") -}); - static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); const FRONT_MATTER_COMMENT: &str = ""; @@ -220,7 +216,6 @@ fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, "linux" | "freebsd" => &KEYMAP_LINUX, - "windows" => &KEYMAP_WINDOWS, _ => unreachable!("Not a valid OS: {}", os), }; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80680ae9c0..448852430e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -55,7 +55,7 @@ pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPla pub use edit_prediction::Direction; pub use editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode, - ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar, + ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, }; pub use editor_settings_controls::*; pub use element::{ @@ -165,7 +165,7 @@ use project::{ }; use rand::{seq::SliceRandom, thread_rng}; use rpc::{ErrorCode, ErrorExt, proto::PeerId}; -use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; +use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager}; use selections_collection::{ MutableSelectionsCollection, SelectionsCollection, resolve_selections, }; @@ -198,7 +198,7 @@ use theme::{ }; use ui::{ ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, - IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, + IconSize, Indicator, Key, Tooltip, h_flex, prelude::*, scrollbars::ScrollbarAutoHide, }; use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; use workspace::{ @@ -2588,7 +2588,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.display_modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers.modified()) })) } @@ -7686,16 +7686,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.display_modifiers == modifiers - && accept_keystroke.display_modifiers.modified()); + || (&accept_keystroke.modifiers == modifiers + && accept_keystroke.modifiers.modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.display_modifiers == modifiers - && accept_partial_keystroke.display_modifiers.modified()); + || (&accept_partial_keystroke.modifiers == modifiers + && accept_partial_keystroke.modifiers.modified()); } if modifiers_held { @@ -9044,7 +9044,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +9056,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.display_key.clone()) + parent.child(accept_keystroke.key.clone()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.display_key), + util::capitalize(&accept_keystroke.key), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9171,7 +9171,7 @@ impl Editor { max_width: Pixels, cursor_point: Point, style: &EditorStyle, - accept_keystroke: Option<&gpui::KeybindingKeystroke>, + accept_keystroke: Option<&gpui::Keystroke>, _window: &Window, cx: &mut Context, ) -> Option { @@ -9249,7 +9249,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9319,7 +9319,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.display_modifiers.modified() { + if !accept_keystroke.modifiers.modified() { return el; } @@ -9338,7 +9338,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(if !has_completion { Color::Muted diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 1d7e04cae0..b52353c86d 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -7,6 +7,7 @@ use project::project_settings::DiagnosticSeverity; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, VsCodeSettings}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; use util::serde::default_true; /// Imports from the VSCode settings at @@ -196,23 +197,6 @@ pub struct Gutter { pub folds: bool, } -/// When to show the scrollbar in the editor. -/// -/// Default: auto -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, -} - /// When to show the minimap in the editor. /// /// Default: never @@ -735,6 +719,12 @@ impl EditorSettings { } } +impl ScrollbarVisibilitySetting for EditorSettings { + fn scrollbar_visibility(&self, _cx: &App) -> ShowScrollbar { + self.scrollbar.show + } +} + impl Settings for EditorSettings { const KEY: Option<&'static str> = None; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 91034829f7..70cd413c01 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -18,7 +18,7 @@ use crate::{ editor_settings::{ CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap, MinimapThumb, MinimapThumbBorder, ScrollBeyondLastLine, ScrollbarAxes, - ScrollbarDiagnostics, ShowMinimap, ShowScrollbar, + ScrollbarDiagnostics, ShowMinimap, }, git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer}, hover_popover::{ @@ -43,10 +43,10 @@ use gpui::{ Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, - ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; @@ -84,7 +84,7 @@ use text::{BufferId, SelectionGoal}; use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor}; use ui::{ ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*, - right_click_menu, + right_click_menu, scrollbars::ShowScrollbar, }; use unicode_segmentation::UnicodeSegmentation; use util::post_inc; @@ -7150,7 +7150,7 @@ fn header_jump_data( pub struct AcceptEditPredictionBinding(pub(crate) Option); impl AcceptEditPredictionBinding { - pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { + pub fn keystroke(&self) -> Option<&Keystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { [keystroke, ..] => Some(keystroke), diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fab5345787..e65c6b1807 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -9,8 +9,8 @@ use anyhow::Context as _; use gpui::{ AnyElement, AsyncWindowContext, Context, Entity, Focusable as _, FontWeight, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, Size, - Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, - TextStyleRefinement, Window, div, px, + StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Task, TextStyleRefinement, + Window, div, px, }; use itertools::Itertools; use language::{DiagnosticEntry, Language, LanguageRegistry}; @@ -23,7 +23,7 @@ use std::{borrow::Cow, cell::RefCell}; use std::{ops::Range, sync::Arc, time::Duration}; use std::{path::PathBuf, rc::Rc}; use theme::ThemeSettings; -use ui::{Scrollbar, ScrollbarState, prelude::*, theme_is_transparent}; +use ui::{Scrollbars, WithScrollbar, prelude::*, theme_is_transparent}; use url::Url; use util::TryFutureExt; use workspace::{OpenOptions, OpenVisible, Workspace}; @@ -184,7 +184,6 @@ pub fn hover_at_inlay( let hover_popover = InfoPopover { symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), parsed_content, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(false)), anchor: None, @@ -387,7 +386,6 @@ fn show_hover( local_diagnostic, markdown, border_color, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, background_color, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), @@ -457,7 +455,6 @@ fn show_hover( info_popovers.push(InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), @@ -507,7 +504,6 @@ fn show_hover( info_popovers.push(InfoPopover { symbol_range: RangeInEditor::Text(range), parsed_content, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, keyboard_grace: Rc::new(RefCell::new(ignore_timeout)), anchor: Some(anchor), @@ -846,7 +842,6 @@ pub struct InfoPopover { pub symbol_range: RangeInEditor, pub parsed_content: Option>, pub scroll_handle: ScrollHandle, - pub scrollbar_state: ScrollbarState, pub keyboard_grace: Rc>, pub anchor: Option, _subscription: Option, @@ -891,7 +886,12 @@ impl InfoPopover { .on_url_click(open_markdown_url), ), ) - .child(self.render_vertical_scrollbar(cx)) + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()), + window, + cx, + ) }) .into_any_element() } @@ -905,39 +905,6 @@ impl InfoPopover { cx.notify(); self.scroll_handle.set_offset(current); } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("info-popover-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } pub struct DiagnosticPopover { @@ -949,7 +916,6 @@ pub struct DiagnosticPopover { pub anchor: Anchor, _subscription: Subscription, pub scroll_handle: ScrollHandle, - pub scrollbar_state: ScrollbarState, } impl DiagnosticPopover { @@ -1013,43 +979,15 @@ impl DiagnosticPopover { ), ), ) - .child(self.render_vertical_scrollbar(cx)), + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()), + window, + cx, + ), ) .into_any_element() } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("diagnostic-popover-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|_, _, _, cx| { - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } #[cfg(test)] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b7110190fd..641e8a97ed 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1404,7 +1404,7 @@ impl ProjectItem for Editor { } fn for_broken_project_item( - abs_path: &Path, + abs_path: PathBuf, is_local: bool, e: &anyhow::Error, window: &mut Window, diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index ec7c149b4e..88fde53947 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use db::{ - query, - sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - domain::Domain, - statement::Statement, - }, - sqlez_macros::sql, -}; +use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; +use db::sqlez::statement::Statement; use fs::MTime; use itertools::Itertools as _; use std::path::PathBuf; +use db::sqlez_macros::sql; +use db::{define_connection, query}; + use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)] @@ -87,11 +83,7 @@ impl Column for SerializedEditor { } } -pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for EditorDb { - const NAME: &str = stringify!(EditorDb); - +define_connection!( // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -121,8 +113,7 @@ impl Domain for EditorDb { // start: usize, // end: usize, // ) - - const MIGRATIONS: &[&str] = &[ + pub static ref DB: EditorDb = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -198,9 +189,7 @@ impl Domain for EditorDb { ) STRICT; ), ]; -} - -db::static_connection!(DB, EditorDb, [WorkspaceDb]); +); // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 8231448618..828ab0594d 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -12,7 +12,7 @@ use crate::{ }; pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use core::fmt::Debug; -use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px}; +use gpui::{Along, App, Axis, Context, Pixels, Task, Window, point, px}; use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; @@ -21,6 +21,7 @@ use std::{ cmp::Ordering, time::{Duration, Instant}, }; +use ui::scrollbars::ScrollbarAutoHide; use util::ResultExt; use workspace::{ItemId, WorkspaceId}; @@ -29,11 +30,6 @@ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); pub struct WasScrolled(pub(crate) bool); -#[derive(Default)] -pub struct ScrollbarAutoHide(pub bool); - -impl Global for ScrollbarAutoHide {} - #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { pub offset: gpui::Point, @@ -327,7 +323,7 @@ impl ScrollManager { cx.notify(); } - if cx.default_global::().0 { + if cx.default_global::().should_hide() { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor() .timer(SCROLLBAR_SHOW_INTERVAL) diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index cb21f35d7e..54d8a50115 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -2,8 +2,8 @@ use crate::actions::ShowSignatureHelp; use crate::hover_popover::open_markdown_url; use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style}; use gpui::{ - App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful, - StyledText, Task, TextStyle, Window, combine_highlights, + App, Context, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, StyledText, Task, + TextStyle, Window, combine_highlights, }; use language::BufferSnapshot; use markdown::{Markdown, MarkdownElement}; @@ -15,8 +15,8 @@ use theme::ThemeSettings; use ui::{ ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton, IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, - LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString, - StatefulInteractiveElement, Styled, StyledExt, div, px, relative, + LabelSize, ParentElement, Pixels, SharedString, StatefulInteractiveElement, Styled, StyledExt, + WithScrollbar, div, relative, }; // Language-specific settings may define quotes as "brackets", so filter them out separately. @@ -243,7 +243,6 @@ impl Editor { .min(signatures.len().saturating_sub(1)); let signature_help_popover = SignatureHelpPopover { - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), style, signatures, current_signature, @@ -330,7 +329,6 @@ pub struct SignatureHelpPopover { pub signatures: Vec, pub current_signature: usize, scroll_handle: ScrollHandle, - scrollbar_state: ScrollbarState, } impl SignatureHelpPopover { @@ -391,7 +389,8 @@ impl SignatureHelpPopover { ) }), ) - .child(self.render_vertical_scrollbar(cx)); + .vertical_scrollbar(window, cx); + let controls = if self.signatures.len() > 1 { let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp) .shape(IconButtonShape::Square) @@ -460,26 +459,4 @@ impl SignatureHelpPopover { .child(main_content) .into_any_element() } - - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Stateful
{ - div() - .occlude() - .id("signature_help_scrollbar") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| cx.stop_propagation()) - .on_any_mouse_down(|_, _, cx| cx.stop_propagation()) - .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify())) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_1() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.scrollbar_state.clone())) - } } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index fd504764b6..427e1a02aa 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -24,8 +24,8 @@ use settings::Settings; use strum::IntoEnumIterator as _; use theme::ThemeSettings; use ui::{ - CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState, - ToggleButton, Tooltip, prelude::*, + CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, ToggleButton, Tooltip, + WithScrollbar, prelude::*, }; use vim_mode_setting::VimModeSetting; use workspace::{ @@ -290,7 +290,6 @@ pub struct ExtensionsPage { _subscriptions: [gpui::Subscription; 2], extension_fetch_task: Option>, upsells: BTreeSet, - scrollbar_state: ScrollbarState, } impl ExtensionsPage { @@ -339,7 +338,7 @@ impl ExtensionsPage { let mut this = Self { workspace: workspace.weak_handle(), - list: scroll_handle.clone(), + list: scroll_handle, is_fetching_extensions: false, filter: ExtensionFilter::All, dev_extension_entries: Vec::new(), @@ -351,7 +350,6 @@ impl ExtensionsPage { _subscriptions: subscriptions, query_editor, upsells: BTreeSet::default(), - scrollbar_state: ScrollbarState::new(scroll_handle), }; this.fetch_extensions( this.search_query(cx), @@ -1375,7 +1373,7 @@ impl ExtensionsPage { } impl Render for ExtensionsPage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .bg(cx.theme().colors().editor_background) @@ -1520,25 +1518,24 @@ impl Render for ExtensionsPage { } if count == 0 { - return this.py_4().child(self.render_empty_state(cx)); - } - - let scroll_handle = self.list.clone(); - this.child( - uniform_list("entries", count, cx.processor(Self::render_extensions)) + this.py_4() + .child(self.render_empty_state(cx)) + .into_any_element() + } else { + let scroll_handle = self.list.clone(); + this.child( + uniform_list( + "entries", + count, + cx.processor(Self::render_extensions), + ) .flex_grow() .pb_4() - .track_scroll(scroll_handle), - ) - .child( - div() - .absolute() - .right_1() - .top_0() - .bottom_0() - .w(px(12.)) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) + .track_scroll(scroll_handle.clone()), + ) + .vertical_scrollbar_for(scroll_handle, window, cx) + .into_any_element() + } }), ) } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f5f7fc42b3..422979c429 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag { // integration too, and we'd like to turn Gemini/Native on in new builds // without enabling Claude Code in old builds. const NAME: &'static str = "gemini-and-native"; - - fn enabled_for_all() -> bool { - true - } } pub struct ClaudeCodeFeatureFlag; @@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(T::enabled_for_all()) + .unwrap_or(false) } fn is_staff(&self) -> bool { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829..ef0ab34394 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -13,10 +13,7 @@ use agent_settings::AgentSettings; use anyhow::Context as _; use askpass::AskPassDelegate; use db::kvp::KEY_VALUE_STORE; -use editor::{ - Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar, - scroll::ScrollbarAutoHide, -}; +use editor::{Editor, EditorElement, EditorMode, MultiBuffer}; use futures::StreamExt as _; use git::blame::ParsedCommitMessage; use git::repository::{ @@ -31,7 +28,7 @@ use git::{ UnstageAll, }; use gpui::{ - Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, + Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, @@ -63,9 +60,10 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ - Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar, - ScrollbarState, SplitButton, Tooltip, prelude::*, + Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, + SplitButton, Tooltip, prelude::*, }; +use ui::{ScrollAxes, Scrollbars, WithScrollbar}; use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; @@ -276,61 +274,6 @@ struct PendingOperation { op_id: usize, } -// computed state related to how to render scrollbars -// one per axis -// on render we just read this off the panel -// we update it when -// - settings change -// - on focus in, on focus out, on hover, etc. -#[derive(Debug)] -struct ScrollbarProperties { - axis: Axis, - show_scrollbar: bool, - show_track: bool, - auto_hide: bool, - hide_task: Option>, - state: ScrollbarState, -} - -impl ScrollbarProperties { - // Shows the scrollbar and cancels any pending hide task - fn show(&mut self, cx: &mut Context) { - if !self.auto_hide { - return; - } - self.show_scrollbar = true; - self.hide_task.take(); - cx.notify(); - } - - fn hide(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - - if !self.auto_hide { - return; - } - - let axis = self.axis; - self.hide_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - - if let Some(panel) = panel.upgrade() { - panel - .update(cx, |panel, cx| { - match axis { - Axis::Vertical => panel.vertical_scrollbar.show_scrollbar = false, - Axis::Horizontal => panel.horizontal_scrollbar.show_scrollbar = false, - } - cx.notify(); - }) - .log_err(); - } - })); - } -} - pub struct GitPanel { pub(crate) active_repository: Option>, pub(crate) commit_editor: Entity, @@ -343,8 +286,6 @@ pub struct GitPanel { single_tracked_entry: Option, focus_handle: FocusHandle, fs: Arc, - horizontal_scrollbar: ScrollbarProperties, - vertical_scrollbar: ScrollbarProperties, new_count: usize, entry_count: usize, new_staged_count: usize, @@ -429,10 +370,6 @@ impl GitPanel { cx.new(|cx| { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, window, Self::focus_in).detach(); - cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { - this.hide_scrollbars(window, cx); - }) - .detach(); let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path; cx.observe_global::(move |this, cx| { @@ -457,24 +394,6 @@ impl GitPanel { let scroll_handle = UniformListScrollHandle::new(); - let vertical_scrollbar = ScrollbarProperties { - axis: Axis::Vertical, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - - let horizontal_scrollbar = ScrollbarProperties { - axis: Axis::Horizontal, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - let mut assistant_enabled = AgentSettings::get_global(cx).enabled; let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; let _settings_subscription = cx.observe_global::(move |_, cx| { @@ -555,8 +474,6 @@ impl GitPanel { workspace: workspace.weak_handle(), modal_open: false, entry_count: 0, - horizontal_scrollbar, - vertical_scrollbar, bulk_staging: None, _settings_subscription, }; @@ -566,86 +483,6 @@ impl GitPanel { }) } - fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { - self.horizontal_scrollbar.hide(window, cx); - self.vertical_scrollbar.hide(window, cx); - } - - fn update_scrollbar_properties(&mut self, _window: &mut Window, cx: &mut Context) { - // TODO: This PR should have defined Editor's `scrollbar.axis` - // as an Option, not a ScrollbarAxes as it would allow you to - // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`. - // - // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis` - // so we can show each axis based on the settings. - // - // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495 - - let show_setting = GitPanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or(EditorSettings::get_global(cx).scrollbar.show); - - let scroll_handle = self.scroll_handle.0.borrow(); - - let autohide = |show: ShowScrollbar, cx: &mut Context| match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => false, - }; - - let longest_item_width = scroll_handle.last_item_size.and_then(|size| { - (size.contents.width > size.item.width).then_some(size.contents.width) - }); - - // is there an item long enough that we should show a horizontal scrollbar? - let item_wider_than_container = if let Some(longest_item_width) = longest_item_width { - longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0) - } else { - true - }; - - let show_horizontal = match (show_setting, item_wider_than_container) { - (_, false) => false, - (ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always, true) => true, - (ShowScrollbar::Never, true) => false, - }; - - let show_vertical = match show_setting { - ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - }; - - let show_horizontal_track = - show_horizontal && matches!(show_setting, ShowScrollbar::Always); - - // TODO: we probably should hide the scroll track when the list doesn't need to scroll - let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always); - - self.vertical_scrollbar = ScrollbarProperties { - axis: self.vertical_scrollbar.axis, - state: self.vertical_scrollbar.state.clone(), - show_scrollbar: show_vertical, - show_track: show_vertical_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - self.horizontal_scrollbar = ScrollbarProperties { - axis: self.horizontal_scrollbar.axis, - state: self.horizontal_scrollbar.state.clone(), - show_scrollbar: show_horizontal, - show_track: show_horizontal_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - cx.notify(); - } - pub fn entry_by_path(&self, path: &RepoPath, cx: &App) -> Option { if GitPanelSettings::get_global(cx).sort_by_path { return self @@ -2594,12 +2431,11 @@ impl GitPanel { cx.background_executor().timer(UPDATE_DEBOUNCE).await; if let Some(git_panel) = handle.upgrade() { git_panel - .update_in(cx, |git_panel, window, cx| { + .update(cx, |git_panel, cx| { if clear_pending { git_panel.clear_pending(); } git_panel.update_visible_entries(cx); - git_panel.update_scrollbar_properties(window, cx); }) .ok(); } @@ -3710,110 +3546,6 @@ impl GitPanel { ) } - fn render_vertical_scrollbar( - &self, - show_horizontal_scrollbar_container: bool, - cx: &mut Context, - ) -> impl IntoElement { - div() - .id("git-panel-vertical-scroll") - .occlude() - .flex_none() - .h_full() - .cursor_default() - .absolute() - .right_0() - .top_0() - .bottom_0() - .w(px(12.)) - .when(show_horizontal_scrollbar_container, |this| { - this.pb_neg_3p5() - }) - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.vertical_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.vertical_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical( - // percentage as f32..end_offset as f32, - self.vertical_scrollbar.state.clone(), - )) - } - - /// Renders the horizontal scrollbar. - /// - /// The right offset is used to determine how far to the right the - /// scrollbar should extend to, useful for ensuring it doesn't collide - /// with the vertical scrollbar when visible. - fn render_horizontal_scrollbar( - &self, - right_offset: Pixels, - cx: &mut Context, - ) -> impl IntoElement { - div() - .id("git-panel-horizontal-scroll") - .occlude() - .flex_none() - .w_full() - .cursor_default() - .absolute() - .bottom_neg_px() - .left_0() - .right_0() - .pr(right_offset) - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.horizontal_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.horizontal_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::horizontal( - // percentage as f32..end_offset as f32, - self.horizontal_scrollbar.state.clone(), - )) - } - fn render_buffer_header_controls( &self, entity: &Entity, @@ -3861,33 +3593,16 @@ impl GitPanel { fn render_entries( &self, has_write_access: bool, - _: &Window, + window: &mut Window, cx: &mut Context, ) -> impl IntoElement { let entry_count = self.entries.len(); - let scroll_track_size = px(16.); - - let h_scroll_offset = if self.vertical_scrollbar.show_scrollbar { - // magic number - px(3.) - } else { - px(0.) - }; - v_flex() .flex_1() .size_full() .overflow_hidden() .relative() - // Show a border on the top and bottom of the container when - // the vertical scrollbar container is visible so we don't have a - // floating left border in the panel. - .when(self.vertical_scrollbar.show_track, |this| { - this.border_t_1() - .border_b_1() - .border_color(cx.theme().colors().border) - }) .child( h_flex() .flex_1() @@ -3928,15 +3643,6 @@ impl GitPanel { items }), ) - .when( - !self.horizontal_scrollbar.show_track - && self.horizontal_scrollbar.show_scrollbar, - |this| { - // when not showing the horizontal scrollbar track, make sure we don't - // obscure the last entry - this.pb(scroll_track_size) - }, - ) .size_full() .flex_grow() .with_sizing_behavior(ListSizingBehavior::Auto) @@ -3952,72 +3658,14 @@ impl GitPanel { this.deploy_panel_context_menu(event.position, window, cx) }), ) - .when(self.vertical_scrollbar.show_track, |this| { - this.child( - v_flex() - .h_full() - .flex_none() - .w(scroll_track_size) - .bg(cx.theme().colors().panel_background) - .child( - div() - .size_full() - .flex_1() - .border_l_1() - .border_color(cx.theme().colors().border), - ), - ) - }) - .when(self.vertical_scrollbar.show_scrollbar, |this| { - this.child( - self.render_vertical_scrollbar( - self.horizontal_scrollbar.show_track, - cx, - ), - ) - }), + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal), + window, + cx, + ), ) - .when(self.horizontal_scrollbar.show_track, |this| { - this.child( - h_flex() - .w_full() - .h(scroll_track_size) - .flex_none() - .relative() - .child( - div() - .w_full() - .flex_1() - // for some reason the horizontal scrollbar is 1px - // taller than the vertical scrollbar?? - .h(scroll_track_size - px(1.)) - .bg(cx.theme().colors().panel_background) - .border_t_1() - .border_color(cx.theme().colors().border), - ) - .when(self.vertical_scrollbar.show_track, |this| { - this.child( - div() - .flex_none() - // -1px prevents a missing pixel between the two container borders - .w(scroll_track_size - px(1.)) - .h_full(), - ) - .child( - // HACK: Fill the missing 1px 🥲 - div() - .absolute() - .right(scroll_track_size - px(1.)) - .bottom(scroll_track_size - px(1.)) - .size_px() - .bg(cx.theme().colors().border), - ) - }), - ) - }) - .when(self.horizontal_scrollbar.show_scrollbar, |this| { - this.child(self.render_horizontal_scrollbar(h_scroll_offset, cx)) - }) } fn entry_label(&self, label: impl Into, color: Color) -> Label { @@ -4466,7 +4114,7 @@ fn current_language_model(cx: &Context<'_, GitPanel>) -> Option ShowScrollbar { + // TODO: This PR should have defined Editor's `scrollbar.axis` + // as an Option, not a ScrollbarAxes as it would allow you to + // `.unwrap_or(EditorSettings::get_global(cx).scrollbar.show)`. + // + // Once this is fixed we can extend the GitPanelSettings with a `scrollbar.axis` + // so we can show each axis based on the settings. + // + // We should fix this. PR: https://github.com/zed-industries/zed/pull/19495 + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for GitPanelSettings { const KEY: Option<&'static str> = Some("git_panel"); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b59d7e717a..bbd59fa7bc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -37,10 +37,10 @@ use crate::{ AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder, - PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, - Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, - Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, + PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, + PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, + SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, + WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -263,7 +263,6 @@ pub struct App { pub(crate) focus_handles: Arc, pub(crate) keymap: Rc>, pub(crate) keyboard_layout: Box, - pub(crate) keyboard_mapper: Rc, pub(crate) global_action_listeners: FxHashMap>>, pending_effects: VecDeque, @@ -313,7 +312,6 @@ impl App { let text_system = Arc::new(TextSystem::new(platform.text_system())); let entities = EntityMap::new(); let keyboard_layout = platform.keyboard_layout(); - let keyboard_mapper = platform.keyboard_mapper(); let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(App { @@ -339,7 +337,6 @@ impl App { focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), keymap: Rc::new(RefCell::new(Keymap::default())), keyboard_layout, - keyboard_mapper, global_action_listeners: FxHashMap::default(), pending_effects: VecDeque::new(), pending_notifications: FxHashSet::default(), @@ -379,7 +376,6 @@ impl App { if let Some(app) = app.upgrade() { let cx = &mut app.borrow_mut(); cx.keyboard_layout = cx.platform.keyboard_layout(); - cx.keyboard_mapper = cx.platform.keyboard_mapper(); cx.keyboard_layout_observers .clone() .retain(&(), move |callback| (callback)(cx)); @@ -428,11 +424,6 @@ impl App { self.keyboard_layout.as_ref() } - /// Get the current keyboard mapper. - pub fn keyboard_mapper(&self) -> &Rc { - &self.keyboard_mapper - } - /// Invokes a handler when the current keyboard layout changes pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription where diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c9826b704e..de5b720d46 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -16,10 +16,10 @@ //! constructed by combining these two systems into an all-in-one element. use crate::{ - Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, DispatchPhase, - Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, HitboxBehavior, - HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, KeyUpEvent, - KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, + AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, + DispatchPhase, Element, ElementId, Entity, FocusHandle, Global, GlobalElementId, Hitbox, + HitboxBehavior, HitboxId, InspectorElementId, IntoElement, IsZero, KeyContext, KeyDownEvent, + KeyUpEvent, KeyboardButton, KeyboardClickEvent, LayoutId, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Overflow, ParentElement, Pixels, Point, Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, TooltipId, Visibility, Window, WindowControlArea, point, px, size, @@ -1036,6 +1036,15 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + /// Set the space to be reserved for rendering the scrollbar. + /// + /// This will only affect the layout of the element when overflow for this element is set to + /// `Overflow::Scroll`. + fn scrollbar_width(mut self, width: impl Into) -> Self { + self.interactivity().base_style.scrollbar_width = Some(width.into()); + self + } + /// Track the scroll state of this element with the given handle. fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { self.interactivity().tracked_scroll_handle = Some(scroll_handle.clone()); diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index cdf90d4eb8..9c601aac1d 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -5,10 +5,10 @@ //! elements with uniform height. use crate::{ - AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId, - Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId, - ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled, - Window, point, size, + AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, Entity, + GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, + IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, + StyleRefinement, Styled, Window, point, size, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -71,7 +71,7 @@ pub struct UniformList { /// Frame state used by the [UniformList]. pub struct UniformListFrameState { items: SmallVec<[AnyElement; 32]>, - decorations: SmallVec<[AnyElement; 1]>, + decorations: SmallVec<[AnyElement; 2]>, } /// A handle for controlling the scroll position of a uniform list. @@ -529,6 +529,31 @@ pub trait UniformListDecoration { ) -> AnyElement; } +impl UniformListDecoration for Entity { + fn compute( + &self, + visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + item_count: usize, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + self.update(cx, |inner, cx| { + inner.compute( + visible_range, + bounds, + scroll_offset, + item_height, + item_count, + window, + cx, + ) + }) + } +} + impl UniformList { /// Selects a specific list item for measurement. pub fn with_width_from_item(mut self, item_index: Option) -> Self { diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b3db09d821..757205fcc3 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ mod context; pub use binding::*; pub use context::*; -use crate::{Action, AsKeystroke, Keystroke, is_no_action}; +use crate::{Action, Keystroke, is_no_action}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::any::TypeId; @@ -141,7 +141,7 @@ impl Keymap { /// only. pub fn bindings_for_input( &self, - input: &[impl AsKeystroke], + input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[KeyBinding; 1]>, bool) { let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); @@ -192,6 +192,7 @@ impl Keymap { (bindings, !pending.is_empty()) } + /// Check if the given binding is enabled, given a certain key context. /// Returns the deepest depth at which the binding matches, or None if it doesn't match. fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { @@ -638,7 +639,7 @@ mod tests { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { let actual = keymap .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner.unparse()) + .map(|binding| binding.keystrokes[0].unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index a7cf9d5c54..729498d153 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,15 +1,14 @@ use std::rc::Rc; -use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, -}; +use collections::HashMap; + +use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; use smallvec::SmallVec; /// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) context_predicate: Option>, pub(crate) meta: Option, /// The json input string used when building the keybinding, if any @@ -33,15 +32,7 @@ impl KeyBinding { pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context_predicate = context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load( - keystrokes, - Box::new(action), - context_predicate, - false, - None, - &DummyKeyboardMapper, - ) - .unwrap() + Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } /// Load a keybinding from the given raw data. @@ -49,22 +40,24 @@ impl KeyBinding { keystrokes: &str, action: Box, context_predicate: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, action_input: Option, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> std::result::Result { - let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new( - keystroke, - use_key_equivalents, - keyboard_mapper, - )) - }) + .map(Keystroke::parse) .collect::>()?; + if let Some(equivalents) = key_equivalents { + for keystroke in keystrokes.iter_mut() { + if keystroke.key.chars().count() == 1 + && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + } + Ok(Self { keystrokes, action, @@ -86,13 +79,13 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { if self.keystrokes.len() < typed.len() { return None; } for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.as_keystroke().should_match(target) { + if !typed.should_match(target) { return None; } } @@ -101,7 +94,7 @@ impl KeyBinding { } /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f64710bc56..4d2feeaf1d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static { fn on_quit(&self, callback: Box); fn on_reopen(&self, callback: Box); + fn on_keyboard_layout_change(&self, callback: Box); fn set_menus(&self, menus: Vec, keymap: &Keymap); fn get_menus(&self) -> Option> { @@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static { fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); + fn keyboard_layout(&self) -> Box; fn compositor_name(&self) -> &'static str { "" @@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index 10b8620258..e28d781520 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -1,7 +1,3 @@ -use collections::HashMap; - -use crate::{KeybindingKeystroke, Keystroke}; - /// A trait for platform-specific keyboard layouts pub trait PlatformKeyboardLayout { /// Get the keyboard layout ID, which should be unique to the layout @@ -9,33 +5,3 @@ pub trait PlatformKeyboardLayout { /// Get the keyboard layout display name fn name(&self) -> &str; } - -/// A trait for platform-specific keyboard mappings -pub trait PlatformKeyboardMapper { - /// Map a key equivalent to its platform-specific representation - fn map_key_equivalent( - &self, - keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke; - /// Get the key equivalents for the current keyboard layout, - /// only used on macOS - fn get_key_equivalents(&self) -> Option<&HashMap>; -} - -/// A dummy implementation of the platform keyboard mapper -pub struct DummyKeyboardMapper; - -impl PlatformKeyboardMapper for DummyKeyboardMapper { - fn map_key_equivalent( - &self, - keystroke: Keystroke, - _use_key_equivalents: bool, - ) -> KeybindingKeystroke { - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 6ce17c3a01..24601eefd6 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -5,14 +5,6 @@ use std::{ fmt::{Display, Write}, }; -use crate::PlatformKeyboardMapper; - -/// This is a helper trait so that we can simplify the implementation of some functions -pub trait AsKeystroke { - /// Returns the GPUI representation of the keystroke. - fn as_keystroke(&self) -> &Keystroke; -} - /// A keystroke and associated metadata generated by the platform #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { @@ -32,17 +24,6 @@ pub struct Keystroke { pub key_char: Option, } -/// Represents a keystroke that can be used in keybindings and displayed to the user. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct KeybindingKeystroke { - /// The GPUI representation of the keystroke. - pub inner: Keystroke, - /// The modifiers to display. - pub display_modifiers: Modifiers, - /// The key to display. - pub display_key: String, -} - /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// markdown to display it. #[derive(Debug)] @@ -77,7 +58,7 @@ impl Keystroke { /// /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. - pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { + pub fn should_match(&self, target: &Keystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char @@ -90,7 +71,7 @@ impl Keystroke { ..Default::default() }; - if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -102,12 +83,12 @@ impl Keystroke { .filter(|key_char| key_char != &&self.key) { // On Windows, if key_char is set, then the typed keystroke produced the key_char - if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + if &target.key == key_char && target.modifiers == Modifiers::none() { return true; } } - target.inner.modifiers == self.modifiers && target.inner.key == self.key + target.modifiers == self.modifiers && target.key == self.key } /// key syntax is: @@ -219,7 +200,31 @@ impl Keystroke { /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - unparse(&self.modifiers, &self.key) + let mut str = String::new(); + if self.modifiers.function { + str.push_str("fn-"); + } + if self.modifiers.control { + str.push_str("ctrl-"); + } + if self.modifiers.alt { + str.push_str("alt-"); + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + str.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + str.push_str("super-"); + + #[cfg(target_os = "windows")] + str.push_str("win-"); + } + if self.modifiers.shift { + str.push_str("shift-"); + } + str.push_str(&self.key); + str } /// Returns true if this keystroke left @@ -261,32 +266,6 @@ impl Keystroke { } } -impl KeybindingKeystroke { - /// Create a new keybinding keystroke from the given keystroke - pub fn new( - inner: Keystroke, - use_key_equivalents: bool, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> Self { - keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) - } - - pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - unparse(&self.display_modifiers, &self.display_key) - } -} - fn is_printable_key(key: &str) -> bool { !matches!( key, @@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.modifiers, f)?; - display_key(&self.key, f) - } -} + if self.modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; -impl std::fmt::Display for KeybindingKeystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.display_modifiers, f)?; - display_key(&self.display_key, f) + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if self.modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if self.modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + let key = match self.key.as_str() { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) } } @@ -571,110 +600,3 @@ pub struct Capslock { #[serde(default)] pub on: bool, } - -impl AsKeystroke for Keystroke { - fn as_keystroke(&self) -> &Keystroke { - self - } -} - -impl AsKeystroke for KeybindingKeystroke { - fn as_keystroke(&self) -> &Keystroke { - &self.inner - } -} - -fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if modifiers.control { - #[cfg(target_os = "macos")] - f.write_char('^')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "ctrl-")?; - } - if modifiers.alt { - #[cfg(target_os = "macos")] - f.write_char('⌥')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "alt-")?; - } - if modifiers.platform { - #[cfg(target_os = "macos")] - f.write_char('⌘')?; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - f.write_char('❖')?; - - #[cfg(target_os = "windows")] - f.write_char('⊞')?; - } - if modifiers.shift { - #[cfg(target_os = "macos")] - f.write_char('⇧')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "shift-")?; - } - Ok(()) -} - -fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let key = match key { - #[cfg(target_os = "macos")] - "backspace" => '⌫', - #[cfg(target_os = "macos")] - "up" => '↑', - #[cfg(target_os = "macos")] - "down" => '↓', - #[cfg(target_os = "macos")] - "left" => '←', - #[cfg(target_os = "macos")] - "right" => '→', - #[cfg(target_os = "macos")] - "tab" => '⇥', - #[cfg(target_os = "macos")] - "escape" => '⎋', - #[cfg(target_os = "macos")] - "shift" => '⇧', - #[cfg(target_os = "macos")] - "control" => '⌃', - #[cfg(target_os = "macos")] - "alt" => '⌥', - #[cfg(target_os = "macos")] - "platform" => '⌘', - - key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), - key => return f.write_str(key), - }; - f.write_char(key) -} - -#[inline] -fn unparse(modifiers: &Modifiers, key: &str) -> String { - let mut result = String::new(); - if modifiers.function { - result.push_str("fn-"); - } - if modifiers.control { - result.push_str("ctrl-"); - } - if modifiers.alt { - result.push_str("alt-"); - } - if modifiers.platform { - #[cfg(target_os = "macos")] - result.push_str("cmd-"); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - result.push_str("super-"); - - #[cfg(target_os = "windows")] - result.push_str("win-"); - } - if modifiers.shift { - result.push_str("shift-"); - } - result.push_str(&key); - result -} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8bd89fc399..3fb1ef4572 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, - Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, - PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, + Point, Result, Task, WindowAppearance, WindowParams, px, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -144,10 +144,6 @@ impl Platform for P { self.keyboard_layout() } - fn keyboard_mapper(&self) -> Rc { - Rc::new(crate::DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/crates/gpui/src/platform/mac/keyboard.rs index 1409731246..a9f6af3edb 100644 --- a/crates/gpui/src/platform/mac/keyboard.rs +++ b/crates/gpui/src/platform/mac/keyboard.rs @@ -1,9 +1,8 @@ -use collections::HashMap; use std::ffi::{CStr, c_void}; use objc::{msg_send, runtime::Object, sel, sel_impl}; -use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; +use crate::PlatformKeyboardLayout; use super::{ TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, @@ -15,10 +14,6 @@ pub(crate) struct MacKeyboardLayout { name: String, } -pub(crate) struct MacKeyboardMapper { - key_equivalents: Option>, -} - impl PlatformKeyboardLayout for MacKeyboardLayout { fn id(&self) -> &str { &self.id @@ -29,27 +24,6 @@ impl PlatformKeyboardLayout for MacKeyboardLayout { } } -impl PlatformKeyboardMapper for MacKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { - if keystroke.key.chars().count() == 1 - && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) - { - keystroke.key = key.to_string(); - } - } - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - self.key_equivalents.as_ref() - } -} - impl MacKeyboardLayout { pub(crate) fn new() -> Self { unsafe { @@ -73,1428 +47,3 @@ impl MacKeyboardLayout { } } } - -impl MacKeyboardMapper { - pub(crate) fn new(layout_id: &str) -> Self { - let key_equivalents = get_key_equivalents(layout_id); - - Self { key_equivalents } - } -} - -// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range -// without using option. This means that some of our built in keyboard shortcuts do not work -// for those users. -// -// The way macOS solves this problem is to move shortcuts around so that they are all reachable, -// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct -// -// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. -// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves -// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position -// as cmd-> on a QWERTY layout. -// -// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö -// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard -// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the -// specific key moves) -// -// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every -// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... -// -// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the -// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: -// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' -// From there I used multi-cursor to produce this match statement. -fn get_key_equivalents(layout_id: &str) -> Option> { - let mappings: &[(char, char)] = match layout_id { - "com.apple.keylayout.ABC-AZERTY" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.ABC-QWERTZ" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Albanian" => &[ - ('"', '\''), - (':', 'Ç'), - (';', 'ç'), - ('<', ';'), - ('>', ':'), - ('@', '"'), - ('\'', '@'), - ('\\', 'ë'), - ('`', '<'), - ('|', 'Ë'), - ('~', '>'), - ], - "com.apple.keylayout.Austrian" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Azeri" => &[ - ('"', 'Ə'), - (',', 'ç'), - ('.', 'ş'), - ('/', '.'), - (':', 'I'), - (';', 'ı'), - ('<', 'Ç'), - ('>', 'Ş'), - ('?', ','), - ('W', 'Ü'), - ('[', 'ö'), - ('\'', 'ə'), - (']', 'ğ'), - ('w', 'ü'), - ('{', 'Ö'), - ('|', '/'), - ('}', 'Ğ'), - ], - "com.apple.keylayout.Belgian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Brazilian-ABNT2" => &[ - ('"', '`'), - ('/', 'ç'), - ('?', 'Ç'), - ('\'', '´'), - ('\\', '~'), - ('^', '¨'), - ('`', '\''), - ('|', '^'), - ('~', '"'), - ], - "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.British" => &[('#', '£')], - "com.apple.keylayout.Canadian-CSA" => &[ - ('"', 'È'), - ('/', 'é'), - ('<', '\''), - ('>', '"'), - ('?', 'É'), - ('[', '^'), - ('\'', 'è'), - ('\\', 'à'), - (']', 'ç'), - ('`', 'ù'), - ('{', '¨'), - ('|', 'À'), - ('}', 'Ç'), - ('~', 'Ù'), - ], - "com.apple.keylayout.Croatian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Croatian-PC" => &[ - ('"', 'Ć'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Czech" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Czech-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Danish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ø'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', '*'), - ('}', 'Ø'), - ('~', '>'), - ], - "com.apple.keylayout.Faroese" => &[ - ('"', 'Ø'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Æ'), - (';', 'æ'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'å'), - ('\'', 'ø'), - ('\\', '\''), - (']', 'ð'), - ('^', '&'), - ('`', '<'), - ('{', 'Å'), - ('|', '*'), - ('}', 'Ð'), - ('~', '>'), - ], - "com.apple.keylayout.Finnish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishExtended" => &[ - ('"', 'ˆ'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.French" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.French-PC" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('-', ')'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '-'), - ('7', 'è'), - ('8', '_'), - ('9', 'ç'), - (':', '§'), - (';', '!'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '*'), - (']', '$'), - ('^', '6'), - ('_', '°'), - ('`', '<'), - ('{', '¨'), - ('|', 'μ'), - ('}', '£'), - ('~', '>'), - ], - "com.apple.keylayout.French-numerical" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.German" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.German-DIN-2137" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], - "com.apple.keylayout.Hungarian" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Hungarian-QWERTY" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Icelandic" => &[ - ('"', 'Ö'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ð'), - (';', 'ð'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', 'ö'), - ('\\', 'þ'), - (']', '´'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', 'Þ'), - ('}', '´'), - ('~', '>'), - ], - "com.apple.keylayout.Irish" => &[('#', '£')], - "com.apple.keylayout.IrishExtended" => &[('#', '£')], - "com.apple.keylayout.Italian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - (',', ';'), - ('.', ':'), - ('/', ','), - ('0', 'é'), - ('1', '&'), - ('2', '"'), - ('3', '\''), - ('4', '('), - ('5', 'ç'), - ('6', 'è'), - ('7', ')'), - ('8', '£'), - ('9', 'à'), - (':', '!'), - (';', 'ò'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', 'ì'), - ('\'', 'ù'), - ('\\', '§'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '^'), - ('|', '°'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Italian-Pro" => &[ - ('"', '^'), - ('#', '£'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'é'), - (';', 'è'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ò'), - ('\'', 'ì'), - ('\\', 'ù'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ç'), - ('|', '§'), - ('}', '°'), - ('~', '>'), - ], - "com.apple.keylayout.LatinAmerican" => &[ - ('"', '¨'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ñ'), - (';', 'ñ'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', '{'), - ('\'', '´'), - ('\\', '¿'), - (']', '}'), - ('^', '&'), - ('`', '<'), - ('{', '['), - ('|', '¡'), - ('}', ']'), - ('~', '>'), - ], - "com.apple.keylayout.Lithuanian" => &[ - ('!', 'Ą'), - ('#', 'Ę'), - ('$', 'Ė'), - ('%', 'Į'), - ('&', 'Ų'), - ('*', 'Ū'), - ('+', 'Ž'), - ('1', 'ą'), - ('2', 'č'), - ('3', 'ę'), - ('4', 'ė'), - ('5', 'į'), - ('6', 'š'), - ('7', 'ų'), - ('8', 'ū'), - ('=', 'ž'), - ('@', 'Č'), - ('^', 'Š'), - ], - "com.apple.keylayout.Maltese" => &[ - ('#', '£'), - ('[', 'ġ'), - (']', 'ħ'), - ('`', 'ż'), - ('{', 'Ġ'), - ('}', 'Ħ'), - ('~', 'Ż'), - ], - "com.apple.keylayout.NorthernSami" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Norwegian" => &[ - ('"', '^'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianExtended" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\\', '@'), - (']', 'æ'), - ('`', '<'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.Polish" => &[ - ('!', '§'), - ('"', 'ę'), - ('#', '!'), - ('$', '?'), - ('%', '+'), - ('&', ':'), - ('(', '/'), - (')', '"'), - ('*', '_'), - ('+', ']'), - (',', '.'), - ('.', ','), - ('/', 'ż'), - (':', 'Ł'), - (';', 'ł'), - ('<', 'ś'), - ('=', '['), - ('>', 'ń'), - ('?', 'Ż'), - ('@', '%'), - ('[', 'ó'), - ('\'', 'ą'), - ('\\', ';'), - (']', '('), - ('^', '='), - ('_', 'ć'), - ('`', '<'), - ('{', 'ź'), - ('|', '$'), - ('}', ')'), - ('~', '>'), - ], - "com.apple.keylayout.Portuguese" => &[ - ('"', '`'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'ª'), - (';', 'º'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ç'), - ('\'', '´'), - (']', '~'), - ('^', '&'), - ('`', '<'), - ('{', 'Ç'), - ('}', '^'), - ('~', '>'), - ], - "com.apple.keylayout.Sami-PC" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Serbian-Latin" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Slovak" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovak-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovenian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish" => &[ - ('!', '¡'), - ('"', '¨'), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '!'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '/'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', ':'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish-ISO" => &[ - ('"', '¨'), - ('#', '·'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '"'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '&'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', '`'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish-Pro" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwedishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissFrench" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'ü'), - (';', 'è'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'é'), - ('\'', '^'), - ('\\', '$'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ö'), - ('|', '£'), - ('}', 'ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissGerman" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'è'), - (';', 'ü'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '^'), - ('\\', '$'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'é'), - ('|', '£'), - ('}', 'à'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish" => &[ - ('"', '-'), - ('#', '"'), - ('$', '\''), - ('%', '('), - ('&', ')'), - ('(', '%'), - (')', ':'), - ('*', '_'), - (',', 'ö'), - ('-', 'ş'), - ('.', 'ç'), - ('/', '.'), - (':', '$'), - ('<', 'Ö'), - ('>', 'Ç'), - ('@', '*'), - ('[', 'ğ'), - ('\'', ','), - ('\\', 'ü'), - (']', 'ı'), - ('^', '/'), - ('_', 'Ş'), - ('`', '<'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-QWERTY-PC" => &[ - ('"', 'I'), - ('#', '^'), - ('$', '+'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', ':'), - (',', 'ö'), - ('.', 'ç'), - ('/', '*'), - (':', 'Ş'), - (';', 'ş'), - ('<', 'Ö'), - ('=', '.'), - ('>', 'Ç'), - ('@', '\''), - ('[', 'ğ'), - ('\'', 'ı'), - ('\\', ','), - (']', 'ü'), - ('^', '&'), - ('`', '<'), - ('{', 'Ğ'), - ('|', ';'), - ('}', 'Ü'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-Standard" => &[ - ('"', 'Ş'), - ('#', '^'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (',', '.'), - ('.', ','), - (':', 'Ç'), - (';', 'ç'), - ('<', ':'), - ('=', '*'), - ('>', ';'), - ('@', '"'), - ('[', 'ğ'), - ('\'', 'ş'), - ('\\', 'ü'), - (']', 'ı'), - ('^', '&'), - ('`', 'ö'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', 'Ö'), - ], - "com.apple.keylayout.Turkmen" => &[ - ('C', 'Ç'), - ('Q', 'Ä'), - ('V', 'Ý'), - ('X', 'Ü'), - ('[', 'ň'), - ('\\', 'ş'), - (']', 'ö'), - ('^', '№'), - ('`', 'ž'), - ('c', 'ç'), - ('q', 'ä'), - ('v', 'ý'), - ('x', 'ü'), - ('{', 'Ň'), - ('|', 'Ş'), - ('}', 'Ö'), - ('~', 'Ž'), - ], - "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.Welsh" => &[('#', '£')], - - _ => return None, - }; - - Some(HashMap::from_iter(mappings.iter().cloned())) -} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 30453def00..832550dc46 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{ - BoolExt, MacKeyboardLayout, MacKeyboardMapper, + BoolExt, MacKeyboardLayout, attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, renderer, @@ -8,9 +8,8 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, - hash, + PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, + SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState { finish_launching: Option>, dock_menu: Option, menus: Option>, - keyboard_mapper: Rc, } impl Default for MacPlatform { @@ -191,9 +189,6 @@ impl MacPlatform { #[cfg(not(feature = "font-kit"))] let text_system = Arc::new(crate::NoopTextSystem::new()); - let keyboard_layout = MacKeyboardLayout::new(); - let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - Self(Mutex::new(MacPlatformState { headless, text_system, @@ -214,7 +209,6 @@ impl MacPlatform { dock_menu: None, on_keyboard_layout_change: None, menus: None, - keyboard_mapper, })) } @@ -354,19 +348,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.display_modifiers.platform, + keystroke.modifiers.platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.display_modifiers.control, + keystroke.modifiers.control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.display_modifiers.alt, + keystroke.modifiers.alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.display_modifiers.shift, + keystroke.modifiers.shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -379,7 +373,7 @@ impl MacPlatform { .initWithTitle_action_keyEquivalent_( ns_string(name), selector, - ns_string(key_to_native(&keystroke.display_key).as_ref()), + ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { @@ -888,10 +882,6 @@ impl Platform for MacPlatform { Box::new(MacKeyboardLayout::new()) } - fn keyboard_mapper(&self) -> Rc { - self.0.lock().keyboard_mapper.clone() - } - fn app_path(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -1403,8 +1393,6 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; let mut lock = platform.0.lock(); - let keyboard_layout = MacKeyboardLayout::new(); - lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); if let Some(mut callback) = lock.on_keyboard_layout_change.take() { drop(lock); callback(); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 15b909199f..00afcd81b5 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,9 +1,8 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, - ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, - TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -238,10 +237,6 @@ impl Platform for TestPlatform { Box::new(TestKeyboardLayout) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, _: Box) {} fn run(&self, _on_finish_launching: Box) { diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 0eb97fbb0c..371feb70c2 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -1,31 +1,22 @@ use anyhow::Result; -use collections::HashMap; use windows::Win32::UI::{ Input::KeyboardAndMouse::{ - GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, - VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, - VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, - VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, + GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0, + VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU, + VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102, + VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, }, WindowsAndMessaging::KL_NAMELENGTH, }; use windows_core::HSTRING; -use crate::{ - KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, -}; +use crate::{Modifiers, PlatformKeyboardLayout}; pub(crate) struct WindowsKeyboardLayout { id: String, name: String, } -pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, -} - impl PlatformKeyboardLayout for WindowsKeyboardLayout { fn id(&self) -> &str { &self.id @@ -36,65 +27,6 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout { } } -impl PlatformKeyboardMapper for WindowsKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) - else { - return KeybindingKeystroke::from_keystroke(keystroke); - }; - if shifted_key && keystroke.modifiers.shift { - log::warn!( - "Keystroke '{}' has both shift and a shifted key, this is likely a bug", - keystroke.key - ); - } - - let shift = shifted_key || keystroke.modifiers.shift; - keystroke.modifiers.shift = false; - - let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { - log::error!( - "Failed to map key equivalent '{:?}' to a valid key", - keystroke - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - - keystroke.key = if shift { - let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { - log::error!( - "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", - keystroke, - vkey - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - shifted_key - } else { - key.clone() - }; - - let modifiers = Modifiers { - shift, - ..keystroke.modifiers - }; - - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} - impl WindowsKeyboardLayout { pub(crate) fn new() -> Result { let mut buffer = [0u16; KL_NAMELENGTH as usize]; @@ -116,41 +48,6 @@ impl WindowsKeyboardLayout { } } -impl WindowsKeyboardMapper { - pub(crate) fn new() -> Self { - let mut key_to_vkey = HashMap::default(); - let mut vkey_to_key = HashMap::default(); - let mut vkey_to_shifted = HashMap::default(); - for vkey in CANDIDATE_VKEYS { - if let Some(key) = get_key_from_vkey(*vkey) { - key_to_vkey.insert(key.clone(), (vkey.0, false)); - vkey_to_key.insert(vkey.0, key); - } - let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; - if scan_code == 0 { - continue; - } - if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { - key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); - vkey_to_shifted.insert(vkey.0, shifted_key); - } - } - Self { - key_to_vkey, - vkey_to_key, - vkey_to_shifted, - } - } - - fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { - if use_key_equivalents { - get_vkey_from_key_with_us_layout(key) - } else { - self.key_to_vkey.get(key).cloned() - } - } -} - pub(crate) fn get_keystroke_key( vkey: VIRTUAL_KEY, scan_code: u32, @@ -243,134 +140,3 @@ pub(crate) fn generate_key_char( _ => None, } } - -fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { - match key { - // ` => VK_OEM_3 - "`" => Some((VK_OEM_3.0, false)), - "~" => Some((VK_OEM_3.0, true)), - "1" => Some((VK_1.0, false)), - "!" => Some((VK_1.0, true)), - "2" => Some((VK_2.0, false)), - "@" => Some((VK_2.0, true)), - "3" => Some((VK_3.0, false)), - "#" => Some((VK_3.0, true)), - "4" => Some((VK_4.0, false)), - "$" => Some((VK_4.0, true)), - "5" => Some((VK_5.0, false)), - "%" => Some((VK_5.0, true)), - "6" => Some((VK_6.0, false)), - "^" => Some((VK_6.0, true)), - "7" => Some((VK_7.0, false)), - "&" => Some((VK_7.0, true)), - "8" => Some((VK_8.0, false)), - "*" => Some((VK_8.0, true)), - "9" => Some((VK_9.0, false)), - "(" => Some((VK_9.0, true)), - "0" => Some((VK_0.0, false)), - ")" => Some((VK_0.0, true)), - "-" => Some((VK_OEM_MINUS.0, false)), - "_" => Some((VK_OEM_MINUS.0, true)), - "=" => Some((VK_OEM_PLUS.0, false)), - "+" => Some((VK_OEM_PLUS.0, true)), - "[" => Some((VK_OEM_4.0, false)), - "{" => Some((VK_OEM_4.0, true)), - "]" => Some((VK_OEM_6.0, false)), - "}" => Some((VK_OEM_6.0, true)), - "\\" => Some((VK_OEM_5.0, false)), - "|" => Some((VK_OEM_5.0, true)), - ";" => Some((VK_OEM_1.0, false)), - ":" => Some((VK_OEM_1.0, true)), - "'" => Some((VK_OEM_7.0, false)), - "\"" => Some((VK_OEM_7.0, true)), - "," => Some((VK_OEM_COMMA.0, false)), - "<" => Some((VK_OEM_COMMA.0, true)), - "." => Some((VK_OEM_PERIOD.0, false)), - ">" => Some((VK_OEM_PERIOD.0, true)), - "/" => Some((VK_OEM_2.0, false)), - "?" => Some((VK_OEM_2.0, true)), - _ => None, - } -} - -const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ - VK_OEM_3, - VK_OEM_MINUS, - VK_OEM_PLUS, - VK_OEM_4, - VK_OEM_5, - VK_OEM_6, - VK_OEM_1, - VK_OEM_7, - VK_OEM_COMMA, - VK_OEM_PERIOD, - VK_OEM_2, - VK_OEM_102, - VK_OEM_8, - VK_ABNT_C1, - VK_0, - VK_1, - VK_2, - VK_3, - VK_4, - VK_5, - VK_6, - VK_7, - VK_8, - VK_9, -]; - -#[cfg(test)] -mod tests { - use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper}; - - #[test] - fn test_keyboard_mapper() { - let mapper = WindowsKeyboardMapper::new(); - - // Normal case - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "a".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "a"); - assert_eq!(mapped.display_modifiers, Modifiers::control()); - - // Shifted case, ctrl-$ - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Shifted case, but shift is true - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Windows style - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "4".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.inner.key, "$"); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - } -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5ac2be2f23..6202e05fb3 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -351,10 +351,6 @@ impl Platform for WindowsPlatform { ) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); } diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 5b69ce7fa6..e5cff552ad 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -153,7 +153,7 @@ pub struct Style { #[refineable] pub overflow: Point, /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes. - pub scrollbar_width: f32, + pub scrollbar_width: AbsoluteLength, /// Whether both x and y axis should be scrollable at the same time. pub allow_concurrent_scroll: bool, /// Whether scrolling should be restricted to the axis indicated by the mouse wheel. @@ -745,7 +745,7 @@ impl Default for Style { }, allow_concurrent_scroll: false, restrict_scroll_to_axis: false, - scrollbar_width: 0.0, + scrollbar_width: AbsoluteLength::default(), position: Position::Relative, inset: Edges::auto(), margin: Edges::::zero(), diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 58386ad1f5..1c67410043 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -277,7 +277,7 @@ impl ToTaffy for Style { taffy::style::Style { display: self.display.into(), overflow: self.overflow.into(), - scrollbar_width: self.scrollbar_width, + scrollbar_width: self.scrollbar_width.to_taffy(rem_size), position: self.position.into(), inset: self.inset.to_taffy(rem_size), size: self.size.to_taffy(rem_size), @@ -314,6 +314,15 @@ impl ToTaffy for Style { } } +impl ToTaffy for AbsoluteLength { + fn to_taffy(&self, rem_size: Pixels) -> f32 { + match self { + AbsoluteLength::Pixels(pixels) => pixels.into(), + AbsoluteLength::Rems(rems) => (*rems * rem_size).into(), + } + } +} + impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { match self { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0791dcc621..0aa4cf804c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2504,7 +2504,7 @@ impl Window { &mut self, key: impl Into, cx: &mut App, - init: impl FnOnce(&mut Self, &mut App) -> S, + init: impl FnOnce(&mut Self, &mut Context) -> S, ) -> Entity { let current_view = self.current_view(); self.with_global_id(key.into(), |global_id, window| { @@ -2537,7 +2537,7 @@ impl Window { pub fn use_state( &mut self, cx: &mut App, - init: impl FnOnce(&mut Self, &mut App) -> S, + init: impl FnOnce(&mut Self, &mut Context) -> S, ) -> Entity { self.use_keyed_state( ElementId::CodeLocation(*core::panic::Location::caller()), @@ -4838,6 +4838,12 @@ impl> From<(ElementId, T)> for ElementId { } } +impl From<&'static core::panic::Location<'static>> for ElementId { + fn from(location: &'static core::panic::Location<'static>) -> Self { + ElementId::CodeLocation(*location) + } +} + /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`Window::paint_quad`]. #[derive(Clone)] diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index f7363395ae..4fc6039fd7 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -215,7 +215,6 @@ pub enum IconName { Tab, Terminal, TerminalAlt, - TerminalGhost, TextSnippet, TextThread, Thread, diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 2dca57424b..b96557b391 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,19 +401,12 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - pub struct ImageViewerDb(ThreadSafeConnection); - - impl Domain for ImageViewerDb { - const NAME: &str = stringify!(ImageViewerDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref IMAGE_VIEWER: ImageViewerDb = + &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -424,11 +417,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]); - impl ImageViewerDb { query! { pub async fn save_image_path( diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index b06a475f93..ebfd37d16c 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -4,16 +4,12 @@ use crate::{ LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice, }; -use anyhow::anyhow; use futures::{FutureExt, channel::mpsc, future::BoxFuture, stream::BoxStream}; use gpui::{AnyView, App, AsyncApp, Entity, Task, Window}; use http_client::Result; use parking_lot::Mutex; use smol::stream::StreamExt; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, -}; +use std::sync::Arc; #[derive(Clone)] pub struct FakeLanguageModelProvider { @@ -110,7 +106,6 @@ pub struct FakeLanguageModel { >, )>, >, - forbid_requests: AtomicBool, } impl Default for FakeLanguageModel { @@ -119,20 +114,11 @@ impl Default for FakeLanguageModel { provider_id: LanguageModelProviderId::from("fake".to_string()), provider_name: LanguageModelProviderName::from("Fake".to_string()), current_completion_txs: Mutex::new(Vec::new()), - forbid_requests: AtomicBool::new(false), } } } impl FakeLanguageModel { - pub fn allow_requests(&self) { - self.forbid_requests.store(false, SeqCst); - } - - pub fn forbid_requests(&self) { - self.forbid_requests.store(true, SeqCst); - } - pub fn pending_completions(&self) -> Vec { self.current_completion_txs .lock() @@ -265,18 +251,9 @@ impl LanguageModel for FakeLanguageModel { LanguageModelCompletionError, >, > { - if self.forbid_requests.load(SeqCst) { - async move { - Err(LanguageModelCompletionError::Other(anyhow!( - "requests are forbidden" - ))) - } - .boxed() - } else { - let (tx, rx) = mpsc::unbounded(); - self.current_completion_txs.lock().push((request, tx)); - async move { Ok(rx.boxed()) }.boxed() - } + let (tx, rx) = mpsc::unbounded(); + self.current_completion_txs.lock().push((request, tx)); + async move { Ok(rx.boxed()) }.boxed() } fn as_fake(&self) -> &Self { diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 531c3615dc..c7693a64c7 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -6,7 +6,6 @@ use collections::BTreeMap; use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*}; use std::{str::FromStr, sync::Arc}; use thiserror::Error; -use util::maybe; pub fn init(cx: &mut App) { let registry = cx.new(|_cx| LanguageModelRegistry::default()); @@ -42,7 +41,9 @@ impl std::fmt::Debug for ConfigurationError { #[derive(Default)] pub struct LanguageModelRegistry { default_model: Option, - default_fast_model: Option, + /// This model is automatically configured by a user's environment after + /// authenticating all providers. It's only used when default_model is not available. + environment_fallback_model: Option, inline_assistant_model: Option, commit_message_model: Option, thread_summary_model: Option, @@ -98,9 +99,6 @@ impl ConfiguredModel { pub enum Event { DefaultModelChanged, - InlineAssistantModelChanged, - CommitMessageModelChanged, - ThreadSummaryModelChanged, ProviderStateChanged(LanguageModelProviderId), AddedProvider(LanguageModelProviderId), RemovedProvider(LanguageModelProviderId), @@ -226,7 +224,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_inline_assistant_model(configured_model, cx); + self.set_inline_assistant_model(configured_model); } pub fn select_commit_message_model( @@ -235,7 +233,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_commit_message_model(configured_model, cx); + self.set_commit_message_model(configured_model); } pub fn select_thread_summary_model( @@ -244,7 +242,7 @@ impl LanguageModelRegistry { cx: &mut Context, ) { let configured_model = model.and_then(|model| self.select_model(model, cx)); - self.set_thread_summary_model(configured_model, cx); + self.set_thread_summary_model(configured_model); } /// Selects and sets the inline alternatives for language models based on @@ -278,68 +276,60 @@ impl LanguageModelRegistry { } pub fn set_default_model(&mut self, model: Option, cx: &mut Context) { - match (self.default_model.as_ref(), model.as_ref()) { + match (self.default_model(), model.as_ref()) { (Some(old), Some(new)) if old.is_same_as(new) => {} (None, None) => {} _ => cx.emit(Event::DefaultModelChanged), } - self.default_fast_model = maybe!({ - let provider = &model.as_ref()?.provider; - let fast_model = provider.default_fast_model(cx)?; - Some(ConfiguredModel { - provider: provider.clone(), - model: fast_model, - }) - }); self.default_model = model; } - pub fn set_inline_assistant_model( + pub fn set_environment_fallback_model( &mut self, model: Option, cx: &mut Context, ) { - match (self.inline_assistant_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::InlineAssistantModelChanged), + if self.default_model.is_none() { + match (self.environment_fallback_model.as_ref(), model.as_ref()) { + (Some(old), Some(new)) if old.is_same_as(new) => {} + (None, None) => {} + _ => cx.emit(Event::DefaultModelChanged), + } } + self.environment_fallback_model = model; + } + + pub fn set_inline_assistant_model(&mut self, model: Option) { self.inline_assistant_model = model; } - pub fn set_commit_message_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.commit_message_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::CommitMessageModelChanged), - } + pub fn set_commit_message_model(&mut self, model: Option) { self.commit_message_model = model; } - pub fn set_thread_summary_model( - &mut self, - model: Option, - cx: &mut Context, - ) { - match (self.thread_summary_model.as_ref(), model.as_ref()) { - (Some(old), Some(new)) if old.is_same_as(new) => {} - (None, None) => {} - _ => cx.emit(Event::ThreadSummaryModelChanged), - } + pub fn set_thread_summary_model(&mut self, model: Option) { self.thread_summary_model = model; } + #[track_caller] pub fn default_model(&self) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; } - self.default_model.clone() + self.default_model + .clone() + .or_else(|| self.environment_fallback_model.clone()) + } + + pub fn default_fast_model(&self, cx: &App) -> Option { + let provider = self.default_model()?.provider; + let fast_model = provider.default_fast_model(cx)?; + Some(ConfiguredModel { + provider, + model: fast_model, + }) } pub fn inline_assistant_model(&self) -> Option { @@ -353,7 +343,7 @@ impl LanguageModelRegistry { .or_else(|| self.default_model.clone()) } - pub fn commit_message_model(&self) -> Option { + pub fn commit_message_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -361,11 +351,11 @@ impl LanguageModelRegistry { self.commit_message_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } - pub fn thread_summary_model(&self) -> Option { + pub fn thread_summary_model(&self, cx: &App) -> Option { #[cfg(debug_assertions)] if std::env::var("ZED_SIMULATE_NO_LLM_PROVIDER").is_ok() { return None; @@ -373,7 +363,7 @@ impl LanguageModelRegistry { self.thread_summary_model .clone() - .or_else(|| self.default_fast_model.clone()) + .or_else(|| self.default_fast_model(cx)) .or_else(|| self.default_model.clone()) } @@ -410,4 +400,34 @@ mod tests { let providers = registry.read(cx).providers(); assert!(providers.is_empty()); } + + #[gpui::test] + async fn test_configure_environment_fallback_model(cx: &mut gpui::TestAppContext) { + let registry = cx.new(|_| LanguageModelRegistry::default()); + + let provider = FakeLanguageModelProvider::default(); + registry.update(cx, |registry, cx| { + registry.register_provider(provider.clone(), cx); + }); + + cx.update(|cx| provider.authenticate(cx)).await.unwrap(); + + registry.update(cx, |registry, cx| { + let provider = registry.provider(&provider.id()).unwrap(); + + registry.set_environment_fallback_model( + Some(ConfiguredModel { + provider: provider.clone(), + model: provider.default_model(cx).unwrap(), + }), + cx, + ); + + let default_model = registry.default_model().unwrap(); + let fallback_model = registry.environment_fallback_model.clone().unwrap(); + + assert_eq!(default_model.model.id(), fallback_model.model.id()); + assert_eq!(default_model.provider.id(), fallback_model.provider.id()); + }); + } } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index b5bfb870f6..cd41478668 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -44,6 +44,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true +project.workspace = true release_channel.workspace = true schemars.workspace = true serde.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 738b72b0c9..beed306e74 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -3,8 +3,12 @@ use std::sync::Arc; use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; use collections::HashSet; -use gpui::{App, Context, Entity}; -use language_model::{LanguageModelProviderId, LanguageModelRegistry}; +use futures::future; +use gpui::{App, AppContext as _, Context, Entity}; +use language_model::{ + AuthenticateError, ConfiguredModel, LanguageModelProviderId, LanguageModelRegistry, +}; +use project::DisableAiSettings; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -13,7 +17,7 @@ pub mod ui; use crate::provider::anthropic::AnthropicLanguageModelProvider; use crate::provider::bedrock::BedrockLanguageModelProvider; -use crate::provider::cloud::CloudLanguageModelProvider; +use crate::provider::cloud::{self, CloudLanguageModelProvider}; use crate::provider::copilot_chat::CopilotChatLanguageModelProvider; use crate::provider::google::GoogleLanguageModelProvider; use crate::provider::lmstudio::LmStudioLanguageModelProvider; @@ -48,6 +52,13 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { cx, ); }); + + let mut already_authenticated = false; + if !DisableAiSettings::get_global(cx).disable_ai { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; + } + cx.observe_global::(move |cx| { let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) .openai_compatible @@ -65,6 +76,12 @@ pub fn init(user_store: Entity, client: Arc, cx: &mut App) { ); }); openai_compatible_providers = openai_compatible_providers_new; + already_authenticated = false; + } + + if !DisableAiSettings::get_global(cx).disable_ai && !already_authenticated { + authenticate_all_providers(registry.clone(), cx); + already_authenticated = true; } }) .detach(); @@ -151,3 +168,83 @@ fn register_language_model_providers( registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } + +/// Authenticates all providers in the [`LanguageModelRegistry`]. +/// +/// We do this so that we can populate the language selector with all of the +/// models from the configured providers. +/// +/// This function won't do anything if AI is disabled. +fn authenticate_all_providers(registry: Entity, cx: &mut App) { + let providers_to_authenticate = registry + .read(cx) + .providers() + .iter() + .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) + .collect::>(); + + let mut tasks = Vec::with_capacity(providers_to_authenticate.len()); + + for (provider_id, provider_name, authenticate_task) in providers_to_authenticate { + tasks.push(cx.background_spawn(async move { + if let Err(err) = authenticate_task.await { + if matches!(err, AuthenticateError::CredentialsNotFound) { + // Since we're authenticating these providers in the + // background for the purposes of populating the + // language selector, we don't care about providers + // where the credentials are not found. + } else { + // Some providers have noisy failure states that we + // don't want to spam the logs with every time the + // language model selector is initialized. + // + // Ideally these should have more clear failure modes + // that we know are safe to ignore here, like what we do + // with `CredentialsNotFound` above. + match provider_id.0.as_ref() { + "lmstudio" | "ollama" => { + // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". + // + // These fail noisily, so we don't log them. + } + "copilot_chat" => { + // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. + } + _ => { + log::error!( + "Failed to authenticate provider: {}: {err}", + provider_name.0 + ); + } + } + } + } + })); + } + + let all_authenticated_future = future::join_all(tasks); + + cx.spawn(async move |cx| { + all_authenticated_future.await; + + registry + .update(cx, |registry, cx| { + let cloud_provider = registry.provider(&cloud::PROVIDER_ID); + let fallback_model = cloud_provider + .iter() + .chain(registry.providers().iter()) + .find(|provider| provider.is_authenticated(cx)) + .and_then(|provider| { + Some(ConfiguredModel { + provider: provider.clone(), + model: provider + .default_model(cx) + .or_else(|| provider.recommended_models(cx).first().cloned())?, + }) + }); + registry.set_environment_fallback_model(fallback_model, cx); + }) + .ok(); + }) + .detach(); +} diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index b473d06357..fb6e2fb1e4 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -44,8 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i use crate::provider::google::{GoogleEventMapper, into_google}; use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai}; -const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; -const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; +pub const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID; +pub const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME; #[derive(Default, Clone, Debug, PartialEq)] pub struct ZedDotDevSettings { @@ -146,7 +146,7 @@ impl State { default_fast_model: None, recommended_models: Vec::new(), _fetch_models_task: cx.spawn(async move |this, cx| { - maybe!(async move { + maybe!(async { let (client, llm_api_token) = this .read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?; diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 4140713544..057259d114 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -4,6 +4,7 @@ use gpui::{ }; use itertools::Itertools; use serde_json::json; +use settings::get_key_equivalents; use ui::{Button, ButtonStyle}; use ui::{ ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon, @@ -168,8 +169,7 @@ impl Item for KeyContextView { impl Render for KeyContextView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; - - let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); + let key_equivalents = get_key_equivalents(cx.keyboard_layout().id()); v_flex() .id("key-context-view") .overflow_scroll() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f26..43c0365291 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1743,5 +1743,6 @@ pub enum Event { } impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 1f607a033a..f16da45d79 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -1323,7 +1323,7 @@ fn render_copy_code_block_button( .icon_size(IconSize::Small) .style(ButtonStyle::Filled) .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Copy")) + .tooltip(Tooltip::text("Copy Code")) .on_click({ let markdown = markdown; move |_event, _window, cx| { diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201..884374a72f 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct OnboardingPagesDb(ThreadSafeConnection); - - impl Domain for OnboardingPagesDb { - const NAME: &str = stringify!(OnboardingPagesDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref ONBOARDING_PAGES: OnboardingPagesDb = + &[ + sql!( CREATE TABLE onboarding_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -872,11 +866,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + ), + ]; } - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - impl OnboardingPagesDb { query! { pub async fn save_onboarding_page( diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 8ff55d812b..3fe9c32a48 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -414,19 +414,13 @@ impl workspace::SerializableItem for WelcomePage { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct WelcomePagesDb(ThreadSafeConnection); - - impl Domain for WelcomePagesDb { - const NAME: &str = stringify!(WelcomePagesDb); - - const MIGRATIONS: &[&str] = (&[sql!( + define_connection! { + pub static ref WELCOME_PAGES: WelcomePagesDb = + &[ + sql!( CREATE TABLE welcome_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -436,11 +430,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]); + ), + ]; } - db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); - impl WelcomePagesDb { query! { pub async fn save_welcome_page( diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 10698cead8..d920b935b3 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4,11 +4,11 @@ use anyhow::Context as _; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use db::kvp::KEY_VALUE_STORE; use editor::{ - AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId, - ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, ShowScrollbar, + AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange, + MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, display_map::ToDisplayPoint, items::{entry_git_aware_label_color, entry_label_color}, - scroll::{Autoscroll, ScrollAnchor, ScrollbarAutoHide}, + scroll::{Autoscroll, ScrollAnchor}, }; use file_icons::FileIcons; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; @@ -45,19 +45,18 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use smol::channel; use theme::{SyntaxTheme, ThemeSettings}; -use ui::{DynamicSpacing, IndentGuideColors, IndentGuideLayout}; +use ui::{ + ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, DynamicSpacing, FluentBuilder, + HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, IndentGuideColors, + IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt, + StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex, +}; use util::{RangeExt, ResultExt, TryFutureExt, debug_panic}; use workspace::{ OpenInTerminal, WeakItemHandle, Workspace, dock::{DockPosition, Panel, PanelEvent}, item::ItemHandle, searchable::{SearchEvent, SearchableItem}, - ui::{ - ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder, HighlightedLabel, - Icon, IconButton, IconButtonShape, IconName, IconSize, Label, LabelCommon, ListItem, - Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable, Tooltip, h_flex, - v_flex, - }, }; use worktree::{Entry, ProjectEntryId, WorktreeId}; @@ -125,10 +124,6 @@ pub struct OutlinePanel { cached_entries: Vec, filter_editor: Entity, mode: ItemsDisplayMode, - show_scrollbar: bool, - vertical_scrollbar_state: ScrollbarState, - horizontal_scrollbar_state: ScrollbarState, - hide_scrollbar_task: Option>, max_width_item_index: Option, preserve_selection_on_buffer_fold_toggles: HashSet, pending_default_expansion_depth: Option, @@ -752,10 +747,6 @@ impl OutlinePanel { let focus_handle = cx.focus_handle(); let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in); - let focus_out_subscription = - cx.on_focus_out(&focus_handle, window, |outline_panel, _, window, cx| { - outline_panel.hide_scrollbar(window, cx); - }); let workspace_subscription = cx.subscribe_in( &workspace .weak_handle() @@ -868,12 +859,6 @@ impl OutlinePanel { workspace: workspace_handle, project, fs: workspace.app_state().fs.clone(), - show_scrollbar: !Self::should_autohide_scrollbar(cx), - hide_scrollbar_task: None, - vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), - horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), max_width_item_index: None, scroll_handle, focus_handle, @@ -903,7 +888,6 @@ impl OutlinePanel { settings_subscription, icons_subscription, focus_subscription, - focus_out_subscription, workspace_subscription, filter_update_subscription, ], @@ -4491,150 +4475,6 @@ impl OutlinePanel { cx.notify(); } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging()) - { - return None; - } - Some( - div() - .occlude() - .id("project-panel-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|outline_panel, _, window, cx| { - if !outline_panel.vertical_scrollbar_state.is_dragging() - && !outline_panel.focus_handle.contains_focused(window, cx) - { - outline_panel.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())), - ) - } - - fn render_horizontal_scrollbar( - &self, - _: &mut Window, - cx: &mut Context, - ) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging()) - { - return None; - } - Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| { - div() - .occlude() - .id("project-panel-horizontal-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|outline_panel, _, window, cx| { - if !outline_panel.horizontal_scrollbar_state.is_dragging() - && !outline_panel.focus_handle.contains_focused(window, cx) - { - outline_panel.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .w_full() - .absolute() - .right_1() - .left_1() - .bottom_0() - .h(px(12.)) - .cursor_default() - .child(scrollbar) - }) - } - - fn should_show_scrollbar(cx: &App) -> bool { - let show = OutlinePanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => true, - ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - } - } - - fn should_autohide_scrollbar(cx: &App) -> bool { - let show = OutlinePanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => true, - } - } - - fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - if !Self::should_autohide_scrollbar(cx) { - return; - } - self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 { let item_text_chars = match entry { PanelEntry::Fs(FsEntry::ExternalFile(external)) => self @@ -4690,7 +4530,7 @@ impl OutlinePanel { indent_size: f32, window: &mut Window, cx: &mut Context, - ) -> Div { + ) -> impl IntoElement { let contents = if self.cached_entries.is_empty() { let header = if self.updating_fs_entries || self.updating_cached_entries { None @@ -4844,17 +4684,20 @@ impl OutlinePanel { }), ) }) + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal) + .notify_content(), + window, + cx, + ) }; v_flex() .flex_shrink() .size_full() .child(list_contents.size_full().flex_shrink()) - .children(self.render_vertical_scrollbar(cx)) - .when_some( - self.render_horizontal_scrollbar(window, cx), - |this, scrollbar| this.pb_4().child(scrollbar), - ) } .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( @@ -5121,15 +4964,6 @@ impl Render for OutlinePanel { .size_full() .overflow_hidden() .relative() - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(window, cx); - } - })) .key_context(self.dispatch_context(window, cx)) .on_action(cx.listener(Self::open_selected_entry)) .on_action(cx.listener(Self::cancel)) diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 133d28b748..255c5f3438 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -1,8 +1,9 @@ -use editor::ShowScrollbar; -use gpui::Pixels; +use editor::EditorSettings; +use gpui::{App, Pixels}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -115,6 +116,14 @@ pub struct OutlinePanelSettingsContent { pub expand_outlines_with_depth: Option, } +impl ScrollbarVisibilitySetting for OutlinePanelSettings { + fn scrollbar_visibility(&self, cx: &App) -> ShowScrollbar { + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl Settings for OutlinePanelSettings { const KEY: Option<&'static str> = Some("outline_panel"); diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 34af5fed02..019e48b355 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -11,17 +11,17 @@ use editor::{ use gpui::{ Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, - ScrollStrategy, Stateful, Task, UniformListScrollHandle, Window, actions, div, list, - prelude::*, uniform_list, + ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*, + uniform_list, }; use head::Head; use schemars::JsonSchema; use serde::Deserialize; use std::{ops::Range, sync::Arc, time::Duration}; use ui::{ - Color, Divider, Label, ListItem, ListItemSpacing, Scrollbar, ScrollbarState, prelude::*, v_flex, + Color, Divider, Label, ListItem, ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar, + prelude::*, v_flex, }; -use util::ResultExt; use workspace::ModalView; enum ElementContainer { @@ -65,13 +65,8 @@ pub struct Picker { width: Option, widest_item: Option, max_height: Option, - focus_handle: FocusHandle, /// An external control to display a scrollbar in the `Picker`. show_scrollbar: bool, - /// An internal state that controls whether to show the scrollbar based on the user's focus. - scrollbar_visibility: bool, - scrollbar_state: ScrollbarState, - hide_scrollbar_task: Option>, /// Whether the `Picker` is rendered as a self-contained modal. /// /// Set this to `false` when rendering the `Picker` as part of a larger modal. @@ -293,13 +288,6 @@ impl Picker { cx: &mut Context, ) -> Self { let element_container = Self::create_element_container(container); - let scrollbar_state = match &element_container { - ElementContainer::UniformList(scroll_handle) => { - ScrollbarState::new(scroll_handle.clone()) - } - ElementContainer::List(state) => ScrollbarState::new(state.clone()), - }; - let focus_handle = cx.focus_handle(); let mut this = Self { delegate, head, @@ -309,12 +297,8 @@ impl Picker { width: None, widest_item: None, max_height: Some(rems(18.).into()), - focus_handle, show_scrollbar: false, - scrollbar_visibility: true, - scrollbar_state, is_modal: true, - hide_scrollbar_task: None, }; this.update_matches("".to_string(), window, cx); // give the delegate 4ms to render the first set of suggestions. @@ -790,67 +774,6 @@ impl Picker { } } } - - fn hide_scrollbar(&mut self, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.scrollbar_visibility = false; - cx.notify(); - }) - .log_err(); - })) - } - - fn render_scrollbar(&self, cx: &mut Context) -> Option> { - if !self.show_scrollbar - || !(self.scrollbar_visibility || self.scrollbar_state.is_dragging()) - { - return None; - } - Some( - div() - .occlude() - .id("picker-scroll") - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_0() - .w(px(12.)) - .cursor_default() - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|picker, _, window, cx| { - if !picker.scrollbar_state.is_dragging() - && !picker.focus_handle.contains_focused(window, cx) - { - picker.hide_scrollbar(cx); - cx.notify(); - } - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } } impl EventEmitter for Picker {} @@ -900,17 +823,12 @@ impl Render for Picker { .overflow_hidden() .children(self.delegate.render_header(window, cx)) .child(self.render_element_container(cx)) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.scrollbar_visibility = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(cx); - } - })) - .when_some(self.render_scrollbar(cx), |div, scrollbar| { - div.child(scrollbar) + .when(self.show_scrollbar, |this| { + this.custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical).width_sm(), + window, + cx, + ) }), ) }) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5a30a3e9bc..7c7a3e2dbe 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -7,12 +7,11 @@ use collections::{BTreeSet, HashMap, hash_map}; use command_palette_hooks::CommandPaletteFilter; use db::kvp::KEY_VALUE_STORE; use editor::{ - Editor, EditorEvent, EditorSettings, ShowScrollbar, + Editor, EditorEvent, items::{ entry_diagnostic_aware_icon_decoration_and_color, entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color, }, - scroll::ScrollbarAutoHide, }; use file_icons::FileIcons; use git::status::GitSummary; @@ -59,7 +58,8 @@ use theme::ThemeSettings; use ui::{ Color, ContextMenu, DecoratedIcon, Divider, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, - ScrollableHandle, Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, + ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*, + v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -109,10 +109,6 @@ pub struct ProjectPanel { workspace: WeakEntity, width: Option, pending_serialization: Task>, - show_scrollbar: bool, - vertical_scrollbar_state: ScrollbarState, - horizontal_scrollbar_state: ScrollbarState, - hide_scrollbar_task: Option>, diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, max_width_item_index: Option, diagnostic_summary_update: Task<()>, @@ -428,7 +424,6 @@ impl ProjectPanel { cx.on_focus(&focus_handle, window, Self::focus_in).detach(); cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { this.focus_out(window, cx); - this.hide_scrollbar(window, cx); }) .detach(); @@ -619,12 +614,6 @@ impl ProjectPanel { workspace: workspace.weak_handle(), width: None, pending_serialization: Task::ready(None), - show_scrollbar: !Self::should_autohide_scrollbar(cx), - hide_scrollbar_task: None, - vertical_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), - horizontal_scrollbar_state: ScrollbarState::new(scroll_handle.clone()) - .parent_entity(&cx.entity()), max_width_item_index: None, diagnostics: Default::default(), diagnostic_summary_update: Task::ready(()), @@ -4089,7 +4078,6 @@ impl ProjectPanel { .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) - .when(settings.drag_and_drop, |this| this .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, _, cx| { let is_current_target = this.drag_target_entry.as_ref() @@ -4223,7 +4211,7 @@ impl ProjectPanel { } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), - )) + ) }) .on_mouse_down( MouseButton::Left, @@ -4434,7 +4422,6 @@ impl ProjectPanel { div() .when(!is_sticky, |div| { div - .when(settings.drag_and_drop, |div| div .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.drag_target_entry = None; @@ -4466,7 +4453,7 @@ impl ProjectPanel { } }, - ))) + )) }) .child( Label::new(DELIMITER.clone()) @@ -4486,7 +4473,6 @@ impl ProjectPanel { .when(index != components_len - 1, |div|{ let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); div - .when(settings.drag_and_drop, |div| div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { if event.bounds.contains(&event.event.position) { @@ -4524,7 +4510,7 @@ impl ProjectPanel { target.index == index ), |this| { this.bg(item_colors.drag_over) - })) + }) }) }) .on_click(cx.listener(move |this, _, _, cx| { @@ -4710,103 +4696,6 @@ impl ProjectPanel { } } - fn render_vertical_scrollbar(&self, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging()) - { - return None; - } - Some( - div() - .occlude() - .id("project-panel-vertical-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.vertical_scrollbar_state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .h_full() - .absolute() - .right_1() - .top_1() - .bottom_1() - .w(px(12.)) - .cursor_default() - .children(Scrollbar::vertical( - // percentage as f32..end_offset as f32, - self.vertical_scrollbar_state.clone(), - )), - ) - } - - fn render_horizontal_scrollbar(&self, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging()) - { - return None; - } - Scrollbar::horizontal(self.horizontal_scrollbar_state.clone()).map(|scrollbar| { - div() - .occlude() - .id("project-panel-horizontal-scroll") - .on_mouse_move(cx.listener(|_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - if !this.horizontal_scrollbar_state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.hide_scrollbar(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _, cx| { - cx.notify(); - })) - .w_full() - .absolute() - .right_1() - .left_1() - .bottom_1() - .h(px(12.)) - .cursor_default() - .child(scrollbar) - }) - } - fn dispatch_context(&self, window: &Window, cx: &Context) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("ProjectPanel"); @@ -4822,52 +4711,6 @@ impl ProjectPanel { dispatch_context } - fn should_show_scrollbar(cx: &App) -> bool { - let show = ProjectPanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => true, - ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - } - } - - fn should_autohide_scrollbar(cx: &App) -> bool { - let show = ProjectPanelSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show); - match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => true, - } - } - - fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - if !Self::should_autohide_scrollbar(cx) { - return; - } - self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - fn reveal_entry( &mut self, project: Entity, @@ -5032,8 +4875,7 @@ impl ProjectPanel { sticky_parents.reverse(); - let panel_settings = ProjectPanelSettings::get_global(cx); - let git_status_enabled = panel_settings.git_status; + let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { @@ -5117,11 +4959,11 @@ impl Render for ProjectPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_worktree = !self.visible_entries.is_empty(); let project = self.project.read(cx); - let panel_settings = ProjectPanelSettings::get_global(cx); - let indent_size = panel_settings.indent_size; - let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; + let indent_size = ProjectPanelSettings::get_global(cx).indent_size; + let show_indent_guides = + ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; let show_sticky_entries = { - if panel_settings.sticky_scroll { + if ProjectPanelSettings::get_global(cx).sticky_scroll { let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrolled = self.scroll_handle.offset().y < px(0.); is_scrollable && is_scrolled @@ -5209,10 +5051,8 @@ impl Render for ProjectPanel { h_flex() .id("project-panel") .group("project-panel") - .when(panel_settings.drag_and_drop, |this| { - this.on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) - }) + .on_drag_move(cx.listener(handle_drag_move::)) + .on_drag_move(cx.listener(handle_drag_move::)) .size_full() .relative() .on_modifiers_changed(cx.listener( @@ -5220,15 +5060,6 @@ impl Render for ProjectPanel { this.refresh_drag_cursor_style(&event.modifiers, window, cx); }, )) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(window, cx); - } - })) .on_click(cx.listener(|this, event, _, cx| { if matches!(event, gpui::ClickEvent::Keyboard(_)) { return; @@ -5489,10 +5320,14 @@ impl Render for ProjectPanel { .with_width_from_item(self.max_width_item_index) .track_scroll(self.scroll_handle.clone()), ) - .children(self.render_vertical_scrollbar(cx)) - .when_some(self.render_horizontal_scrollbar(cx), |this, scrollbar| { - this.pb_4().child(scrollbar) - }) + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(self.scroll_handle.clone()) + .with_track_along(ScrollAxes::Horizontal) + .notify_content(), + window, + cx, + ) .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( anchored() @@ -5550,32 +5385,30 @@ impl Render for ProjectPanel { })), ) .when(is_local, |div| { - div.when(panel_settings.drag_and_drop, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) + div.drag_over::(|style, _, _, cx| { + style.bg(cx.theme().colors().drop_target_background) }) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + window, + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + cx.stop_propagation(); + }, + )) }) } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index fc399d66a7..0101b4333c 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -1,8 +1,9 @@ -use editor::ShowScrollbar; +use editor::EditorSettings; use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; +use ui::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, Copy, PartialEq)] #[serde(rename_all = "snake_case")] @@ -47,7 +48,6 @@ pub struct ProjectPanelSettings { pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, - pub drag_and_drop: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -161,10 +161,14 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, +} + +impl ScrollbarVisibilitySetting for ProjectPanelSettings { + fn scrollbar_visibility(&self, cx: &ui::App) -> ShowScrollbar { + self.scrollbar + .show + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } } impl Settings for ProjectPanelSettings { diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index a9c3284d0b..107a857cb0 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -1,4 +1,3 @@ -use std::any::Any; use std::borrow::Cow; use std::collections::BTreeSet; use std::path::PathBuf; @@ -37,9 +36,10 @@ use settings::watch_config_file; use smol::stream::StreamExt as _; use ui::Navigable; use ui::NavigableEntry; +use ui::WithScrollbar; use ui::{ - IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar, ScrollbarState, - Section, Tooltip, prelude::*, + IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Section, Tooltip, + prelude::*, }; use util::{ ResultExt, @@ -297,7 +297,7 @@ impl RemoteEntry { #[derive(Clone)] struct DefaultState { - scrollbar: ScrollbarState, + scroll_handle: ScrollHandle, add_new_server: NavigableEntry, servers: Vec, } @@ -305,7 +305,6 @@ struct DefaultState { impl DefaultState { fn new(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { let handle = ScrollHandle::new(); - let scrollbar = ScrollbarState::new(handle.clone()); let add_new_server = NavigableEntry::new(&handle, cx); let ssh_settings = SshSettings::get_global(cx); @@ -346,7 +345,7 @@ impl DefaultState { } Self { - scrollbar, + scroll_handle: handle, add_new_server, servers, } @@ -1449,7 +1448,6 @@ impl RemoteServerProjects { } } - let scroll_state = state.scrollbar.parent_entity(&cx.entity()); let connect_button = div() .id("ssh-connect-new-server-container") .track_focus(&state.add_new_server.focus_handle) @@ -1480,17 +1478,12 @@ impl RemoteServerProjects { cx.notify(); })); - let handle = &**scroll_state.scroll_handle() as &dyn Any; - let Some(scroll_handle) = handle.downcast_ref::() else { - unreachable!() - }; - let mut modal_section = Navigable::new( v_flex() .track_focus(&self.focus_handle(cx)) .id("ssh-server-list") .overflow_y_scroll() - .track_scroll(scroll_handle) + .track_scroll(&state.scroll_handle) .size_full() .child(connect_button) .child( @@ -1585,17 +1578,7 @@ impl RemoteServerProjects { ) .size_full(), ) - .child( - div() - .occlude() - .h_full() - .absolute() - .top_1() - .bottom_1() - .right_1() - .w(px(8.)) - .children(Scrollbar::vertical(scroll_state)), - ), + .vertical_scrollbar_for(state.scroll_handle.clone(), window, cx), ), ) .into_any_element() diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 6794018470..b9af528643 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -445,7 +445,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -476,7 +476,7 @@ impl SshSocket { } async fn shell(&self) -> String { - match self.run_command("sh", &["-lc", "echo $SHELL"]).await { + match self.run_command("sh", &["-c", "echo $SHELL"]).await { Ok(shell) => shell.trim().to_owned(), Err(e) => { log::error!("Failed to get shell: {e}"); @@ -1533,7 +1533,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-lc", &start_proxy_command]) + .ssh_command("sh", &["-c", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1910,7 +1910,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -1988,7 +1988,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -2036,7 +2036,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-lc", &script]).await?; + self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs new file mode 100644 index 0000000000..6580137535 --- /dev/null +++ b/crates/settings/src/key_equivalents.rs @@ -0,0 +1,1424 @@ +use collections::HashMap; + +// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range +// without using option. This means that some of our built in keyboard shortcuts do not work +// for those users. +// +// The way macOS solves this problem is to move shortcuts around so that they are all reachable, +// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct +// +// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. +// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves +// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position +// as cmd-> on a QWERTY layout. +// +// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö +// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard +// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the +// specific key moves) +// +// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every +// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... +// +// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the +// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: +// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' +// From there I used multi-cursor to produce this match statement. +#[cfg(target_os = "macos")] +pub fn get_key_equivalents(layout: &str) -> Option> { + let mappings: &[(char, char)] = match layout { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], + + _ => return None, + }; + + Some(HashMap::from_iter(mappings.iter().cloned())) +} + +#[cfg(not(target_os = "macos"))] +pub fn get_key_equivalents(_layout: &str) -> Option> { + None +} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0e8303c4c1..ae3f42853a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -3,8 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap}; use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, - KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, + KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -212,6 +211,9 @@ impl KeymapFile { } pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { + let key_equivalents = + crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id()); + if content.is_empty() { return KeymapFileLoadResult::Success { key_bindings: Vec::new(), @@ -253,6 +255,12 @@ impl KeymapFile { } }; + let key_equivalents = if *use_key_equivalents { + key_equivalents.as_ref() + } else { + None + }; + let mut section_errors = String::new(); if !unrecognized_fields.is_empty() { @@ -270,7 +278,7 @@ impl KeymapFile { keystrokes, action, context_predicate.clone(), - *use_key_equivalents, + key_equivalents, cx, ); match result { @@ -328,7 +336,7 @@ impl KeymapFile { keystrokes: &str, action: &KeymapAction, context: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, cx: &App, ) -> std::result::Result { let (build_result, action_input_string) = match &action.0 { @@ -396,9 +404,8 @@ impl KeymapFile { keystrokes, action, context, - use_key_equivalents, + key_equivalents, action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), ) { Ok(key_binding) => key_binding, Err(InvalidKeystrokeError { keystroke }) => { @@ -600,7 +607,6 @@ impl KeymapFile { mut operation: KeybindUpdateOperation<'a>, mut keymap_contents: String, tab_size: usize, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Result { match operation { // if trying to replace a keybinding that is not user-defined, treat it as an add operation @@ -640,7 +646,7 @@ impl KeymapFile { .action_value() .context("Failed to generate target action JSON value")?; let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) else { anyhow::bail!("Failed to find keybinding to remove"); }; @@ -675,7 +681,7 @@ impl KeymapFile { .context("Failed to generate source action JSON value")?; if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) { if target.context == source.context { // if we are only changing the keybinding (common case) @@ -775,7 +781,7 @@ impl KeymapFile { } let use_key_equivalents = from.and_then(|from| { let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; - let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?; + let (index, _) = find_binding(&keymap, &from, &action_value)?; Some(keymap.0[index].use_key_equivalents) }).unwrap_or(false); if use_key_equivalents { @@ -802,7 +808,6 @@ impl KeymapFile { keymap: &'b KeymapFile, target: &KeybindUpdateTarget<'a>, target_action_value: &Value, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Option<(usize, &'b str)> { let target_context_parsed = KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); @@ -818,11 +823,8 @@ impl KeymapFile { for (keystrokes_str, action) in bindings { let Ok(keystrokes) = keystrokes_str .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper)) - }) - .collect::, InvalidKeystrokeError>>() + .map(Keystroke::parse) + .collect::, _>>() else { continue; }; @@ -830,7 +832,7 @@ impl KeymapFile { || !keystrokes .iter() .zip(target.keystrokes) - .all(|(a, b)| a.inner.should_match(b)) + .all(|(a, b)| a.should_match(b)) { continue; } @@ -845,7 +847,7 @@ impl KeymapFile { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -914,7 +916,7 @@ impl<'a> KeybindUpdateOperation<'a> { #[derive(Debug, Clone)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, - pub keystrokes: &'a [KeybindingKeystroke], + pub keystrokes: &'a [Keystroke], pub action_name: &'a str, pub action_arguments: Option<&'a str>, } @@ -939,9 +941,6 @@ impl<'a> KeybindUpdateTarget<'a> { fn keystrokes_unparsed(&self) -> String { let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); for keystroke in self.keystrokes { - // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()` - // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$` - // by default on Windows. keystrokes.push_str(&keystroke.unparse()); keystrokes.push(' '); } @@ -960,7 +959,7 @@ impl<'a> KeybindUpdateTarget<'a> { } } -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum KeybindSource { User, Vim, @@ -1021,7 +1020,7 @@ impl From for KeyBindingMetaIndex { #[cfg(test)] mod tests { - use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; + use gpui::Keystroke; use unindent::Unindent; use crate::{ @@ -1050,27 +1049,16 @@ mod tests { operation: KeybindUpdateOperation, expected: impl ToString, ) { - let result = KeymapFile::update_keybinding( - operation, - input.to_string(), - 4, - &gpui::DummyKeyboardMapper, - ) - .expect("Update succeeded"); + let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) + .expect("Update succeeded"); pretty_assertions::assert_eq!(expected.to_string(), result); } #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { + fn parse_keystrokes(keystrokes: &str) -> Vec { keystrokes .split(' ') - .map(|s| { - KeybindingKeystroke::new( - Keystroke::parse(s).expect("Keystrokes valid"), - false, - &DummyKeyboardMapper, - ) - }) + .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) .collect() } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1966755d62..b73ab9ae95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,6 @@ mod base_keymap_setting; mod editable_setting_control; +mod key_equivalents; mod keymap_file; mod settings_file; mod settings_json; @@ -13,6 +14,7 @@ use util::asset_str; pub use base_keymap_setting::*; pub use editable_setting_control::*; +pub use key_equivalents::*; pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, @@ -87,10 +89,7 @@ pub fn default_settings() -> Cow<'static, str> { #[cfg(target_os = "macos")] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; -#[cfg(target_os = "windows")] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json"; - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] +#[cfg(not(target_os = "macos"))] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; pub fn default_keymap() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 76c7166007..7e28977ee6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -11,12 +11,12 @@ use editor::{CompletionProvider, Editor, EditorEvent}; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, + Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, + FocusHandle, Focusable, Global, IsZero, KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, - KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, - ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, - TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, + KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, + StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, + div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -174,7 +174,7 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystrokes: Vec, + keystrokes: Vec, context: Option, } @@ -236,7 +236,7 @@ struct ConflictState { } type ConflictKeybindMapping = HashMap< - Vec, + Vec, Vec<( Option, Vec, @@ -414,21 +414,19 @@ impl Focusable for KeymapEditor { } } /// Helper function to check if two keystroke sequences match exactly -fn keystrokes_match_exactly( - keystrokes1: &[KeybindingKeystroke], - keystrokes2: &[KeybindingKeystroke], -) -> bool { +fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { keystrokes1.len() == keystrokes2.len() - && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { - k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers - }) + && keystrokes1 + .iter() + .zip(keystrokes2) + .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) } impl KeymapEditor { fn new(workspace: WeakEntity, window: &mut Window, cx: &mut Context) -> Self { let _keymap_subscription = cx.observe_global_in::(window, Self::on_keymap_changed); - let table_interaction_state = TableInteractionState::new(window, cx); + let table_interaction_state = TableInteractionState::new(cx); let keystroke_editor = cx.new(|cx| { let mut keystroke_editor = KeystrokeInput::new(None, window, cx); @@ -511,7 +509,7 @@ impl KeymapEditor { self.filter_editor.read(cx).text(cx) } - fn current_keystroke_query(&self, cx: &App) -> Vec { + fn current_keystroke_query(&self, cx: &App) -> Vec { match self.search_mode { SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), @@ -532,7 +530,7 @@ impl KeymapEditor { let keystroke_query = keystroke_query .into_iter() - .map(|keystroke| keystroke.inner.unparse()) + .map(|keystroke| keystroke.unparse()) .collect::>() .join(" "); @@ -556,7 +554,7 @@ impl KeymapEditor { async fn update_matches( this: WeakEntity, action_query: String, - keystroke_query: Vec, + keystroke_query: Vec, cx: &mut AsyncApp, ) -> anyhow::Result<()> { let action_query = command_palette::normalize_action_query(&action_query); @@ -605,15 +603,13 @@ impl KeymapEditor { { let query = &keystroke_query[query_cursor]; let keystroke = &keystrokes[keystroke_cursor]; - let matches = query - .inner - .modifiers - .is_subset_of(&keystroke.inner.modifiers) - && ((query.inner.key.is_empty() - || query.inner.key == keystroke.inner.key) - && query.inner.key_char.as_ref().is_none_or( - |q_kc| q_kc == &keystroke.inner.key, - )); + let matches = + query.modifiers.is_subset_of(&keystroke.modifiers) + && ((query.key.is_empty() + || query.key == keystroke.key) + && query.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -682,7 +678,7 @@ impl KeymapEditor { .map(KeybindSource::from_meta) .unwrap_or(KeybindSource::Unknown); - let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); + let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) .vim_mode(source == KeybindSource::Vim); @@ -784,9 +780,8 @@ impl KeymapEditor { match previous_edit { // should remove scroll from process_query PreviousEdit::ScrollBarOffset(offset) => { - this.table_interaction_state.update(cx, |table, _| { - table.set_scrollbar_offset(Axis::Vertical, offset) - }) + this.table_interaction_state + .update(cx, |table, _| table.set_scroll_offset(offset)) // set selected index and scroll } PreviousEdit::Keybinding { @@ -815,9 +810,8 @@ impl KeymapEditor { cx, ); } else { - this.table_interaction_state.update(cx, |table, _| { - table.set_scrollbar_offset(Axis::Vertical, fallback) - }); + this.table_interaction_state + .update(cx, |table, _| table.set_scroll_offset(fallback)); } cx.notify(); } @@ -1202,15 +1196,10 @@ impl KeymapEditor { }; let tab_size = cx.global::().json_tab_size(); self.previous_edit = Some(PreviousEdit::ScrollBarOffset( - self.table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), + self.table_interaction_state.read(cx).scroll_offset(), )); - let keyboard_mapper = cx.keyboard_mapper().clone(); - cx.spawn(async move |_, _| { - remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await - }) - .detach_and_notify_err(window, cx); + cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) + .detach_and_notify_err(window, cx); } fn copy_context_to_clipboard( @@ -1429,7 +1418,7 @@ impl ProcessedBinding { .map(|keybind| keybind.get_action_mapping()) } - fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { + fn keystrokes(&self) -> Option<&[Keystroke]> { self.ui_key_binding() .map(|binding| binding.keystrokes.as_slice()) } @@ -2227,7 +2216,7 @@ impl KeybindingEditorModal { Ok(action_arguments) } - fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { + fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { let new_keystrokes = self .keybind_editor .read_with(cx, |editor, _| editor.keystrokes().to_vec()); @@ -2323,7 +2312,6 @@ impl KeybindingEditorModal { }).unwrap_or(Ok(()))?; let create = self.creating; - let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; @@ -2336,7 +2324,6 @@ impl KeybindingEditorModal { new_action_args.as_deref(), &fs, tab_size, - keyboard_mapper.as_ref(), ) .await { @@ -2346,10 +2333,7 @@ impl KeybindingEditorModal { keymap.previous_edit = Some(PreviousEdit::Keybinding { action_mapping, action_name, - fallback: keymap - .table_interaction_state - .read(cx) - .get_scrollbar_offset(Axis::Vertical), + fallback: keymap.table_interaction_state.read(cx).scroll_offset(), }); let status_toast = StatusToast::new( format!("Saved edits to the {} action.", humanized_action_name), @@ -2454,21 +2438,11 @@ impl KeybindingEditorModal { } } -fn remove_key_char( - KeybindingKeystroke { - inner, - display_modifiers, - display_key, - }: KeybindingKeystroke, -) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers: inner.modifiers, - key: inner.key, - key_char: None, - }, - display_modifiers, - display_key, +fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { + Keystroke { + modifiers, + key, + ..Default::default() } } @@ -3011,7 +2985,6 @@ async fn save_keybinding_update( new_args: Option<&str>, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await @@ -3054,13 +3027,9 @@ async fn save_keybinding_update( let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3081,7 +3050,6 @@ async fn remove_keybinding( existing: ProcessedBinding, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let Some(keystrokes) = existing.keystrokes() else { anyhow::bail!("Cannot remove a keybinding that does not exist"); @@ -3105,13 +3073,9 @@ async fn remove_keybinding( }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .context("Failed to update keybinding")?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .context("Failed to update keybinding")?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3377,15 +3341,12 @@ impl SerializableItem for KeymapEditor { } mod persistence { - use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - - impl Domain for KeybindingEditorDb { - const NAME: &str = stringify!(KeybindingEditorDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = + &[sql!( CREATE TABLE keybinding_editors ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -3394,11 +3355,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]); - impl KeybindingEditorDb { query! { pub async fn save_keybinding_editor( diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index ca50d5c03d..1b8010853e 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult { } pub struct KeystrokeInput { - keystrokes: Vec, - placeholder_keystrokes: Option>, + keystrokes: Vec, + placeholder_keystrokes: Option>, outer_focus_handle: FocusHandle, inner_focus_handle: FocusHandle, intercept_subscription: Option, @@ -70,7 +70,7 @@ impl KeystrokeInput { const KEYSTROKE_COUNT_MAX: usize = 3; pub fn new( - placeholder_keystrokes: Option>, + placeholder_keystrokes: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -97,7 +97,7 @@ impl KeystrokeInput { } } - pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { + pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { self.keystrokes = keystrokes; self.keystrokes_changed(cx); } @@ -106,7 +106,7 @@ impl KeystrokeInput { self.search = search; } - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { if let Some(placeholders) = self.placeholder_keystrokes.as_ref() && self.keystrokes.is_empty() { @@ -116,22 +116,18 @@ impl KeystrokeInput { && self .keystrokes .last() - .is_some_and(|last| last.display_key.is_empty()) + .is_some_and(|last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } &self.keystrokes } - fn dummy(modifiers: Modifiers) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers, - key: "".to_string(), - key_char: None, - }, - display_modifiers: modifiers, - display_key: "".to_string(), + fn dummy(modifiers: Modifiers) -> Keystroke { + Keystroke { + modifiers, + key: "".to_string(), + key_char: None, } } @@ -258,7 +254,7 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() - && last.display_key.is_empty() + && last.key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !self.search && !event.modifiers.modified() { @@ -267,15 +263,13 @@ impl KeystrokeInput { } if self.search { if self.previous_modifiers.modified() { - last.display_modifiers |= event.modifiers; - last.inner.modifiers |= event.modifiers; + last.modifiers |= event.modifiers; } else { self.keystrokes.push(Self::dummy(event.modifiers)); } self.previous_modifiers |= event.modifiers; } else { - last.display_modifiers = event.modifiers; - last.inner.modifiers = event.modifiers; + last.modifiers = event.modifiers; return; } } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { @@ -303,17 +297,14 @@ impl KeystrokeInput { return; } - let mut keystroke = - KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); + let mut keystroke = keystroke.clone(); if let Some(last) = self.keystrokes.last() - && last.display_key.is_empty() + && last.key.is_empty() && (!self.search || self.previous_modifiers.modified()) { - let display_key = keystroke.display_key.clone(); - let inner_key = keystroke.inner.key.clone(); + let key = keystroke.key.clone(); keystroke = last.clone(); - keystroke.display_key = display_key; - keystroke.inner.key = inner_key; + keystroke.key = key; self.keystrokes.pop(); } @@ -333,14 +324,11 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if self.search { - self.previous_modifiers = keystroke.display_modifiers; + self.previous_modifiers = keystroke.modifiers; return; } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.display_modifiers.modified() - { - self.keystrokes - .push(Self::dummy(keystroke.display_modifiers)); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); } } @@ -376,7 +364,7 @@ impl KeystrokeInput { &self.keystrokes }; keystrokes.iter().map(move |keystroke| { - h_flex().children(ui::render_keybinding_keystroke( + h_flex().children(ui::render_keystroke( keystroke, Some(Color::Default), Some(rems(0.875).into()), @@ -821,13 +809,9 @@ mod tests { /// Verifies that the keystrokes match the expected strings #[track_caller] pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { - let actual: Vec = self.input.read_with(&self.cx, |input, _| { - input - .keystrokes - .iter() - .map(|keystroke| keystroke.inner.clone()) - .collect() - }); + let actual = self + .input + .read_with(&self.cx, |input, _| input.keystrokes.clone()); Self::expect_keystrokes_equal(&actual, expected); self } @@ -955,7 +939,7 @@ mod tests { } struct KeystrokeUpdateTracker { - initial_keystrokes: Vec, + initial_keystrokes: Vec, _subscription: Subscription, input: Entity, received_keystrokes_updated: bool, @@ -999,8 +983,8 @@ mod tests { ); } - fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String { - ks.iter().map(|ks| ks.inner.unparse()).join(" ") + fn keystrokes_str(ks: &[Keystroke]) -> String { + ks.iter().map(|ks| ks.unparse()).join(" ") } } } diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 9d7bb07360..cb0332c868 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -1,20 +1,20 @@ -use std::{ops::Range, rc::Rc, time::Duration}; +use std::{ops::Range, rc::Rc}; -use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; +use editor::EditorSettings; use gpui::{ - AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, - FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, - Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, + AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, + FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful, + UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, }; use itertools::intersperse_with; -use settings::Settings as _; use ui::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, - StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, + ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, + StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px, + single_example, v_flex, }; const RESIZE_COLUMN_WIDTH: f32 = 8.0; @@ -56,136 +56,22 @@ impl TableContents { pub struct TableInteractionState { pub focus_handle: FocusHandle, pub scroll_handle: UniformListScrollHandle, - pub horizontal_scrollbar: ScrollbarProperties, - pub vertical_scrollbar: ScrollbarProperties, } impl TableInteractionState { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - - cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| { - this.hide_scrollbars(window, cx); - }) - .detach(); - - let scroll_handle = UniformListScrollHandle::new(); - let vertical_scrollbar = ScrollbarProperties { - axis: Axis::Vertical, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - - let horizontal_scrollbar = ScrollbarProperties { - axis: Axis::Horizontal, - state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()), - show_scrollbar: false, - show_track: false, - auto_hide: false, - hide_task: None, - }; - - let mut this = Self { - focus_handle, - scroll_handle, - horizontal_scrollbar, - vertical_scrollbar, - }; - - this.update_scrollbar_visibility(cx); - this + pub fn new(cx: &mut App) -> Entity { + cx.new(|cx| Self { + focus_handle: cx.focus_handle(), + scroll_handle: UniformListScrollHandle::new(), }) } - pub fn get_scrollbar_offset(&self, axis: Axis) -> Point { - match axis { - Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(), - Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(), - } + pub fn scroll_offset(&self) -> Point { + self.scroll_handle.offset() } - pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point) { - match axis { - Axis::Vertical => self - .vertical_scrollbar - .state - .scroll_handle() - .set_offset(offset), - Axis::Horizontal => self - .horizontal_scrollbar - .state - .scroll_handle() - .set_offset(offset), - } - } - - fn update_scrollbar_visibility(&mut self, cx: &mut Context) { - let show_setting = EditorSettings::get_global(cx).scrollbar.show; - - let scroll_handle = self.scroll_handle.0.borrow(); - - let autohide = |show: ShowScrollbar, cx: &mut Context| match show { - ShowScrollbar::Auto => true, - ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - ShowScrollbar::Always => false, - ShowScrollbar::Never => false, - }; - - let longest_item_width = scroll_handle.last_item_size.and_then(|size| { - (size.contents.width > size.item.width).then_some(size.contents.width) - }); - - // is there an item long enough that we should show a horizontal scrollbar? - let item_wider_than_container = if let Some(longest_item_width) = longest_item_width { - longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0) - } else { - true - }; - - let show_scrollbar = match show_setting { - ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - }; - let show_vertical = show_scrollbar; - - let show_horizontal = item_wider_than_container && show_scrollbar; - - let show_horizontal_track = - show_horizontal && matches!(show_setting, ShowScrollbar::Always); - - // TODO: we probably should hide the scroll track when the list doesn't need to scroll - let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always); - - self.vertical_scrollbar = ScrollbarProperties { - axis: self.vertical_scrollbar.axis, - state: self.vertical_scrollbar.state.clone(), - show_scrollbar: show_vertical, - show_track: show_vertical_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - self.horizontal_scrollbar = ScrollbarProperties { - axis: self.horizontal_scrollbar.axis, - state: self.horizontal_scrollbar.state.clone(), - show_scrollbar: show_horizontal, - show_track: show_horizontal_track, - auto_hide: autohide(show_setting, cx), - hide_task: None, - }; - - cx.notify(); - } - - fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { - self.horizontal_scrollbar.hide(window, cx); - self.vertical_scrollbar.hide(window, cx); + pub fn set_scroll_offset(&self, offset: Point) { + self.scroll_handle.set_offset(offset); } pub fn listener( @@ -280,183 +166,6 @@ impl TableInteractionState { .children(dividers) .into_any_element() } - - fn render_vertical_scrollbar_track( - this: &Entity, - parent: Div, - scroll_track_size: Pixels, - cx: &mut App, - ) -> Div { - if !this.read(cx).vertical_scrollbar.show_track { - return parent; - } - let child = v_flex() - .h_full() - .flex_none() - .w(scroll_track_size) - .bg(cx.theme().colors().background) - .child( - div() - .size_full() - .flex_1() - .border_l_1() - .border_color(cx.theme().colors().border), - ); - parent.child(child) - } - - fn render_vertical_scrollbar(this: &Entity, parent: Div, cx: &mut App) -> Div { - if !this.read(cx).vertical_scrollbar.show_scrollbar { - return parent; - } - let child = div() - .id(("table-vertical-scrollbar", this.entity_id())) - .occlude() - .flex_none() - .h_full() - .cursor_default() - .absolute() - .right_0() - .top_0() - .bottom_0() - .w(px(12.)) - .on_mouse_move(Self::listener(this, |_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - Self::listener(this, |this, _, window, cx| { - if !this.vertical_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.vertical_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_scroll_wheel(Self::listener(this, |_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::vertical( - this.read(cx).vertical_scrollbar.state.clone(), - )); - parent.child(child) - } - - /// Renders the horizontal scrollbar. - /// - /// The right offset is used to determine how far to the right the - /// scrollbar should extend to, useful for ensuring it doesn't collide - /// with the vertical scrollbar when visible. - fn render_horizontal_scrollbar( - this: &Entity, - parent: Div, - right_offset: Pixels, - cx: &mut App, - ) -> Div { - if !this.read(cx).horizontal_scrollbar.show_scrollbar { - return parent; - } - let child = div() - .id(("table-horizontal-scrollbar", this.entity_id())) - .occlude() - .flex_none() - .w_full() - .cursor_default() - .absolute() - .bottom_neg_px() - .left_0() - .right_0() - .pr(right_offset) - .on_mouse_move(Self::listener(this, |_, _, _, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - Self::listener(this, |this, _, window, cx| { - if !this.horizontal_scrollbar.state.is_dragging() - && !this.focus_handle.contains_focused(window, cx) - { - this.horizontal_scrollbar.hide(window, cx); - cx.notify(); - } - - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(Self::listener(this, |_, _, _, cx| { - cx.notify(); - })) - .children(Scrollbar::horizontal( - // percentage as f32..end_offset as f32, - this.read(cx).horizontal_scrollbar.state.clone(), - )); - parent.child(child) - } - - fn render_horizontal_scrollbar_track( - this: &Entity, - parent: Div, - scroll_track_size: Pixels, - cx: &mut App, - ) -> Div { - if !this.read(cx).horizontal_scrollbar.show_track { - return parent; - } - let child = h_flex() - .w_full() - .h(scroll_track_size) - .flex_none() - .relative() - .child( - div() - .w_full() - .flex_1() - // for some reason the horizontal scrollbar is 1px - // taller than the vertical scrollbar?? - .h(scroll_track_size - px(1.)) - .bg(cx.theme().colors().background) - .border_t_1() - .border_color(cx.theme().colors().border), - ) - .when(this.read(cx).vertical_scrollbar.show_track, |parent| { - parent - .child( - div() - .flex_none() - // -1px prevents a missing pixel between the two container borders - .w(scroll_track_size - px(1.)) - .h_full(), - ) - .child( - // HACK: Fill the missing 1px 🥲 - div() - .absolute() - .right(scroll_track_size - px(1.)) - .bottom(scroll_track_size - px(1.)) - .size_px() - .bg(cx.theme().colors().border), - ) - }); - - parent.child(child) - } } #[derive(Debug, Copy, Clone, PartialEq)] @@ -1054,17 +763,6 @@ impl RenderOnce for Table { .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); - let scroll_track_size = px(16.); - let h_scroll_offset = if interaction_state - .as_ref() - .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar) - { - // magic number - px(3.) - } else { - px(0.) - }; - let width = self.width; let no_rows_rendered = self.rows.is_empty(); @@ -1115,8 +813,8 @@ impl RenderOnce for Table { }) } }) - .child( - div() + .child({ + let content = div() .flex_grow() .w_full() .relative() @@ -1187,25 +885,21 @@ impl RenderOnce for Table { ) })) }, - ) - .when_some(interaction_state.as_ref(), |this, interaction_state| { - this.map(|this| { - TableInteractionState::render_vertical_scrollbar_track( - interaction_state, - this, - scroll_track_size, - cx, - ) - }) - .map(|this| { - TableInteractionState::render_vertical_scrollbar( - interaction_state, - this, - cx, - ) - }) - }), - ) + ); + + if let Some(state) = interaction_state.as_ref() { + content + .custom_scrollbars( + Scrollbars::for_settings::() + .tracked_scroll_handle(state.read(cx).scroll_handle.clone()), + window, + cx, + ) + .into_any_element() + } else { + content.into_any_element() + } + }) .when_some( no_rows_rendered .then_some(self.empty_table_callback) @@ -1220,52 +914,12 @@ impl RenderOnce for Table { .child(callback(window, cx)), ) }, - ) - .when_some( - width.and(interaction_state.as_ref()), - |this, interaction_state| { - this.map(|this| { - TableInteractionState::render_horizontal_scrollbar_track( - interaction_state, - this, - scroll_track_size, - cx, - ) - }) - .map(|this| { - TableInteractionState::render_horizontal_scrollbar( - interaction_state, - this, - h_scroll_offset, - cx, - ) - }) - }, ); if let Some(interaction_state) = interaction_state.as_ref() { table .track_focus(&interaction_state.read(cx).focus_handle) .id(("table", interaction_state.entity_id())) - .on_hover({ - let interaction_state = interaction_state.downgrade(); - move |hovered, window, cx| { - interaction_state - .update(cx, |interaction_state, cx| { - if *hovered { - interaction_state.horizontal_scrollbar.show(cx); - interaction_state.vertical_scrollbar.show(cx); - cx.notify(); - } else if !interaction_state - .focus_handle - .contains_focused(window, cx) - { - interaction_state.hide_scrollbars(window, cx); - } - }) - .ok(); - } - }) .into_any_element() } else { table.into_any_element() @@ -1273,65 +927,6 @@ impl RenderOnce for Table { } } -// computed state related to how to render scrollbars -// one per axis -// on render we just read this off the keymap editor -// we update it when -// - settings change -// - on focus in, on focus out, on hover, etc. -#[derive(Debug)] -pub struct ScrollbarProperties { - axis: Axis, - show_scrollbar: bool, - show_track: bool, - auto_hide: bool, - hide_task: Option>, - state: ScrollbarState, -} - -impl ScrollbarProperties { - // Shows the scrollbar and cancels any pending hide task - fn show(&mut self, cx: &mut Context) { - if !self.auto_hide { - return; - } - self.show_scrollbar = true; - self.hide_task.take(); - cx.notify(); - } - - fn hide(&mut self, window: &mut Window, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - - if !self.auto_hide { - return; - } - - let axis = self.axis; - self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - - if let Some(keymap_editor) = keymap_editor.upgrade() { - keymap_editor - .update(cx, |keymap_editor, cx| { - match axis { - Axis::Vertical => { - keymap_editor.vertical_scrollbar.show_scrollbar = false - } - Axis::Horizontal => { - keymap_editor.horizontal_scrollbar.show_scrollbar = false - } - } - cx.notify(); - }) - .ok(); - } - })); - } -} - impl Component for Table<3> { fn scope() -> ComponentScope { ComponentScope::Layout diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index 5744a67da2..a83f4e18d6 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -1,12 +1,8 @@ use crate::connection::Connection; pub trait Domain: 'static { - const NAME: &str; - const MIGRATIONS: &[&str]; - - fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { - false - } + fn name() -> &'static str; + fn migrations() -> &'static [&'static str]; } pub trait Migrator: 'static { @@ -21,11 +17,7 @@ impl Migrator for () { impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate( - Self::NAME, - Self::MIGRATIONS, - Self::should_allow_migration_change, - ) + connection.migrate(Self::name(), Self::migrations()) } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 2429ddeb41..7c59ffe658 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -34,12 +34,7 @@ impl Connection { /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// preparing the SQL statements. This makes it possible to do multi-statement schema /// updates in a single string without running into prepare errors. - pub fn migrate( - &self, - domain: &'static str, - migrations: &[&'static str], - mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, - ) -> Result<()> { + pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally self.exec(indoc! {" @@ -70,14 +65,9 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration - || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") - { + if completed_migration == migration { // Migration already run. Continue continue; - } else if should_allow_migration_change(index, &completed_migration, &migration) - { - continue; } else { anyhow::bail!(formatdoc! {" Migration changed for {domain} at step {index} @@ -118,7 +108,6 @@ mod test { a TEXT, b TEXT )"}], - disallow_migration_change, ) .unwrap(); @@ -147,7 +136,6 @@ mod test { d TEXT )"}, ], - disallow_migration_change, ) .unwrap(); @@ -226,11 +214,7 @@ mod test { // Run the migration verifying that the row got dropped connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -248,11 +232,7 @@ mod test { // Run the same migration again and verify that the table was left unchanged connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -272,28 +252,27 @@ mod test { .migrate( "test migration", &[ - "CREATE TABLE test (col INTEGER)", - "INSERT INTO test (col) VALUES (1)", + indoc! {" + CREATE TABLE test ( + col INTEGER + )"}, + indoc! {" + INSERT INTO test (col) VALUES (1)"}, ], - disallow_migration_change, ) .unwrap(); - let mut migration_changed = false; - // Create another migration with the same domain but different steps let second_migration_result = connection.migrate( "test migration", &[ - "CREATE TABLE test (color INTEGER )", - "INSERT INTO test (color) VALUES (1)", + indoc! {" + CREATE TABLE test ( + color INTEGER + )"}, + indoc! {" + INSERT INTO test (color) VALUES (1)"}, ], - |_, old, new| { - assert_eq!(old, "CREATE TABLE test (col INTEGER)"); - assert_eq!(new, "CREATE TABLE test (color INTEGER)"); - migration_changed = true; - false - }, ); // Verify new migration returns error when run @@ -305,11 +284,7 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate( - "first_migration", - &["CREATE TABLE table1(a TEXT) STRICT;"], - disallow_migration_change, - ) + .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) .unwrap(); connection @@ -330,7 +305,6 @@ mod test { ALTER TABLE table2 RENAME TO table1; "}], - disallow_migration_change, ) .unwrap(); @@ -338,8 +312,4 @@ mod test { assert_eq!(res, "test text"); } - - fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { - false - } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 58d3afe78f..afdc96586e 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -278,8 +278,12 @@ mod test { enum TestDomain {} impl Domain for TestDomain { - const NAME: &str = "test"; - const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; + fn name() -> &'static str { + "test" + } + fn migrations() -> &'static [&'static str] { + &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"] + } } for _ in 0..100 { @@ -308,9 +312,12 @@ mod test { fn wild_zed_lost_failure() { enum TestWorkspace {} impl Domain for TestWorkspace { - const NAME: &str = "workspace"; + fn name() -> &'static str { + "workspace" + } - const MIGRATIONS: &[&str] = &[" + fn migrations() -> &'static [&'static str] { + &[" CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, dock_visible INTEGER, -- Boolean @@ -329,7 +336,8 @@ mod test { ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - "]; + "] + } } let builder = diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index c7ebd314e4..b93b267f58 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -9,11 +9,7 @@ use std::path::{Path, PathBuf}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, WorkspaceDb, WorkspaceId, @@ -379,13 +375,9 @@ impl<'de> Deserialize<'de> for SerializedAxis { } } -pub struct TerminalDb(ThreadSafeConnection); - -impl Domain for TerminalDb { - const NAME: &str = stringify!(TerminalDb); - - const MIGRATIONS: &[&str] = &[ - sql!( +define_connection! { + pub static ref TERMINAL_DB: TerminalDb = + &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -422,8 +414,6 @@ impl Domain for TerminalDb { ]; } -db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); - impl TerminalDb { query! { pub async fn update_workspace_id( diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7..36df9d88d9 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -7,12 +7,11 @@ mod terminal_slash_command; pub mod terminal_tab_tooltip; use assistant_slash_command::SlashCommandRegistry; -use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; +use editor::{EditorSettings, actions::SelectAll}; use gpui::{ Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, - ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, - deferred, div, + ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; use persistence::TERMINAL_DB; use project::{Project, search::SearchQuery, terminals::TerminalKind}; @@ -35,7 +34,9 @@ use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; use terminal_tab_tooltip::TerminalTooltip; use ui::{ - ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*, + ContextMenu, Icon, IconName, Label, ScrollAxes, Scrollbars, Tooltip, WithScrollbar, h_flex, + prelude::*, + scrollbars::{self, GlobalValue, ScrollbarVisibilitySetting}, }; use util::ResultExt; use workspace::{ @@ -63,7 +64,6 @@ use std::{ }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view #[derive(Clone, Debug, PartialEq)] @@ -134,10 +134,7 @@ pub struct TerminalView { show_breadcrumbs: bool, block_below_cursor: Option>, scroll_top: Pixels, - scrollbar_state: ScrollbarState, scroll_handle: TerminalScrollHandle, - show_scrollbar: bool, - hide_scrollbar_task: Option>, marked_text: Option, marked_range_utf16: Option>, _subscriptions: Vec, @@ -261,10 +258,7 @@ impl TerminalView { show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs, block_below_cursor: None, scroll_top: Pixels::ZERO, - scrollbar_state: ScrollbarState::new(scroll_handle.clone()), scroll_handle, - show_scrollbar: !Self::should_autohide_scrollbar(cx), - hide_scrollbar_task: None, cwd_serialized: false, marked_text: None, marked_range_utf16: None, @@ -833,136 +827,6 @@ impl TerminalView { self.terminal = terminal; } - // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor. - fn map_show_scrollbar_from_editor_to_terminal( - show_scrollbar: editor::ShowScrollbar, - ) -> terminal_settings::ShowScrollbar { - match show_scrollbar { - editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto, - editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System, - editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always, - editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never, - } - } - - fn should_show_scrollbar(cx: &App) -> bool { - let show = TerminalSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| { - Self::map_show_scrollbar_from_editor_to_terminal( - EditorSettings::get_global(cx).scrollbar.show, - ) - }); - match show { - terminal_settings::ShowScrollbar::Auto => true, - terminal_settings::ShowScrollbar::System => true, - terminal_settings::ShowScrollbar::Always => true, - terminal_settings::ShowScrollbar::Never => false, - } - } - - fn should_autohide_scrollbar(cx: &App) -> bool { - let show = TerminalSettings::get_global(cx) - .scrollbar - .show - .unwrap_or_else(|| { - Self::map_show_scrollbar_from_editor_to_terminal( - EditorSettings::get_global(cx).scrollbar.show, - ) - }); - match show { - terminal_settings::ShowScrollbar::Auto => true, - terminal_settings::ShowScrollbar::System => cx - .try_global::() - .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0), - terminal_settings::ShowScrollbar::Always => false, - terminal_settings::ShowScrollbar::Never => true, - } - } - - fn hide_scrollbar(&mut self, cx: &mut Context) { - const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); - if !Self::should_autohide_scrollbar(cx) { - return; - } - self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| { - cx.background_executor() - .timer(SCROLLBAR_SHOW_INTERVAL) - .await; - panel - .update(cx, |panel, cx| { - panel.show_scrollbar = false; - cx.notify(); - }) - .log_err(); - })) - } - - fn render_scrollbar(&self, window: &Window, cx: &mut Context) -> Option> { - if !Self::should_show_scrollbar(cx) - || !(self.show_scrollbar || self.scrollbar_state.is_dragging()) - || !self.content_mode(window, cx).is_scrollable() - { - return None; - } - - if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() { - return None; - } - - self.scroll_handle.update(self.terminal.read(cx)); - - if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() { - self.terminal.update(cx, |term, _| { - let delta = new_display_offset as i32 - term.last_content.display_offset as i32; - match delta.cmp(&0) { - std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize), - std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize), - std::cmp::Ordering::Equal => {} - } - }); - } - - Some( - div() - .occlude() - .id("terminal-view-scroll") - .on_mouse_move(cx.listener(|_, _, _window, cx| { - cx.notify(); - cx.stop_propagation() - })) - .on_hover(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_any_mouse_down(|_, _window, cx| { - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Left, - cx.listener(|terminal_view, _, window, cx| { - if !terminal_view.scrollbar_state.is_dragging() - && !terminal_view.focus_handle.contains_focused(window, cx) - { - terminal_view.hide_scrollbar(cx); - cx.notify(); - } - cx.stop_propagation(); - }), - ) - .on_scroll_wheel(cx.listener(|_, _, _window, cx| { - cx.notify(); - })) - .absolute() - .top_0() - .bottom_0() - .right_0() - .h_full() - .w(TERMINAL_SCROLLBAR_WIDTH) - .children(Scrollbar::vertical(self.scrollbar_state.clone())), - ) - } - fn rerun_button(task: &TaskState) -> Option { if !task.show_rerun { return None; @@ -1117,6 +981,29 @@ fn regex_search_for_query(query: &project::search::SearchQuery) -> Option &Self { + &Self + } +} + +impl ScrollbarVisibilitySetting for TerminalScrollbarSettingsWrapper { + fn scrollbar_visibility(&self, cx: &App) -> scrollbars::ShowScrollbar { + TerminalSettings::get_global(cx) + .scrollbar + .show + .map(|value| match value { + terminal_settings::ShowScrollbar::Auto => scrollbars::ShowScrollbar::Auto, + terminal_settings::ShowScrollbar::System => scrollbars::ShowScrollbar::System, + terminal_settings::ShowScrollbar::Always => scrollbars::ShowScrollbar::Always, + terminal_settings::ShowScrollbar::Never => scrollbars::ShowScrollbar::Never, + }) + .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) + } +} + impl TerminalView { fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { self.clear_bell(cx); @@ -1148,28 +1035,31 @@ impl TerminalView { terminal.focus_out(); terminal.set_cursor_shape(CursorShape::Hollow); }); - self.hide_scrollbar(cx); cx.notify(); } } impl Render for TerminalView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // TODO: this should be moved out of render + self.scroll_handle.update(self.terminal.read(cx)); + + if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() { + self.terminal.update(cx, |term, _| { + let delta = new_display_offset as i32 - term.last_content.display_offset as i32; + match delta.cmp(&0) { + std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize), + std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize), + std::cmp::Ordering::Equal => {} + } + }); + } + let terminal_handle = self.terminal.clone(); let terminal_view_handle = cx.entity(); let focused = self.focus_handle.is_focused(window); - // Always calculate scrollbar width to prevent layout shift - let scrollbar_width = if Self::should_show_scrollbar(cx) - && self.content_mode(window, cx).is_scrollable() - && self.terminal.read(cx).total_lines() > self.terminal.read(cx).viewport_lines() - { - TERMINAL_SCROLLBAR_WIDTH - } else { - px(0.) - }; - div() .id("terminal-view") .size_full() @@ -1206,21 +1096,12 @@ impl Render for TerminalView { } }), ) - .on_hover(cx.listener(|this, hovered, window, cx| { - if *hovered { - this.show_scrollbar = true; - this.hide_scrollbar_task.take(); - cx.notify(); - } else if !this.focus_handle.contains_focused(window, cx) { - this.hide_scrollbar(cx); - } - })) .child( // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu div() + .id("terminal-view-container") .size_full() .bg(cx.theme().colors().editor_background) - .when(scrollbar_width > px(0.), |div| div.pr(scrollbar_width)) .child(TerminalElement::new( terminal_handle, terminal_view_handle, @@ -1231,8 +1112,15 @@ impl Render for TerminalView { self.block_below_cursor.clone(), self.mode.clone(), )) - .when_some(self.render_scrollbar(window, cx), |div, scrollbar| { - div.child(scrollbar) + .when(self.content_mode(window, cx).is_scrollable(), |div| { + div.custom_scrollbars( + Scrollbars::for_settings::() + .show_along(ScrollAxes::Vertical) + .with_track_along(ScrollAxes::Vertical) + .tracked_scroll_handle(self.scroll_handle.clone()), + window, + cx, + ) }), ) .children(self.context_menu.as_ref().map(|(menu, position, _)| { diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 1c28942490..ed43c5277a 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -119,7 +119,7 @@ impl Render for OnboardingBanner { h_flex() .h_full() .gap_1() - .child(Icon::new(self.details.icon_name).size(IconSize::XSmall)) + .child(Icon::new(self.details.icon_name).size(IconSize::Small)) .child( h_flex() .gap_0p5() diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ad64dac9c6..b84a2800b6 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -275,11 +275,11 @@ impl TitleBar { let banner = cx.new(|cx| { OnboardingBanner::new( - "ACP Onboarding", - IconName::Sparkle, - "Bring Your Own Agent", - Some("Introducing:".into()), - zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(), + "Debugger Onboarding", + IconName::Debug, + "The Debugger", + None, + zed_actions::debugger::OpenOnboardingModal.boxed_clone(), cx, ) }); diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index c047291772..985a2bcdc7 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -21,6 +21,7 @@ gpui_macros.workspace = true icons.workspace = true itertools.workspace = true menu.workspace = true +schemars.workspace = true serde.workspace = true settings.workspace = true smallvec.workspace = true diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 6e552ddcee..09c3bbeb94 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -13,9 +13,6 @@ use crate::prelude::*; )] #[strum(serialize_all = "snake_case")] pub enum VectorName { - AcpGrid, - AcpLogo, - AcpLogoSerif, AiGrid, DebuggerGrid, Grid, diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 81817045dc..1e7bb40c40 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,8 +1,8 @@ use crate::PlatformStyle; use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, - Modifiers, Window, relative, + Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window, + relative, }; use itertools::Itertools; @@ -13,7 +13,7 @@ pub struct KeyBinding { /// More than one keystroke produces a chord. /// /// This should always contain at least one keystroke. - pub keystrokes: Vec, + pub keystrokes: Vec, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, @@ -59,7 +59,7 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(keystrokes: Vec, cx: &App) -> Self { + pub fn new(keystrokes: Vec, cx: &App) -> Self { Self { keystrokes, platform_style: PlatformStyle::platform(), @@ -99,16 +99,16 @@ impl KeyBinding { } fn render_key( - key: &str, + keystroke: &Keystroke, color: Option, platform_style: PlatformStyle, size: impl Into>, ) -> AnyElement { - let key_icon = icon_for_key(key, platform_style); + let key_icon = icon_for_key(keystroke, platform_style); match key_icon { Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), None => { - let key = util::capitalize(key); + let key = util::capitalize(&keystroke.key); Key::new(&key, color).size(size).into_any_element() } } @@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding { "KEY_BINDING-{}", self.keystrokes .iter() - .map(|k| k.display_key.to_string()) + .map(|k| k.key.to_string()) .collect::>() .join(" ") ) @@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding { .py_0p5() .rounded_xs() .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( + .children(render_keystroke( keystroke, color, self.size, @@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding { } } -pub fn render_keybinding_keystroke( - keystroke: &KeybindingKeystroke, +pub fn render_keystroke( + keystroke: &Keystroke, color: Option, size: impl Into>, platform_style: PlatformStyle, @@ -163,39 +163,26 @@ pub fn render_keybinding_keystroke( let size = size.into(); if use_text { - let element = Key::new( - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_mode, - ), - color, - ) - .size(size) - .into_any_element(); + let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) + .size(size) + .into_any_element(); vec![element] } else { let mut elements = Vec::new(); elements.extend(render_modifiers( - &keystroke.display_modifiers, + &keystroke.modifiers, platform_style, color, size, true, )); - elements.push(render_key( - &keystroke.display_key, - color, - platform_style, - size, - )); + elements.push(render_key(keystroke, color, platform_style, size)); elements } } -fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { - match key { +fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { + match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), "right" => Some(IconName::ArrowRight), "up" => Some(IconName::ArrowUp), @@ -392,7 +379,7 @@ impl KeyIcon { /// Returns a textual representation of the key binding for the given [`Action`]. pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx)) + Some(text_for_keystrokes(key_binding.keystrokes(), cx)) } pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { @@ -400,50 +387,22 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { let vim_enabled = cx.try_global::().is_some(); keystrokes .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - platform_style, - vim_enabled, - ) - }) + .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) .join(" ") } -pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { +pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { let platform_style = PlatformStyle::platform(); let vim_enabled = cx.try_global::().is_some(); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = cx.try_global::().is_some(); - keystroke_text(modifiers, key, platform_style, vim_enabled) + keystroke_text(keystroke, platform_style, vim_enabled) } /// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text( - modifiers: &Modifiers, - key: &str, - platform_style: PlatformStyle, - vim_mode: bool, -) -> String { +fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String { let mut text = String::new(); let delimiter = '-'; - if modifiers.function { + if keystroke.modifiers.function { match vim_mode { false => text.push_str("Fn"), true => text.push_str("fn"), @@ -452,7 +411,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.control { + if keystroke.modifiers.control { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Control"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), @@ -462,7 +421,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.platform { + if keystroke.modifiers.platform { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Command"), (PlatformStyle::Mac, true) => text.push_str("cmd"), @@ -475,7 +434,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.alt { + if keystroke.modifiers.alt { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Option"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), @@ -485,7 +444,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.shift { + if keystroke.modifiers.shift { match (platform_style, vim_mode) { (_, false) => text.push_str("Shift"), (_, true) => text.push_str("shift"), @@ -494,9 +453,9 @@ fn keystroke_text( } if vim_mode { - text.push_str(key) + text.push_str(&keystroke.key) } else { - let key = match key { + let key = match keystroke.key.as_str() { "pageup" => "PageUp", "pagedown" => "PageDown", key => &util::capitalize(key), @@ -603,11 +562,9 @@ mod tests { #[test] fn test_text_for_keystroke() { - let keystroke = Keystroke::parse("cmd-c").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac, false ), @@ -615,8 +572,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux, false ), @@ -624,19 +580,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows, false ), "Win-C".to_string() ); - let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Mac, false ), @@ -644,8 +597,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Linux, false ), @@ -653,19 +605,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Windows, false ), "Ctrl-Alt-Delete".to_string() ); - let keystroke = Keystroke::parse("shift-pageup").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Mac, false ), @@ -673,8 +622,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Linux, false, ), @@ -682,8 +630,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Windows, false ), diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 605028202f..264fee566e 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -1,39 +1,786 @@ -use std::{ - any::Any, - cell::{Cell, RefCell}, - fmt::Debug, - ops::Range, - rc::Rc, - sync::Arc, - time::Duration, -}; +use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Not, time::Duration}; -use crate::{IntoElement, prelude::*, px, relative}; use gpui::{ - Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners, CursorStyle, - Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, - IsZero, LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Point, ScrollHandle, ScrollWheelEvent, Size, Style, Task, UniformListScrollHandle, Window, - quad, + Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context, + Corner, Corners, CursorStyle, Div, Edges, Element, ElementId, Entity, EntityId, + GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, + LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate, + ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful, + StatefulInteractiveElement, Style, Styled, Task, UniformList, UniformListDecoration, + UniformListScrollHandle, Window, prelude::FluentBuilder as _, px, quad, relative, size, }; +use settings::SettingsStore; +use smallvec::SmallVec; +use theme::ActiveTheme as _; +use util::ResultExt; -pub struct Scrollbar { - thumb: Range, - state: ScrollbarState, - kind: ScrollbarAxis, +use std::ops::Range; + +use crate::scrollbars::{ScrollbarVisibilitySetting, ShowScrollbar}; + +const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_millis(1500); +const SCROLLBAR_PADDING: Pixels = px(4.); + +pub mod scrollbars { + use gpui::{App, Global}; + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use settings::Settings; + + /// When to show the scrollbar in the editor. + /// + /// Default: auto + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] + #[serde(rename_all = "snake_case")] + pub enum ShowScrollbar { + /// Show the scrollbar if there's important information or + /// follow the system's configured behavior. + Auto, + /// Match the system's configured behavior. + System, + /// Always show the scrollbar. + Always, + /// Never show the scrollbar. + Never, + } + + impl ShowScrollbar { + pub(super) fn show(&self) -> bool { + !matches!(self, Self::Never) + } + + pub(super) fn should_auto_hide(&self, cx: &mut App) -> bool { + matches!(self, Self::System | Self::Auto if cx.default_global::().should_hide()) + } + } + + pub trait GlobalValue { + fn get_value(cx: &App) -> &Self; + } + + impl GlobalValue for T { + fn get_value(cx: &App) -> &T { + T::get_global(cx) + } + } + + impl GlobalValue for ShowScrollbar { + fn get_value(_cx: &App) -> &Self { + &ShowScrollbar::Always + } + } + + pub trait ScrollbarVisibilitySetting: GlobalValue + 'static { + fn scrollbar_visibility(&self, cx: &App) -> ShowScrollbar; + } + + impl ScrollbarVisibilitySetting for ShowScrollbar { + fn scrollbar_visibility(&self, cx: &App) -> ShowScrollbar { + *ShowScrollbar::get_value(cx) + } + } + + #[derive(Default)] + pub struct ScrollbarAutoHide(pub bool); + + impl ScrollbarAutoHide { + pub fn should_hide(&self) -> bool { + self.0 + } + } + + impl Global for ScrollbarAutoHide {} } -#[derive(Default, Debug, Clone, Copy)] +fn get_scrollbar_state( + mut config: Scrollbars, + caller_location: &'static std::panic::Location, + window: &mut Window, + cx: &mut App, +) -> Entity> +where + S: ScrollbarVisibilitySetting, + T: ScrollableHandle, +{ + let element_id = config.id.take().unwrap_or_else(|| caller_location.into()); + + window.use_keyed_state(element_id, cx, |window, cx| { + let parent_id = cx.entity_id(); + ScrollbarStateWrapper( + cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, window, cx)), + ) + }) +} + +pub trait WithScrollbar: Sized { + type Output; + + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibilitySetting, + T: ScrollableHandle; + + #[track_caller] + fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { + self.custom_scrollbars( + Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()), + window, + cx, + ) + } + + #[track_caller] + fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { + self.custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()), + window, + cx, + ) + } + + #[track_caller] + fn vertical_scrollbar_for( + self, + scroll_handle: ScrollHandle, + window: &mut Window, + cx: &mut App, + ) -> Self::Output { + self.custom_scrollbars( + Scrollbars::new(ScrollAxes::Vertical) + .tracked_scroll_handle(scroll_handle) + .ensure_id(core::panic::Location::caller()), + window, + cx, + ) + } +} + +impl WithScrollbar for Stateful
{ + type Output = Self; + + #[track_caller] + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibilitySetting, + T: ScrollableHandle, + { + render_scrollbar( + get_scrollbar_state(config, std::panic::Location::caller(), window, cx), + self, + cx, + ) + } +} + +impl WithScrollbar for Div { + type Output = Stateful
; + + #[track_caller] + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibilitySetting, + T: ScrollableHandle, + { + let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); + // We know this ID stays consistent as long as the element is rendered for + // consecutive frames, which is sufficient for our use case here + let scrollbar_entity_id = scrollbar.entity_id(); + + render_scrollbar( + scrollbar, + self.id(("track-scroll", scrollbar_entity_id)), + cx, + ) + } +} + +fn render_scrollbar( + scrollbar: Entity>, + div: Stateful
, + cx: &App, +) -> Stateful
+where + S: ScrollbarVisibilitySetting, + T: ScrollableHandle, +{ + let state = &scrollbar.read(cx).0; + + div.when_some(state.read(cx).handle_to_track(), |this, handle| { + this.track_scroll(handle).when_some( + state.read(cx).visible_axes(), + |this, axes| match axes { + ScrollAxes::Horizontal => this.overflow_x_scroll(), + ScrollAxes::Vertical => this.overflow_y_scroll(), + ScrollAxes::Both => this.overflow_scroll(), + }, + ) + }) + .when_some( + state + .read(cx) + .space_to_reserve_for(ScrollbarAxis::Horizontal), + |this, space| this.pb(space), + ) + .when_some( + state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical), + |this, space| this.pr(space), + ) + .child(state.clone()) +} + +impl UniformListDecoration + for ScrollbarStateWrapper +{ + fn compute( + &self, + _visible_range: Range, + _bounds: Bounds, + scroll_offset: Point, + _item_height: Pixels, + _item_count: usize, + _window: &mut Window, + _cx: &mut App, + ) -> gpui::AnyElement { + ScrollbarElement { + origin: scroll_offset.negate(), + state: self.0.clone(), + } + .into_any() + } +} + +impl WithScrollbar for UniformList { + type Output = Self; + + #[track_caller] + fn custom_scrollbars( + self, + config: Scrollbars, + window: &mut Window, + cx: &mut App, + ) -> Self::Output + where + S: ScrollbarVisibilitySetting, + T: ScrollableHandle, + { + let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); + self.when_some( + scrollbar.read_with(cx, |wrapper, cx| { + wrapper + .0 + .read(cx) + .handle_to_track::() + .cloned() + }), + |this, handle| this.track_scroll(handle), + ) + .with_decoration(scrollbar) + } +} + +#[derive(PartialEq, Eq)] +pub enum ScrollAxes { + Horizontal, + Vertical, + Both, +} + +impl ScrollAxes { + fn apply_to(self, point: Point, value: T) -> Point + where + T: Debug + Default + PartialEq + Clone, + { + match self { + Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value), + Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value), + Self::Both => Point::new(value.clone(), value), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq)] +enum ReservedSpace { + #[default] + None, + Thumb, + Track, +} + +impl ReservedSpace { + fn is_visible(&self) -> bool { + !matches!(self, ReservedSpace::None) + } + + fn needs_scroll_track(&self) -> bool { + matches!(self, ReservedSpace::Track) + } +} + +#[derive(Debug, Default, Clone)] +enum ScrollbarWidth { + #[default] + Normal, + Small, + XSmall, +} + +impl ScrollbarWidth { + fn to_pixels(&self) -> Pixels { + match self { + ScrollbarWidth::Normal => px(8.), + ScrollbarWidth::Small => px(6.), + ScrollbarWidth::XSmall => px(4.), + } + } +} + +pub struct Scrollbars< + S: ScrollbarVisibilitySetting = ShowScrollbar, + T: ScrollableHandle = ScrollHandle, +> { + id: Option, + tracked_setting: PhantomData, + tracked_entity: Option>, + scrollable_handle: Box T>, + handle_was_added: bool, + visibility: Point, + scrollbar_width: ScrollbarWidth, +} + +impl Scrollbars { + pub fn new(show_along: ScrollAxes) -> Self { + Self::new_with_setting(show_along) + } + + pub fn for_settings() -> Scrollbars { + Scrollbars::::new_with_setting(ScrollAxes::Both) + } +} + +impl Scrollbars { + fn new_with_setting(show_along: ScrollAxes) -> Self { + Self { + id: None, + tracked_setting: PhantomData, + handle_was_added: false, + scrollable_handle: Box::new(ScrollHandle::new), + tracked_entity: None, + visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb), + scrollbar_width: ScrollbarWidth::Normal, + } + } +} + +impl + Scrollbars +{ + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + fn ensure_id(mut self, id: impl Into) -> Self { + if self.id.is_none() { + self.id = Some(id.into()); + } + self + } + + /// Notify the current context whenever this scrollbar gets a scroll event + pub fn notify_content(mut self) -> Self { + self.tracked_entity = Some(None); + self + } + + /// Set a parent model which should be notified whenever this scrollbar gets a scroll event. + pub fn tracked_entity(mut self, entity: &Entity) -> Self { + self.tracked_entity = Some(Some(entity.entity_id())); + self + } + + pub fn tracked_scroll_handle( + self, + tracked_scroll_handle: TrackedHandle, + ) -> Scrollbars { + let Self { + id, + tracked_setting, + tracked_entity: tracked_entity_id, + scrollbar_width, + visibility, + .. + } = self; + + Scrollbars { + handle_was_added: true, + scrollable_handle: Box::new(|| tracked_scroll_handle), + id, + tracked_setting, + tracked_entity: tracked_entity_id, + visibility, + scrollbar_width, + } + } + + pub fn show_along(mut self, along: ScrollAxes) -> Self { + self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb); + self + } + + pub fn with_track_along(mut self, along: ScrollAxes) -> Self { + self.visibility = along.apply_to(self.visibility, ReservedSpace::Track); + self + } + + pub fn width_sm(mut self) -> Self { + self.scrollbar_width = ScrollbarWidth::Small; + self + } + + pub fn width_xs(mut self) -> Self { + self.scrollbar_width = ScrollbarWidth::XSmall; + self + } +} + +#[derive(PartialEq, Eq)] +enum VisibilityState { + Visible, + Hidden, + Disabled, +} + +impl VisibilityState { + fn from_show_setting(show_setting: ShowScrollbar) -> Self { + if show_setting.show() { + Self::Visible + } else { + Self::Disabled + } + } + + fn is_visible(&self) -> bool { + matches!(self, VisibilityState::Visible) + } + + #[inline] + fn is_disabled(&self) -> bool { + matches!(self, VisibilityState::Disabled) + } +} + +enum ParentHovered { + Yes(bool), + No(bool), +} + +/// This is used to ensure notifies within the state do not notify the parent +/// unintentionally. +struct ScrollbarStateWrapper( + Entity>, +); + +/// A scrollbar state that should be persisted across frames. +struct ScrollbarState { + thumb_state: ThumbState, + notify_id: Option, + manually_added: bool, + scroll_handle: T, + width: ScrollbarWidth, + tracked_setting: PhantomData, + show_setting: ShowScrollbar, + visibility: Point, + show_state: VisibilityState, + mouse_in_parent: bool, + last_prepaint_state: Option, + _auto_hide_task: Option>, +} + +impl ScrollbarState { + fn new_from_config( + config: Scrollbars, + parent_id: EntityId, + window: &mut Window, + cx: &mut Context, + ) -> Self { + cx.observe_global_in::(window, Self::settings_changed) + .detach(); + + let mut state = ScrollbarState { + thumb_state: Default::default(), + notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)), + manually_added: config.handle_was_added, + scroll_handle: (config.scrollable_handle)(), + width: config.scrollbar_width, + visibility: config.visibility, + tracked_setting: PhantomData, + show_setting: ShowScrollbar::Always, + show_state: VisibilityState::Visible, + mouse_in_parent: true, + last_prepaint_state: None, + _auto_hide_task: None, + }; + state.schedule_auto_hide(window, cx); + state + } + + fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { + self.set_show_scrollbar(S::get_value(cx).scrollbar_visibility(cx), window, cx); + } + + /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet. + fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context) { + if self._auto_hide_task.is_none() { + self._auto_hide_task = + (self.visible() && self.show_setting.should_auto_hide(cx)).then(|| { + cx.spawn_in(window, async move |scrollbar_state, cx| { + cx.background_executor() + .timer(SCROLLBAR_SHOW_INTERVAL) + .await; + scrollbar_state + .update(cx, |state, cx| { + state.set_visibility(VisibilityState::Hidden, cx); + }) + .log_err(); + }) + }); + } + } + + fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { + self.set_visibility(VisibilityState::Visible, cx); + self._auto_hide_task.take(); + self.schedule_auto_hide(window, cx); + } + + fn set_show_scrollbar( + &mut self, + show: ShowScrollbar, + window: &mut Window, + cx: &mut Context, + ) { + if self.show_setting != show { + self.show_setting = show; + self.set_visibility(VisibilityState::from_show_setting(show), cx); + self.schedule_auto_hide(window, cx); + cx.notify(); + } + } + + fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context) { + if self.show_state != visibility { + self.show_state = visibility; + cx.notify(); + } + } + + #[inline] + fn visible_axes(&self) -> Option { + match (&self.visibility.x, &self.visibility.y) { + (ReservedSpace::None, ReservedSpace::None) => None, + (ReservedSpace::None, _) => Some(ScrollAxes::Vertical), + (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal), + _ => Some(ScrollAxes::Both), + } + } + + fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option { + (self.show_state.is_disabled().not() && self.visibility.along(axis).needs_scroll_track()) + .then(|| self.space_to_reserve()) + } + + fn space_to_reserve(&self) -> Pixels { + self.width.to_pixels() + 2 * SCROLLBAR_PADDING + } + + fn handle_to_track(&self) -> Option<&Handle> { + (!self.manually_added) + .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::()) + .flatten() + } + + fn scroll_handle(&self) -> &T { + &self.scroll_handle + } + + fn set_offset(&mut self, offset: Point, cx: &mut Context) { + if self.scroll_handle.offset() != offset { + self.scroll_handle.set_offset(offset); + self.notify_parent(cx); + cx.notify(); + } + } + + fn is_dragging(&self) -> bool { + self.thumb_state.is_dragging() + } + + fn set_dragging( + &mut self, + axis: ScrollbarAxis, + drag_offset: Pixels, + window: &mut Window, + cx: &mut Context, + ) { + self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx); + self.scroll_handle().drag_started(); + } + + fn update_hovered_thumb( + &mut self, + position: &Point, + window: &mut Window, + cx: &mut Context, + ) { + self.set_thumb_state( + if let Some(&ScrollbarLayout { axis, .. }) = self + .last_prepaint_state + .as_ref() + .and_then(|state| state.thumb_for_position(position)) + { + ThumbState::Hover(axis) + } else { + ThumbState::Inactive + }, + window, + cx, + ); + } + + fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context) { + if self.thumb_state != state { + if state == ThumbState::Inactive { + self.schedule_auto_hide(window, cx); + } else { + self.show_scrollbars(window, cx); + } + self.thumb_state = state; + cx.notify(); + } + } + + fn update_parent_hovered(&mut self, position: &Point) -> ParentHovered { + let last_parent_hovered = self.mouse_in_parent; + self.mouse_in_parent = self.parent_hovered(position); + let state_changed = self.mouse_in_parent != last_parent_hovered; + match self.mouse_in_parent { + true => ParentHovered::Yes(state_changed), + false => ParentHovered::No(state_changed), + } + } + + fn parent_hovered(&self, position: &Point) -> bool { + self.last_prepaint_state + .as_ref() + .is_some_and(|state| state.parent_bounds.contains(position)) + } + + fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { + self.last_prepaint_state + .as_ref() + .and_then(|state| state.hit_for_position(position)) + } + + fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> { + self.last_prepaint_state + .as_ref() + .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis)) + } + + fn thumb_ranges( + &self, + ) -> impl Iterator, ReservedSpace)> + '_ { + const MINIMUM_THUMB_SIZE: Pixels = px(25.); + let max_offset = self.scroll_handle().max_offset(); + let viewport_size = self.scroll_handle().viewport().size; + let current_offset = self.scroll_handle().offset(); + + [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical] + .into_iter() + .filter(|&axis| self.visibility.along(axis).is_visible()) + .flat_map(move |axis| { + let max_offset = max_offset.along(axis); + let viewport_size = viewport_size.along(axis); + if max_offset.is_zero() || viewport_size.is_zero() { + return None; + } + let content_size = viewport_size + max_offset; + let visible_percentage = viewport_size / content_size; + let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage); + if thumb_size > viewport_size { + return None; + } + let current_offset = current_offset + .along(axis) + .clamp(-max_offset, Pixels::ZERO) + .abs(); + let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size); + let thumb_percentage_start = start_offset / viewport_size; + let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; + Some(( + axis, + thumb_percentage_start..thumb_percentage_end, + self.visibility.along(axis), + )) + }) + } + + fn visible(&self) -> bool { + self.show_state.is_visible() + } + + #[inline] + fn disabled(&self) -> bool { + self.show_state.is_disabled() + } + + fn notify_parent(&self, cx: &mut App) { + if let Some(entity_id) = self.notify_id { + cx.notify(entity_id); + } + } +} + +impl Render for ScrollbarState { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + ScrollbarElement { + state: cx.entity(), + origin: Default::default(), + } + } +} + +struct ScrollbarElement { + origin: Point, + state: Entity>, +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] enum ThumbState { #[default] Inactive, - Hover, - Dragging(Pixels), + Hover(ScrollbarAxis), + Dragging(ScrollbarAxis, Pixels), } impl ThumbState { fn is_dragging(&self) -> bool { - matches!(*self, ThumbState::Dragging(_)) + matches!(*self, ThumbState::Dragging(..)) } } @@ -99,170 +846,112 @@ impl ScrollableHandle for ScrollHandle { } } -pub trait ScrollableHandle: Any + Debug { - fn content_size(&self) -> Size { - self.viewport().size + self.max_offset() - } +pub trait ScrollableHandle: 'static + Any + Sized { fn max_offset(&self) -> Size; fn set_offset(&self, point: Point); fn offset(&self) -> Point; fn viewport(&self) -> Bounds; fn drag_started(&self) {} fn drag_ended(&self) {} -} -/// A scrollbar state that should be persisted across frames. -#[derive(Clone, Debug)] -pub struct ScrollbarState { - thumb_state: Rc>, - parent_id: Option, - scroll_handle: Arc, - auto_hide: Rc>, -} - -#[derive(Debug)] -enum AutoHide { - Disabled, - Hidden { - parent_id: EntityId, - }, - Visible { - parent_id: EntityId, - _task: Task<()>, - }, -} - -impl AutoHide { - fn is_hidden(&self) -> bool { - matches!(self, AutoHide::Hidden { .. }) + fn scrollable_along(&self, axis: ScrollbarAxis) -> bool { + self.max_offset().along(axis) > Pixels::ZERO + } + fn content_size(&self) -> Size { + self.viewport().size + self.max_offset() } } -impl ScrollbarState { - pub fn new(scroll: impl ScrollableHandle) -> Self { - Self { - thumb_state: Default::default(), - parent_id: None, - scroll_handle: Arc::new(scroll), - auto_hide: Rc::new(RefCell::new(AutoHide::Disabled)), - } - } +enum ScrollbarMouseEvent { + TrackClick, + ThumbDrag(Pixels), +} - /// Set a parent model which should be notified whenever this Scrollbar gets a scroll event. - pub fn parent_entity(mut self, v: &Entity) -> Self { - self.parent_id = Some(v.entity_id()); - self - } +#[derive(Clone)] +struct ScrollbarLayout { + thumb_bounds: Bounds, + track_bounds: Bounds, + cursor_hitbox: Hitbox, + reserved_space: ReservedSpace, + axis: ScrollbarAxis, +} - pub fn scroll_handle(&self) -> &Arc { - &self.scroll_handle - } +impl ScrollbarLayout { + fn compute_click_offset( + &self, + event_position: Point, + max_offset: Size, + event_type: ScrollbarMouseEvent, + ) -> Pixels { + let Self { + track_bounds, + thumb_bounds, + axis, + .. + } = self; + let axis = *axis; - pub fn is_dragging(&self) -> bool { - matches!(self.thumb_state.get(), ThumbState::Dragging(_)) - } - - fn set_dragging(&self, drag_offset: Pixels) { - self.set_thumb_state(ThumbState::Dragging(drag_offset)); - self.scroll_handle.drag_started(); - } - - fn set_thumb_hovered(&self, hovered: bool) { - self.set_thumb_state(if hovered { - ThumbState::Hover - } else { - ThumbState::Inactive - }); - } - - fn set_thumb_state(&self, state: ThumbState) { - self.thumb_state.set(state); - } - - fn thumb_range(&self, axis: ScrollbarAxis) -> Option> { - const MINIMUM_THUMB_SIZE: Pixels = px(25.); - let max_offset = self.scroll_handle.max_offset().along(axis); - let viewport_size = self.scroll_handle.viewport().size.along(axis); - if max_offset.is_zero() || viewport_size.is_zero() { - return None; - } - let content_size = viewport_size + max_offset; - let visible_percentage = viewport_size / content_size; - let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage); - if thumb_size > viewport_size { - return None; - } - let current_offset = self - .scroll_handle - .offset() - .along(axis) - .clamp(-max_offset, Pixels::ZERO) - .abs(); - let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size); - let thumb_percentage_start = start_offset / viewport_size; - let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; - Some(thumb_percentage_start..thumb_percentage_end) - } - - fn show_temporarily(&self, parent_id: EntityId, cx: &mut App) { - const SHOW_INTERVAL: Duration = Duration::from_secs(1); - - let auto_hide = self.auto_hide.clone(); - auto_hide.replace(AutoHide::Visible { - parent_id, - _task: cx.spawn({ - let this = auto_hide.clone(); - async move |cx| { - cx.background_executor().timer(SHOW_INTERVAL).await; - this.replace(AutoHide::Hidden { parent_id }); - cx.update(|cx| { - cx.notify(parent_id); - }) - .ok(); - } - }), - }); - } - - fn unhide(&self, position: &Point, cx: &mut App) { - let parent_id = match &*self.auto_hide.borrow() { - AutoHide::Disabled => return, - AutoHide::Hidden { parent_id } => *parent_id, - AutoHide::Visible { parent_id, _task } => *parent_id, + let viewport_size = track_bounds.size.along(axis); + let thumb_size = thumb_bounds.size.along(axis); + let thumb_offset = match event_type { + ScrollbarMouseEvent::TrackClick => thumb_size / 2., + ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset, }; - if self.scroll_handle().viewport().contains(position) { - self.show_temporarily(parent_id, cx); - } + let thumb_start = + (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset) + .clamp(px(0.), viewport_size - thumb_size); + + let max_offset = max_offset.along(axis); + let percentage = if viewport_size > thumb_size { + thumb_start / (viewport_size - thumb_size) + } else { + 0. + }; + + -max_offset * percentage } } -impl Scrollbar { - pub fn vertical(state: ScrollbarState) -> Option { - Self::new(state, ScrollbarAxis::Vertical) - } - - pub fn horizontal(state: ScrollbarState) -> Option { - Self::new(state, ScrollbarAxis::Horizontal) - } - - fn new(state: ScrollbarState, kind: ScrollbarAxis) -> Option { - let thumb = state.thumb_range(kind)?; - Some(Self { thumb, state, kind }) - } - - /// Automatically hide the scrollbar when idle - pub fn auto_hide(self, cx: &mut Context) -> Self { - if matches!(*self.state.auto_hide.borrow(), AutoHide::Disabled) { - self.state.show_temporarily(cx.entity_id(), cx); - } - self +impl PartialEq for ScrollbarLayout { + fn eq(&self, other: &Self) -> bool { + self.axis == other.axis && self.thumb_bounds == other.thumb_bounds } } -impl Element for Scrollbar { +#[derive(Clone)] +pub struct ScrollbarPrepaintState { + parent_bounds: Bounds, + thumbs: SmallVec<[ScrollbarLayout; 2]>, +} + +impl ScrollbarPrepaintState { + fn thumb_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { + self.thumbs + .iter() + .find(|info| info.thumb_bounds.contains(position)) + } + + fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { + self.thumbs.iter().find(|info| { + if info.reserved_space.needs_scroll_track() { + info.track_bounds.contains(position) + } else { + info.thumb_bounds.contains(position) + } + }) + } +} + +impl PartialEq for ScrollbarPrepaintState { + fn eq(&self, other: &Self) -> bool { + self.thumbs == other.thumbs + } +} + +impl Element for ScrollbarElement { type RequestLayoutState = (); - type PrepaintState = Hitbox; + type PrepaintState = Option; fn id(&self) -> Option { None @@ -279,19 +968,14 @@ impl Element for Scrollbar { window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.flex_grow = 1.; - style.flex_shrink = 1.; + let scrollbar_style = Style { + position: Position::Absolute, + inset: Edges::default(), + size: size(relative(1.), relative(1.)).map(Into::into), + ..Default::default() + }; - if self.kind == ScrollbarAxis::Vertical { - style.size.width = px(12.).into(); - style.size.height = relative(1.).into(); - } else { - style.size.width = relative(1.).into(); - style.size.height = px(12.).into(); - } - - (window.request_layout(style, None, cx), ()) + (window.request_layout(scrollbar_style, None, cx), ()) } fn prepaint( @@ -301,203 +985,290 @@ impl Element for Scrollbar { bounds: Bounds, _request_layout: &mut Self::RequestLayoutState, window: &mut Window, - _: &mut App, + cx: &mut App, ) -> Self::PrepaintState { - window.with_content_mask(Some(ContentMask { bounds }), |window| { - window.insert_hitbox(bounds, HitboxBehavior::Normal) - }) + let prepaint_state = self + .state + .read(cx) + .disabled() + .not() + .then(|| ScrollbarPrepaintState { + parent_bounds: bounds, + thumbs: { + let thumb_ranges = self.state.read(cx).thumb_ranges().collect::>(); + let width = self.state.read(cx).width.to_pixels(); + + let additional_padding = if thumb_ranges.len() == 2 { + width + } else { + Pixels::ZERO + }; + + thumb_ranges + .into_iter() + .map(|(axis, thumb_range, reserved_space)| { + let track_anchor = match axis { + ScrollbarAxis::Horizontal => Corner::BottomLeft, + ScrollbarAxis::Vertical => Corner::TopRight, + }; + let Bounds { origin, size } = Bounds::from_corner_and_size( + track_anchor, + bounds + .corner(track_anchor) + .apply_along(axis.invert(), |corner| { + corner - SCROLLBAR_PADDING + }), + bounds.size.apply_along(axis.invert(), |_| width), + ); + let scroll_track_bounds = Bounds::new(self.origin + origin, size); + + let padded_bounds = scroll_track_bounds.extend(match axis { + ScrollbarAxis::Horizontal => Edges { + right: -SCROLLBAR_PADDING, + left: -SCROLLBAR_PADDING, + ..Default::default() + }, + ScrollbarAxis::Vertical => Edges { + top: -SCROLLBAR_PADDING, + bottom: -SCROLLBAR_PADDING, + ..Default::default() + }, + }); + + let available_space = + padded_bounds.size.along(axis) - additional_padding; + + let thumb_offset = thumb_range.start * available_space; + let thumb_end = thumb_range.end * available_space; + let thumb_bounds = Bounds::new( + padded_bounds + .origin + .apply_along(axis, |origin| origin + thumb_offset), + padded_bounds + .size + .apply_along(axis, |_| thumb_end - thumb_offset), + ); + + ScrollbarLayout { + thumb_bounds, + track_bounds: padded_bounds, + axis, + cursor_hitbox: window.insert_hitbox( + if reserved_space.needs_scroll_track() { + padded_bounds + } else { + thumb_bounds + }, + HitboxBehavior::BlockMouseExceptScroll, + ), + reserved_space, + } + }) + .collect() + }, + }); + if prepaint_state + .as_ref() + .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref()) + { + self.state + .update(cx, |state, cx| state.show_scrollbars(window, cx)); + } + + prepaint_state } fn paint( &mut self, _id: Option<&GlobalElementId>, _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, + Bounds { origin, size }: Bounds, _request_layout: &mut Self::RequestLayoutState, - hitbox: &mut Self::PrepaintState, + prepaint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App, ) { - const EXTRA_PADDING: Pixels = px(5.0); + let Some(prepaint_state) = prepaint_state.take() else { + return; + }; + + let bounds = Bounds::new(self.origin + origin, size); window.with_content_mask(Some(ContentMask { bounds }), |window| { - let axis = self.kind; let colors = cx.theme().colors(); - let thumb_state = self.state.thumb_state.get(); - let thumb_base_color = match thumb_state { - ThumbState::Dragging(_) => colors.scrollbar_thumb_active_background, - ThumbState::Hover => colors.scrollbar_thumb_hover_background, - ThumbState::Inactive => colors.scrollbar_thumb_background, - }; - let thumb_background = colors.surface_background.blend(thumb_base_color); - - let padded_bounds = Bounds::from_corners( - bounds - .origin - .apply_along(axis, |origin| origin + EXTRA_PADDING), - bounds - .bottom_right() - .apply_along(axis, |track_end| track_end - 3.0 * EXTRA_PADDING), - ); - - let thumb_offset = self.thumb.start * padded_bounds.size.along(axis); - let thumb_end = self.thumb.end * padded_bounds.size.along(axis); - - let thumb_bounds = Bounds::new( - padded_bounds - .origin - .apply_along(axis, |origin| origin + thumb_offset), - padded_bounds - .size - .apply_along(axis, |_| thumb_end - thumb_offset) - .apply_along(axis.invert(), |width| width / 1.5), - ); - - if thumb_state.is_dragging() || !self.state.auto_hide.borrow().is_hidden() { - let corners = Corners::all(thumb_bounds.size.along(axis.invert()) / 2.0); - - window.paint_quad(quad( + if self.state.read(cx).visible() { + for ScrollbarLayout { thumb_bounds, - corners, - thumb_background, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); - } - - if thumb_state.is_dragging() { - window.set_window_cursor_style(CursorStyle::Arrow); - } else { - window.set_cursor_style(CursorStyle::Arrow, hitbox); - } - - enum ScrollbarMouseEvent { - GutterClick, - ThumbDrag(Pixels), - } - - let compute_click_offset = - move |event_position: Point, - max_offset: Size, - event_type: ScrollbarMouseEvent| { - let viewport_size = padded_bounds.size.along(axis); - - let thumb_size = thumb_bounds.size.along(axis); - - let thumb_offset = match event_type { - ScrollbarMouseEvent::GutterClick => thumb_size / 2., - ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset, + cursor_hitbox, + axis, + reserved_space, + .. + } in &prepaint_state.thumbs + { + const MAXIMUM_OPACITY: f32 = 0.7; + let thumb_state = self.state.read(cx).thumb_state; + let (thumb_base_color, hovered) = match thumb_state { + ThumbState::Dragging(dragged_axis, _) if dragged_axis == *axis => { + (colors.scrollbar_thumb_active_background, false) + } + ThumbState::Hover(hovered_axis) if hovered_axis == *axis => { + (colors.scrollbar_thumb_hover_background, true) + } + _ => (colors.scrollbar_thumb_background, false), }; - let thumb_start = (event_position.along(axis) - - padded_bounds.origin.along(axis) - - thumb_offset) - .clamp(px(0.), viewport_size - thumb_size); - - let max_offset = max_offset.along(axis); - let percentage = if viewport_size > thumb_size { - thumb_start / (viewport_size - thumb_size) + let blending_color = if hovered || reserved_space.needs_scroll_track() { + colors.surface_background } else { - 0. + let blend_color = colors.surface_background; + blend_color.min(blend_color.alpha(MAXIMUM_OPACITY)) }; - -max_offset * percentage - }; + let thumb_background = blending_color.blend(thumb_base_color); + + window.paint_quad(quad( + *thumb_bounds, + Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size), + thumb_background, + Edges::default(), + Hsla::transparent_black(), + BorderStyle::default(), + )); + + if thumb_state.is_dragging() { + window.set_window_cursor_style(CursorStyle::Arrow); + } else { + window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox); + } + } + } + + self.state.update(cx, |state, _| { + state.last_prepaint_state = Some(prepaint_state) + }); window.on_mouse_event({ let state = self.state.clone(); - move |event: &MouseDownEvent, phase, _, _| { - if !phase.bubble() - || event.button != MouseButton::Left - || !bounds.contains(&event.position) - { + + move |event: &MouseDownEvent, phase, window, cx| { + state.update(cx, |state, cx| { + let Some(scrollbar_layout) = (phase.capture() + && event.button == MouseButton::Left) + .then(|| state.hit_for_position(&event.position)) + .flatten() + else { + return; + }; + + let ScrollbarLayout { + thumb_bounds, axis, .. + } = scrollbar_layout; + + if thumb_bounds.contains(&event.position) { + let offset = + event.position.along(*axis) - thumb_bounds.origin.along(*axis); + state.set_dragging(*axis, offset, window, cx); + } else { + let scroll_handle = state.scroll_handle(); + let click_offset = scrollbar_layout.compute_click_offset( + event.position, + scroll_handle.max_offset(), + ScrollbarMouseEvent::TrackClick, + ); + state.set_offset( + scroll_handle.offset().apply_along(*axis, |_| click_offset), + cx, + ); + }; + + cx.stop_propagation(); + }); + } + }); + + window.on_mouse_event({ + let state = self.state.clone(); + + move |event: &ScrollWheelEvent, phase, window, cx| { + if phase.capture() { + state.update(cx, |state, cx| { + state.update_hovered_thumb(&event.position, window, cx) + }); + } + } + }); + + window.on_mouse_event({ + let state = self.state.clone(); + + move |event: &MouseMoveEvent, phase, window, cx| { + if !phase.capture() { return; } - if thumb_bounds.contains(&event.position) { - let offset = event.position.along(axis) - thumb_bounds.origin.along(axis); - state.set_dragging(offset); - } else { - let scroll_handle = state.scroll_handle(); - let click_offset = compute_click_offset( - event.position, - scroll_handle.max_offset(), - ScrollbarMouseEvent::GutterClick, - ); - scroll_handle - .set_offset(scroll_handle.offset().apply_along(axis, |_| click_offset)); - } - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - let scroll_handle = self.state.scroll_handle().clone(); - move |event: &ScrollWheelEvent, phase, window, cx| { - if phase.bubble() { - state.unhide(&event.position, cx); - - if bounds.contains(&event.position) { - let current_offset = scroll_handle.offset(); - scroll_handle.set_offset( - current_offset + event.delta.pixel_delta(window.line_height()), - ); - } - } - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - move |event: &MouseMoveEvent, phase, window, cx| { - if phase.bubble() { - state.unhide(&event.position, cx); - - match state.thumb_state.get() { - ThumbState::Dragging(drag_state) if event.dragging() => { - let scroll_handle = state.scroll_handle(); - let drag_offset = compute_click_offset( + match state.read(cx).thumb_state { + ThumbState::Dragging(axis, drag_state) if event.dragging() => { + if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) { + let scroll_handle = state.read(cx).scroll_handle(); + let drag_offset = scrollbar_layout.compute_click_offset( event.position, scroll_handle.max_offset(), ScrollbarMouseEvent::ThumbDrag(drag_state), ); - scroll_handle.set_offset( - scroll_handle.offset().apply_along(axis, |_| drag_offset), - ); - window.refresh(); - if let Some(id) = state.parent_id { - cx.notify(id); - } + let new_offset = + scroll_handle.offset().apply_along(axis, |_| drag_offset); + + state.update(cx, |state, cx| state.set_offset(new_offset, cx)); + cx.stop_propagation(); } - _ if event.pressed_button.is_none() => { - state.set_thumb_hovered(thumb_bounds.contains(&event.position)) - } - _ => {} } + _ => state.update(cx, |state, cx| { + match state.update_parent_hovered(&event.position) { + ParentHovered::Yes(state_changed) + if event.pressed_button.is_none() => + { + if state_changed { + state.show_scrollbars(window, cx); + } + state.update_hovered_thumb(&event.position, window, cx); + cx.stop_propagation(); + } + ParentHovered::No(state_changed) if state_changed => { + state.set_thumb_state(ThumbState::Inactive, window, cx); + } + _ => {} + } + }), } } }); window.on_mouse_event({ let state = self.state.clone(); - move |event: &MouseUpEvent, phase, _, cx| { - if phase.bubble() { + move |event: &MouseUpEvent, phase, window, cx| { + if !phase.capture() { + return; + } + + state.update(cx, |state, cx| { if state.is_dragging() { state.scroll_handle().drag_ended(); - if let Some(id) = state.parent_id { - cx.notify(id); - } } - state.set_thumb_hovered(thumb_bounds.contains(&event.position)); - } + + if !state.parent_hovered(&event.position) { + state.schedule_auto_hide(window, cx); + return; + } + + state.update_hovered_thumb(&event.position, window, cx); + }); } }); }) } } -impl IntoElement for Scrollbar { +impl IntoElement for ScrollbarElement { type Element = Self; fn into_element(self) -> Self::Element { diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 726022021d..2bc531268d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,8 +23,6 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, - /// Goes to the location of the last modification. - HelixGotoLastModification, ] ); @@ -33,7 +31,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); - Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -433,15 +430,6 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } - - pub fn helix_goto_last_modification( - &mut self, - _: &HelixGotoLastModification, - window: &mut Window, - cx: &mut Context, - ) { - self.jump(".".into(), false, false, window, cx); - } } #[cfg(test)] @@ -453,7 +441,6 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // « // ˇ // » @@ -515,7 +502,6 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test delete a selection cx.set_state( @@ -596,7 +582,6 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" @@ -650,7 +635,6 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -668,7 +652,6 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -691,7 +674,6 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -734,7 +716,6 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -782,72 +763,4 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } - #[gpui::test] - async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // First copy some text to clipboard - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test paste with shift-r on single cursor - cx.set_state("foo ˇbar", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); - - // Test paste with shift-r on selection - cx.set_state("foo «barˇ» baz", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("ˇhello", Mode::HelixNormal); - assert_eq!(cx.mode(), Mode::HelixNormal); - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::HelixNormal); - } - - #[gpui::test] - async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("escape"); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("m o d i f i e d space"); - cx.simulate_keystrokes("escape"); - - // TODO: this fails, because state is no longer helix - cx.assert_state( - "line one\nline modified ˇtwo\nline three", - Mode::HelixNormal, - ); - - // Move cursor away from the modification - cx.simulate_keystrokes("up"); - - // Use "g ." to go back to last modification - cx.simulate_keystrokes("g ."); - - // Verify we're back at the modification location and still in HelixNormal mode - cx.assert_state( - "line one\nline modifiedˇ two\nline three", - Mode::HelixNormal, - ); - } } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index dba003ec5f..4fbeec7236 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -203,10 +203,7 @@ impl Vim { // hook into the existing to clear out any vim search state on cmd+f or edit -> find. fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context) { - // Preserve the current mode when resetting search state - let current_mode = self.mode; self.search = Default::default(); - self.search.prior_mode = current_mode; cx.propagate(); } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index fe4bc7433d..c0176cb12c 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,10 +7,8 @@ use crate::{motion::Motion, object::Object}; use anyhow::Result; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; -use db::{ - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::define_connection; +use db::sqlez_macros::sql; use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; use gpui::{ @@ -1670,12 +1668,8 @@ impl MarksView { } } -pub struct VimDb(ThreadSafeConnection); - -impl Domain for VimDb { - const NAME: &str = stringify!(VimDb); - - const MIGRATIONS: &[&str] = &[ +define_connection! ( + pub static ref DB: VimDb = &[ sql! ( CREATE TABLE vim_marks ( workspace_id INTEGER, @@ -1695,9 +1689,7 @@ impl Domain for VimDb { ON vim_global_marks_paths(workspace_id, mark_name); ), ]; -} - -db::static_connection!(DB, VimDb, [WorkspaceDb]); +); struct SerializedMark { path: Arc, diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs index b8c0db29d3..b017373474 100644 --- a/crates/workspace/src/invalid_buffer_view.rs +++ b/crates/workspace/src/invalid_buffer_view.rs @@ -1,4 +1,4 @@ -use std::{path::Path, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use gpui::{EventEmitter, FocusHandle, Focusable}; use ui::{ @@ -12,7 +12,7 @@ use crate::Item; /// A view to display when a certain buffer fails to open. pub struct InvalidBufferView { /// Which path was attempted to open. - pub abs_path: Arc, + pub abs_path: Arc, /// An error message, happened when opening the buffer. pub error: SharedString, is_local: bool, @@ -21,7 +21,7 @@ pub struct InvalidBufferView { impl InvalidBufferView { pub fn new( - abs_path: &Path, + abs_path: PathBuf, is_local: bool, e: &anyhow::Error, _: &mut Window, @@ -29,7 +29,7 @@ impl InvalidBufferView { ) -> Self { Self { is_local, - abs_path: Arc::from(abs_path), + abs_path: Arc::new(abs_path), error: format!("{e}").into(), focus_handle: cx.focus_handle(), } @@ -43,7 +43,7 @@ impl Item for InvalidBufferView { // Ensure we always render at least the filename. detail += 1; - let path = self.abs_path.as_ref(); + let path = self.abs_path.as_path(); let mut prefix = path; while detail > 0 { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index db91bd82b9..3485fcca43 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -23,7 +23,7 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::Path, + path::PathBuf, rc::Rc, sync::Arc, time::Duration, @@ -1168,7 +1168,7 @@ pub trait ProjectItem: Item { /// with the error from that failure as an argument. /// Allows to open an item that can gracefully display and handle errors. fn for_broken_project_item( - _abs_path: &Path, + _abs_path: PathBuf, _is_local: bool, _e: &anyhow::Error, _window: &mut Window, diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs index cf463e6b22..4f9ed42312 100644 --- a/crates/workspace/src/path_list.rs +++ b/crates/workspace/src/path_list.rs @@ -58,7 +58,11 @@ impl PathList { let mut paths: Vec = if serialized.paths.is_empty() { Vec::new() } else { - serialized.paths.split('\n').map(PathBuf::from).collect() + serde_json::from_str::>(&serialized.paths) + .unwrap_or(Vec::new()) + .into_iter() + .map(|s| SanitizedPath::from(s).into()) + .collect() }; let mut order: Vec = serialized @@ -81,13 +85,7 @@ impl PathList { pub fn serialize(&self) -> SerializedPathList { use std::fmt::Write as _; - let mut paths = String::new(); - for path in self.paths.iter() { - if !paths.is_empty() { - paths.push('\n'); - } - paths.push_str(&path.to_string_lossy()); - } + let paths = serde_json::to_string(&self.paths).unwrap_or_default(); let mut order = String::new(); for ix in self.order.iter() { diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c4ba93bcec..39a1e08c93 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -10,11 +10,7 @@ use std::{ use anyhow::{Context as _, Result, bail}; use collections::HashMap; -use db::{ - query, - sqlez::{connection::Connection, domain::Domain}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; @@ -279,189 +275,186 @@ impl sqlez::bindable::Bind for SerializedPixels { } } -pub struct WorkspaceDb(ThreadSafeConnection); +define_connection! { + pub static ref DB: WorkspaceDb<()> = + &[ + sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; -impl Domain for WorkspaceDb { - const NAME: &str = stringify!(WorkspaceDb); + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ), + // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. + sql!( + ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool + ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), + // Add centered_layout field to workspace + sql!( + ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool + ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), + sql!( + DROP TABLE remote_projects; + CREATE TABLE dev_server_projects ( + id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces DROP COLUMN remote_project_id; + ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), + sql!( + ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; + ), + sql!( + CREATE TABLE ssh_projects ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + path TEXT NOT NULL, + user TEXT + ); + ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; + ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), + sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, path TEXT NOT NULL, @@ -473,172 +466,141 @@ impl Domain for WorkspaceDb { ON UPDATE CASCADE ); ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; + CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); + ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; + ), + sql!( + ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL + ), + sql!( + ALTER TABLE breakpoints DROP COLUMN kind + ), + sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), + sql!( + ALTER TABLE breakpoints ADD COLUMN condition TEXT; + ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; + ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ), + sql!( + CREATE TABLE ssh_connections ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + user TEXT + ); - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; + INSERT INTO ssh_connections (host, port, user) + SELECT DISTINCT host, port, user + FROM ssh_projects; - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + paths TEXT, + paths_order TEXT, + ssh_connection_id INTEGER REFERENCES ssh_connections(id), + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB, + left_dock_visible INTEGER, + left_dock_active_panel TEXT, + right_dock_visible INTEGER, + right_dock_active_panel TEXT, + bottom_dock_visible INTEGER, + bottom_dock_active_panel TEXT, + left_dock_zoom INTEGER, + right_dock_zoom INTEGER, + bottom_dock_zoom INTEGER, + fullscreen INTEGER, + centered_layout INTEGER, + session_id TEXT, + window_id INTEGER + ) STRICT; - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths - ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - replace(workspaces.local_paths_array, ',', CHAR(10)) - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DELETE FROM workspaces_2 - WHERE workspace_id NOT IN ( - SELECT MAX(workspace_id) - FROM workspaces_2 - GROUP BY ssh_connection_id, paths - ); - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), - // Fix any data from when workspaces.paths were briefly encoded as JSON arrays - sql!( - UPDATE workspaces - SET paths = CASE - WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN - replace( - substr(paths, 3, length(paths) - 4), - '"' || ',' || '"', - CHAR(10) - ) + INSERT + INTO workspaces_2 + SELECT + workspaces.workspace_id, + CASE + WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths ELSE - replace(paths, ',', CHAR(10)) - END - WHERE paths IS NOT NULL - ), + CASE + WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN + NULL + ELSE + json('[' || '"' || replace(workspaces.local_paths_array, ',', '"' || "," || '"') || '"' || ']') + END + END as paths, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN "" + ELSE workspaces.local_paths_order_array + END as paths_order, + + CASE + WHEN ssh_projects.id IS NOT NULL THEN ( + SELECT ssh_connections.id + FROM ssh_connections + WHERE + ssh_connections.host IS ssh_projects.host AND + ssh_connections.port IS ssh_projects.port AND + ssh_connections.user IS ssh_projects.user + ) + ELSE NULL + END as ssh_connection_id, + + workspaces.timestamp, + workspaces.window_state, + workspaces.window_x, + workspaces.window_y, + workspaces.window_width, + workspaces.window_height, + workspaces.display, + workspaces.left_dock_visible, + workspaces.left_dock_active_panel, + workspaces.right_dock_visible, + workspaces.right_dock_active_panel, + workspaces.bottom_dock_visible, + workspaces.bottom_dock_active_panel, + workspaces.left_dock_zoom, + workspaces.right_dock_zoom, + workspaces.bottom_dock_zoom, + workspaces.fullscreen, + workspaces.centered_layout, + workspaces.session_id, + workspaces.window_id + FROM + workspaces LEFT JOIN + ssh_projects ON + workspaces.ssh_project_id = ssh_projects.id; + + DROP TABLE ssh_projects; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + + CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); + ), ]; - - // Allow recovering from bad migration that was initially shipped to nightly - // when introducing the ssh_connections table. - fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { - old.starts_with("CREATE TABLE ssh_connections") - && new.starts_with("CREATE TABLE ssh_connections") - } } -db::static_connection!(DB, WorkspaceDb, []); - impl WorkspaceDb { /// Returns a serialized workspace for the given worktree_roots. If the passed array /// is empty, the most recent workspace is returned instead. If no workspace for the @@ -1841,7 +1803,6 @@ mod tests { ON DELETE CASCADE ) STRICT; )], - |_, _, _| false, ) .unwrap(); }) @@ -1890,7 +1851,6 @@ mod tests { REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT;)], - |_, _, _| false, ) }) .await diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97..0b4694601e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -613,59 +613,48 @@ impl ProjectItemRegistry { self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { let project_path = project_path.clone(); - let is_file = project - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| entry.is_file()); - let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); + let abs_path = project.read(cx).absolute_path(&project_path, cx); let is_local = project.read(cx).is_local(); let project_item = ::try_open(project, &project_path, cx)?; let project = project.clone(); - Some(window.spawn(cx, async move |cx| { - match project_item.await.with_context(|| { - format!( - "opening project path {:?}", - entry_abs_path.as_deref().unwrap_or(&project_path.path) - ) - }) { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( + Some(window.spawn(cx, async move |cx| match project_item.await { + Ok(project_item) => { + let project_item = project_item; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item( + project, + Some(pane), + project_item, + window, + cx, + ) + })) as Box + }, + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) + } + Err(e) => match abs_path { + Some(abs_path) => match cx.update(|window, cx| { + T::for_broken_project_item(abs_path, is_local, &e, window, cx) + })? { + Some(broken_project_item_view) => { + let build_workspace_item = Box::new( move |_: &mut Pane, _: &mut Window, cx: &mut Context| { cx.new(|_| broken_project_item_view).boxed_clone() }, ) as Box<_>; - Ok((None, build_workspace_item)) - } - None => Err(e)?, - }, + Ok((None, build_workspace_item)) + } None => Err(e)?, }, - } + None => Err(e)?, + }, })) }); } @@ -4022,6 +4011,52 @@ impl Workspace { maybe_pane_handle } + pub fn split_pane_with_item( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + from: WeakEntity, + item_id_to_move: EntityId, + window: &mut Window, + cx: &mut Context, + ) { + let Some(pane_to_split) = pane_to_split.upgrade() else { + return; + }; + let Some(from) = from.upgrade() else { + return; + }; + + let new_pane = self.add_pane(window, cx); + move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + cx.notify(); + } + + pub fn split_pane_with_project_entry( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + project_entry: ProjectEntryId, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let pane_to_split = pane_to_split.upgrade()?; + let new_pane = self.add_pane(window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + + let path = self.project.read(cx).path_for_entry(project_entry, cx)?; + let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx); + Some(cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + })) + } + pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { let active_item = self.active_pane.read(cx).active_item(); for pane in &self.panes { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 553444ebdb..1b9657dcc6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes( }) .detach(); - let mut current_layout_id = cx.keyboard_layout().id().to_string(); + let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); cx.on_keyboard_layout_change(move |cx| { - let next_layout_id = cx.keyboard_layout().id(); - if next_layout_id != current_layout_id { - current_layout_id = next_layout_id.to_string(); + let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); + if next_mapping != current_mapping { + current_mapping = next_mapping; keyboard_layout_tx.unbounded_send(()).ok(); } }) @@ -4434,6 +4434,7 @@ mod tests { assert_eq!(actions_without_namespace, Vec::<&str>::new()); let expected_namespaces = vec![ + "acp", "activity_indicator", "agent", #[cfg(not(target_os = "macos"))] @@ -4729,7 +4730,7 @@ mod tests { // and key strokes contain the given key bindings .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)), + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), "On {} Failed to find {} with key binding {}", line, action.name(), diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs index c37a4cc389..780f7f7626 100644 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ b/crates/zed/src/zed/component_preview/persistence.rs @@ -1,17 +1,10 @@ use anyhow::Result; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; -pub struct ComponentPreviewDb(ThreadSafeConnection); - -impl Domain for ComponentPreviewDb { - const NAME: &str = stringify!(ComponentPreviewDb); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection! { + pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = + &[sql!( CREATE TABLE component_previews ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -20,11 +13,9 @@ impl Domain for ComponentPreviewDb { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } -db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); - impl ComponentPreviewDb { pub async fn save_active_page( &self, diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs index fb5a75f78d..3772104f39 100644 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -72,10 +72,7 @@ impl QuickActionBar { Tooltip::with_meta( tooltip_text, Some(open_action_for_tooltip), - format!( - "{} to open in a split", - text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) - ), + format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), window, cx, ) diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8f4c42ca49..a5223a2cdf 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -284,8 +284,6 @@ pub mod agent { OpenSettings, /// Opens the agent onboarding modal. OpenOnboardingModal, - /// Opens the ACP onboarding modal. - OpenAcpOnboardingModal, /// Resets the agent onboarding state. ResetOnboarding, /// Starts a chat conversation with the agent. diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a4689689..fb139db6e4 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -3243,7 +3243,6 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_size": 20, "auto_reveal_entries": true, "auto_fold_dirs": true, - "drag_and_drop": true, "scrollbar": { "show": null }, diff --git a/docs/src/tasks.md b/docs/src/tasks.md index bff3eac860..9550563432 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true + "show_output": true, // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 4fc5a9ba88..24b2a9d769 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -431,7 +431,6 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir "sticky_scroll": true, // Stick parent directories at top of the project panel. - "drag_and_drop": true, // Whether drag and drop is enabled "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) },